From c640fbd4169cebdd003a0bbbcaff9e0d212d2c2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?POL=20Micka=C3=ABl?= Date: Mon, 6 Apr 2026 05:42:53 +0200 Subject: [PATCH] feat: architecture DDD hexagonale + tooling Vite/TypeScript MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migration complète du monolithe vers une architecture en couches : - Domain : entités, value objects, services purs, ports - Application : CQRS avec CommandBus/QueryBus, 15+ commandes, 9 requêtes - Tooling : Vite + TypeScript strict + Vitest + path aliases Co-Authored-By: Claude Opus 4.6 --- package-lock.json | 2185 +++++++++++- package.json | 44 +- src/application/commands/AddDragodinde.ts | 13 + src/application/commands/CompleteTimer.ts | 52 + src/application/commands/CreateEnclos.ts | 24 + src/application/commands/DeleteEnclos.ts | 17 + src/application/commands/DeleteWorkflow.ts | 13 + src/application/commands/DragodindeActions.ts | 65 + src/application/commands/EnclosActions.ts | 56 + src/application/commands/ImportWorkflows.ts | 25 + src/application/commands/RechargeGauge.ts | 35 + .../commands/RegisterAccouplement.ts | 23 + src/application/commands/RemoveDragodinde.ts | 13 + src/application/commands/ReorderEnclos.ts | 17 + src/application/commands/ResetStats.ts | 11 + src/application/commands/SaveWorkflow.ts | 56 + src/application/commands/StartTimer.ts | 59 + src/application/commands/StopTimer.ts | 13 + src/application/commands/UpdateGauge.ts | 42 + src/application/commands/UpdateSettings.ts | 19 + src/application/commands/UpdateWorkflow.ts | 32 + src/application/handlers/CommandBus.ts | 24 + src/application/handlers/QueryBus.ts | 24 + src/application/queries/GetBreedingOptions.ts | 15 + src/application/queries/GetDashboard.ts | 57 + src/application/queries/GetEnclosDetail.ts | 10 + src/application/queries/GetInventaire.ts | 9 + src/application/queries/GetReapproTree.ts | 22 + src/application/queries/GetSettings.ts | 17 + src/application/queries/GetStatistics.ts | 288 ++ src/application/queries/GetTimerState.ts | 22 + src/application/queries/GetWorkflows.ts | 20 + src/domain/entities/Accouplement.ts | 16 + src/domain/entities/Dragodinde.ts | 37 + src/domain/entities/Enclos.ts | 59 + src/domain/events/DomainEvent.ts | 6 + src/domain/events/EventBus.ts | 24 + src/domain/ports/AlarmPort.ts | 4 + src/domain/ports/NotificationPort.ts | 4 + src/domain/ports/StateRepository.ts | 20 + src/domain/ports/UpdatePort.ts | 11 + src/domain/services/BreedingService.ts | 28 + src/domain/services/GaugeCalculator.ts | 151 + src/domain/services/InventaireCalculator.ts | 69 + src/domain/services/ReapproCalculator.ts | 80 + src/domain/services/SerenityCalculator.ts | 34 + src/domain/services/StockSimulator.ts | 108 + src/domain/services/XpCalculator.ts | 32 + src/domain/value-objects/GaugeType.ts | 49 + src/domain/value-objects/Gender.ts | 1 + src/domain/value-objects/Race.ts | 257 ++ src/domain/value-objects/Tier.ts | 14 + src/domain/value-objects/XpTable.ts | 16 + src/index.html | 3132 +---------------- tsconfig.json | 28 + vite.config.ts | 44 + vitest.config.ts | 28 + 57 files changed, 4419 insertions(+), 3155 deletions(-) mode change 100644 => 100755 package-lock.json mode change 100644 => 100755 package.json create mode 100644 src/application/commands/AddDragodinde.ts create mode 100644 src/application/commands/CompleteTimer.ts create mode 100644 src/application/commands/CreateEnclos.ts create mode 100644 src/application/commands/DeleteEnclos.ts create mode 100644 src/application/commands/DeleteWorkflow.ts create mode 100644 src/application/commands/DragodindeActions.ts create mode 100644 src/application/commands/EnclosActions.ts create mode 100644 src/application/commands/ImportWorkflows.ts create mode 100644 src/application/commands/RechargeGauge.ts create mode 100644 src/application/commands/RegisterAccouplement.ts create mode 100644 src/application/commands/RemoveDragodinde.ts create mode 100644 src/application/commands/ReorderEnclos.ts create mode 100644 src/application/commands/ResetStats.ts create mode 100644 src/application/commands/SaveWorkflow.ts create mode 100644 src/application/commands/StartTimer.ts create mode 100644 src/application/commands/StopTimer.ts create mode 100644 src/application/commands/UpdateGauge.ts create mode 100644 src/application/commands/UpdateSettings.ts create mode 100644 src/application/commands/UpdateWorkflow.ts create mode 100644 src/application/handlers/CommandBus.ts create mode 100644 src/application/handlers/QueryBus.ts create mode 100644 src/application/queries/GetBreedingOptions.ts create mode 100644 src/application/queries/GetDashboard.ts create mode 100644 src/application/queries/GetEnclosDetail.ts create mode 100644 src/application/queries/GetInventaire.ts create mode 100644 src/application/queries/GetReapproTree.ts create mode 100644 src/application/queries/GetSettings.ts create mode 100644 src/application/queries/GetStatistics.ts create mode 100644 src/application/queries/GetTimerState.ts create mode 100644 src/application/queries/GetWorkflows.ts create mode 100644 src/domain/entities/Accouplement.ts create mode 100644 src/domain/entities/Dragodinde.ts create mode 100644 src/domain/entities/Enclos.ts create mode 100644 src/domain/events/DomainEvent.ts create mode 100644 src/domain/events/EventBus.ts create mode 100644 src/domain/ports/AlarmPort.ts create mode 100644 src/domain/ports/NotificationPort.ts create mode 100644 src/domain/ports/StateRepository.ts create mode 100644 src/domain/ports/UpdatePort.ts create mode 100644 src/domain/services/BreedingService.ts create mode 100644 src/domain/services/GaugeCalculator.ts create mode 100644 src/domain/services/InventaireCalculator.ts create mode 100644 src/domain/services/ReapproCalculator.ts create mode 100644 src/domain/services/SerenityCalculator.ts create mode 100644 src/domain/services/StockSimulator.ts create mode 100644 src/domain/services/XpCalculator.ts create mode 100644 src/domain/value-objects/GaugeType.ts create mode 100644 src/domain/value-objects/Gender.ts create mode 100644 src/domain/value-objects/Race.ts create mode 100644 src/domain/value-objects/Tier.ts create mode 100644 src/domain/value-objects/XpTable.ts create mode 100644 tsconfig.json create mode 100644 vite.config.ts create mode 100644 vitest.config.ts diff --git a/package-lock.json b/package-lock.json old mode 100644 new mode 100755 index d6ed2b5..ff1554d --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,87 @@ { "name": "minuteur-dragodinde", - "version": "1.0.0", + "version": "1.1.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "minuteur-dragodinde", - "version": "1.0.0", + "version": "1.1.6", + "dependencies": { + "electron-updater": "^6.8.3" + }, "devDependencies": { + "@playwright/test": "^1.59.1", + "@types/node": "^25.5.0", + "@vitest/coverage-v8": "^4.1.2", "electron": "32.2.7", - "electron-builder": "24.13.3" + "electron-builder": "24.13.3", + "esbuild": "^0.27.4", + "typescript": "^6.0.2", + "vite": "^8.0.3", + "vite-plugin-electron": "^0.29.1", + "vite-plugin-electron-renderer": "^0.14.6", + "vitest": "^4.1.2" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" } }, "node_modules/@develar/schema-utils": { @@ -303,6 +375,482 @@ "node": ">= 10.0.0" } }, + "node_modules/@emnapi/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", + "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -406,6 +954,34 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@malept/cross-spawn-promise": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", @@ -484,6 +1060,33 @@ "node": ">= 10.0.0" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.122.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", + "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -495,6 +1098,284 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", + "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", + "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", + "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", + "dev": true, + "license": "MIT" + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -508,6 +1389,13 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", @@ -531,6 +1419,17 @@ "node": ">= 10" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/cacheable-request": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", @@ -544,6 +1443,17 @@ "@types/responselike": "^1.0.0" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/debug": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", @@ -554,6 +1464,20 @@ "@types/ms": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/fs-extra": { "version": "9.0.13", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", @@ -589,13 +1513,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.37", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", - "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.18.0" } }, "node_modules/@types/plist": { @@ -639,6 +1563,150 @@ "@types/node": "*" } }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.2.tgz", + "integrity": "sha512-sPK//PHO+kAkScb8XITeB1bf7fsk85Km7+rt4eeuRR3VS1/crD47cmV5wicisJmjNdfeokTZwjMk4Mj2d58Mgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.2", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.2", + "vitest": "4.1.2" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", + "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz", + "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.2", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz", + "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz", + "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.2", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz", + "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.2", + "@vitest/utils": "4.1.2", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz", + "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz", + "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.2", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@xmldom/xmldom": { "version": "0.8.11", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", @@ -906,7 +1974,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/assert-plus": { @@ -920,6 +1987,28 @@ "node": ">=0.8" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", + "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -1217,6 +2306,16 @@ "node": ">= 0.4" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1449,6 +2548,27 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/config-file-ts/node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -1515,7 +2635,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1616,6 +2735,16 @@ "node": ">=0.4.0" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-node": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", @@ -1990,6 +3119,99 @@ "node": ">= 10.0.0" } }, + "node_modules/electron-updater": { + "version": "6.8.3", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.8.3.tgz", + "integrity": "sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ==", + "license": "MIT", + "dependencies": { + "builder-util-runtime": "9.5.1", + "fs-extra": "^10.1.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "lodash.escaperegexp": "^4.1.2", + "lodash.isequal": "^4.5.0", + "semver": "~7.7.3", + "tiny-typed-emitter": "^2.1.0" + } + }, + "node_modules/electron-updater/node_modules/builder-util-runtime": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", + "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/electron-updater/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-updater/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-updater/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-updater/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron/node_modules/@types/node": { + "version": "20.19.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", + "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/electron/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -2044,6 +3266,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -2081,6 +3310,48 @@ "license": "MIT", "optional": true }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2105,6 +3376,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -2161,6 +3452,24 @@ "pend": "~1.2.0" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/filelist": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", @@ -2261,6 +3570,21 @@ "dev": true, "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -2476,7 +3800,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/has-flag": { @@ -2558,6 +3881,13 @@ "node": ">=10" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/http-cache-semantics": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", @@ -2730,6 +4060,45 @@ "dev": true, "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -2764,11 +4133,17 @@ "node": ">=10" } }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -2836,7 +4211,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", - "dev": true, "license": "MIT" }, "node_modules/lazystream": { @@ -2889,6 +4263,267 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lodash": { "version": "4.17.23", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", @@ -2912,6 +4547,12 @@ "license": "MIT", "peer": true }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, "node_modules/lodash.flatten": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", @@ -2920,6 +4561,13 @@ "license": "MIT", "peer": true }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", @@ -2959,6 +4607,57 @@ "node": ">=10" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -3106,9 +4805,27 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/node-addon-api": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", @@ -3152,6 +4869,17 @@ "node": ">= 0.4" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -3223,6 +4951,13 @@ "dev": true, "license": "ISC" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -3237,6 +4972,66 @@ "dev": true, "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", @@ -3252,6 +5047,35 @@ "node": ">=10.4.0" } }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -3422,6 +5246,40 @@ "node": ">=8.0" } }, + "node_modules/rolldown": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", + "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.122.0", + "@rolldown/pluginutils": "1.0.0-rc.12" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-x64": "1.0.0-rc.12", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -3465,7 +5323,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" @@ -3529,6 +5386,13 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -3606,6 +5470,16 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -3625,6 +5499,13 @@ "license": "BSD-3-Clause", "optional": true }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/stat-mode": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", @@ -3635,6 +5516,13 @@ "node": ">= 6" } }, + "node_modules/std-env": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "dev": true, + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -3816,6 +5704,56 @@ "node": ">= 10.0.0" } }, + "node_modules/tiny-typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", + "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tmp": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", @@ -3846,6 +5784,14 @@ "utf8-byte-length": "^1.0.1" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/type-fest": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", @@ -3861,9 +5807,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3875,9 +5821,9 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "dev": true, "license": "MIT" }, @@ -3932,6 +5878,188 @@ "node": ">=0.6.0" } }, + "node_modules/vite": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", + "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.12", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-electron": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/vite-plugin-electron/-/vite-plugin-electron-0.29.1.tgz", + "integrity": "sha512-AejNed5BgHFnuw8h5puTa61C6vdP4ydbsbo/uVjH1fTdHAlCDz1+o6pDQ/scQj1udDrGvH01+vTbzQh/vMnR9w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "vite-plugin-electron-renderer": "*" + }, + "peerDependenciesMeta": { + "vite-plugin-electron-renderer": { + "optional": true + } + } + }, + "node_modules/vite-plugin-electron-renderer": { + "version": "0.14.6", + "resolved": "https://registry.npmjs.org/vite-plugin-electron-renderer/-/vite-plugin-electron-renderer-0.14.6.tgz", + "integrity": "sha512-oqkWFa7kQIkvHXG7+Mnl1RTroA4sP0yesKatmAy0gjZC4VwUqlvF9IvOpHd1fpLWsqYX/eZlVxlhULNtaQ78Jw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitest": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz", + "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.2", + "@vitest/mocker": "4.1.2", + "@vitest/pretty-format": "4.1.2", + "@vitest/runner": "4.1.2", + "@vitest/snapshot": "4.1.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.2", + "@vitest/browser-preview": "4.1.2", + "@vitest/browser-webdriverio": "4.1.2", + "@vitest/ui": "4.1.2", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3948,6 +6076,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json old mode 100644 new mode 100755 index 755503a..214e999 --- a/package.json +++ b/package.json @@ -1,24 +1,32 @@ { "name": "minuteur-dragodinde", - "version": "1.1.5", + "version": "1.1.6", "description": "Minuteur elevage Dragodinde Dofus 3", - "main": "main.js", + "main": "dist-electron/main.js", "author": "Mickael", "scripts": { - "start": "electron .", - "build": "electron-builder --win --x64" + "dev": "vite", + "build": "vite build && electron-builder --win --x64", + "start": "npm run dev", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "test:e2e": "npx playwright test" }, "build": { "appId": "fr.mickael-pol.minuteur-dragodinde", + "publish": { + "provider": "generic", + "url": "https://gitea.mickael-pol.fr/mickael/dd-timer/releases/download/latest" + }, "productName": "Minuteur Dragodinde", "directories": { "output": "dist" }, "files": [ - "main.js", - "preload.js", - "src/**/*", - "icon.png" + "dist-vite/**/*", + "dist-electron/**/*", + "icon.ico" ], "win": { "target": [ @@ -31,7 +39,7 @@ ], "sign": null, "signingHashAlgorithms": [], - "icon": "icon.png", + "icon": "icon.ico", "requestedExecutionLevel": "asInvoker" }, "nsis": { @@ -46,12 +54,24 @@ } }, "devDependencies": { + "@playwright/test": "^1.59.1", + "@types/node": "^25.5.0", + "@vitest/coverage-v8": "^4.1.2", "electron": "32.2.7", - "electron-builder": "24.13.3" + "electron-builder": "24.13.3", + "esbuild": "^0.27.4", + "typescript": "^6.0.2", + "vite": "^8.0.3", + "vite-plugin-electron": "^0.29.1", + "vite-plugin-electron-renderer": "^0.14.6", + "vitest": "^4.1.2" }, "repository": { "type": "git", "url": "https://gitea.mickael-pol.fr/mickael/dd-timer.git" }, - "productName": "Minuteur Dragodinde" -} \ No newline at end of file + "productName": "Minuteur Dragodinde", + "dependencies": { + "electron-updater": "^6.8.3" + } +} diff --git a/src/application/commands/AddDragodinde.ts b/src/application/commands/AddDragodinde.ts new file mode 100644 index 0000000..d79e7f0 --- /dev/null +++ b/src/application/commands/AddDragodinde.ts @@ -0,0 +1,13 @@ +import type { AppState, StateRepository } from '@domain/ports/StateRepository'; +import { addDragodinde } from '@domain/entities/Enclos'; + +export interface AddDragodindeCommand { type: 'add-dragodinde'; enclosId: number; } + +export function createAddDragodindeHandler(state: AppState, repo: StateRepository) { + return (cmd: AddDragodindeCommand): void => { + const idx = state.enclos.findIndex(e => e.id === cmd.enclosId); + if (idx < 0) return; + state.enclos[idx] = addDragodinde(state.enclos[idx]!); + repo.save(state); + }; +} diff --git a/src/application/commands/CompleteTimer.ts b/src/application/commands/CompleteTimer.ts new file mode 100644 index 0000000..4fc5f6a --- /dev/null +++ b/src/application/commands/CompleteTimer.ts @@ -0,0 +1,52 @@ +import type { AppState, StateRepository } from '@domain/ports/StateRepository'; +import type { EventBus } from '@domain/events/EventBus'; +import { GAUGE_DEFS, STAT_DEFS } from '@domain/value-objects/GaugeType'; +import { computeGaugeState } from '@domain/services/GaugeCalculator'; +import { xpForLevel, levelFromXp } from '@domain/value-objects/XpTable'; + +export interface CompleteTimerCommand { type: 'complete-timer'; enclosId: number; } + +export function createCompleteTimerHandler(state: AppState, repo: StateRepository, events: EventBus) { + return (cmd: CompleteTimerCommand): void => { + const enc = state.enclos.find(e => e.id === cmd.enclosId); + if (!enc || !enc.timer.running || enc.alerted['__done__']) return; + + const now = Date.now(); + enc.timer.running = false; + enc.timer.pausedAt = now; + enc.alerted['__done__'] = true; + + // Persister les stats finales de chaque DD dans dd.stats + // pour que la session suivante parte des bonnes valeurs. + const el = (now - enc.timer.startTime! - enc.timer.pausedMs) / 1000; + for (const dd of enc.dragodindes) { + for (const gid of enc.activeGauges) { + const def = GAUGE_DEFS[gid]; + const startGl = enc.timer.snapGauges[gid] ?? enc.gaugeLevels[gid]; + const startSt = (enc.timer.snapStats[dd.id]?.[def.stat] ?? (dd.stats as Record)[def.stat]) as number; + const recharges = enc.timer.gaugeRecharges[gid] ?? []; + + let pts: number; + if (def.isXp) { + pts = Math.max(0, xpForLevel(200) - xpForLevel(startSt)); + } else { + const sd = STAT_DEFS[def.stat]; + pts = def.dir > 0 ? Math.max(0, sd.max - startSt) : Math.max(0, startSt - sd.min); + } + + const { gained } = computeGaugeState(startGl, recharges, pts, el); + + if (def.isXp) { + dd.stats.xp = Math.min(200, Math.max(1, levelFromXp(xpForLevel(startSt) + gained))); + } else { + const sd = STAT_DEFS[def.stat]; + const raw = startSt + def.dir * gained; + (dd.stats as Record)[def.stat] = Math.min(sd.max, Math.max(sd.min, Math.round(raw))); + } + } + } + + repo.save(state); + events.emit({ type: 'timer-completed', enclosName: enc.name }); + }; +} diff --git a/src/application/commands/CreateEnclos.ts b/src/application/commands/CreateEnclos.ts new file mode 100644 index 0000000..1d7cb50 --- /dev/null +++ b/src/application/commands/CreateEnclos.ts @@ -0,0 +1,24 @@ +import type { AppState, StateRepository } from '@domain/ports/StateRepository'; +import { createEnclos, addDragodinde, MAX_ENCLOS } from '@domain/entities/Enclos'; + +export interface CreateEnclosCommand { type: 'create-enclos'; } + +export function createCreateEnclosHandler(state: AppState, repo: StateRepository) { + return (_cmd: CreateEnclosCommand): void => { + if (state.enclos.length >= MAX_ENCLOS) return; + // Gap-filling : premier numéro de slot (1..MAX_ENCLOS) absent des noms existants "Enclos N" + const usedSlots = new Set(); + for (const e of state.enclos) { + const m = e.name.match(/^Enclos (\d+)$/); + if (m) usedSlots.add(Number(m[1])); + } + let slot = 1; + while (usedSlots.has(slot)) slot++; + let enc = createEnclos(state.nextEnclosId, `Enclos ${slot}`); + enc = addDragodinde(enc); // Always start with 1 DD + state.enclos.push(enc); + state.nextEnclosId++; + state.activeId = enc.id; + repo.save(state); + }; +} diff --git a/src/application/commands/DeleteEnclos.ts b/src/application/commands/DeleteEnclos.ts new file mode 100644 index 0000000..e5f41ac --- /dev/null +++ b/src/application/commands/DeleteEnclos.ts @@ -0,0 +1,17 @@ +import type { AppState, StateRepository } from '@domain/ports/StateRepository'; +import type { EventBus } from '@domain/events/EventBus'; + +export interface DeleteEnclosCommand { type: 'delete-enclos'; enclosId: number; } + +export function createDeleteEnclosHandler(state: AppState, repo: StateRepository, events: EventBus) { + return (cmd: DeleteEnclosCommand): void => { + const idx = state.enclos.findIndex(e => e.id === cmd.enclosId); + if (idx < 0) return; + state.enclos.splice(idx, 1); + if (state.activeId === cmd.enclosId) { + state.activeId = state.enclos.length > 0 ? state.enclos[0]!.id : 'dashboard'; + } + events.emit({ type: 'enclos-deleted', enclosId: cmd.enclosId }); + repo.save(state); + }; +} diff --git a/src/application/commands/DeleteWorkflow.ts b/src/application/commands/DeleteWorkflow.ts new file mode 100644 index 0000000..de655ef --- /dev/null +++ b/src/application/commands/DeleteWorkflow.ts @@ -0,0 +1,13 @@ +import type { AppState, StateRepository } from '@domain/ports/StateRepository'; + +export interface DeleteWorkflowCommand { + type: 'delete-workflow'; + workflowId: number; +} + +export function createDeleteWorkflowHandler(state: AppState, repo: StateRepository) { + return (cmd: DeleteWorkflowCommand): void => { + state.workflows = (state.workflows as { id: number }[]).filter(w => w.id !== cmd.workflowId); + repo.save(state); + }; +} diff --git a/src/application/commands/DragodindeActions.ts b/src/application/commands/DragodindeActions.ts new file mode 100644 index 0000000..90e6121 --- /dev/null +++ b/src/application/commands/DragodindeActions.ts @@ -0,0 +1,65 @@ +import type { AppState, StateRepository } from '@domain/ports/StateRepository'; +import type { StatType } from '@domain/value-objects/GaugeType'; + +export interface RenameDragodindeCommand { type: 'rename-dragodinde'; enclosId: number; ddId: number; name: string; } +export interface UpdateDdStatCommand { type: 'update-dd-stat'; enclosId: number; ddId: number; stat: StatType; value: number; } +export interface UpdateDdSerenTargetCmd { type: 'update-dd-seren-target'; enclosId: number; ddId: number; target: number | null; } +export interface UpdateDdLevelTargetCmd { type: 'update-dd-level-target'; enclosId: number; ddId: number; target: number | null; } +export interface ReorderDragodindeCommand { type: 'reorder-dragodinde'; enclosId: number; fromDdId: number; toDdId: number; } + +export function createRenameDragodindeHandler(state: AppState, repo: StateRepository) { + return (cmd: RenameDragodindeCommand): void => { + const enc = state.enclos.find(e => e.id === cmd.enclosId); + if (!enc) return; + const dd = enc.dragodindes.find(d => d.id === cmd.ddId); + if (!dd || !cmd.name.trim()) return; + dd.name = cmd.name.trim(); + repo.save(state); + }; +} + +export function createUpdateDdStatHandler(state: AppState, repo: StateRepository) { + return (cmd: UpdateDdStatCommand): void => { + const enc = state.enclos.find(e => e.id === cmd.enclosId); + if (!enc) return; + const dd = enc.dragodindes.find(d => d.id === cmd.ddId); + if (!dd) return; + dd.stats[cmd.stat] = cmd.value; + repo.save(state); + }; +} + +export function createUpdateDdSerenTargetHandler(state: AppState, repo: StateRepository) { + return (cmd: UpdateDdSerenTargetCmd): void => { + const enc = state.enclos.find(e => e.id === cmd.enclosId); + if (!enc) return; + const dd = enc.dragodindes.find(d => d.id === cmd.ddId); + if (!dd) return; + dd.sereniteTarget = cmd.target; + repo.save(state); + }; +} + +export function createUpdateDdLevelTargetHandler(state: AppState, repo: StateRepository) { + return (cmd: UpdateDdLevelTargetCmd): void => { + const enc = state.enclos.find(e => e.id === cmd.enclosId); + if (!enc) return; + const dd = enc.dragodindes.find(d => d.id === cmd.ddId); + if (!dd) return; + dd.levelTarget = cmd.target; + repo.save(state); + }; +} + +export function createReorderDragodindeHandler(state: AppState, repo: StateRepository) { + return (cmd: ReorderDragodindeCommand): void => { + const enc = state.enclos.find(e => e.id === cmd.enclosId); + if (!enc) return; + const fromIdx = enc.dragodindes.findIndex(d => d.id === cmd.fromDdId); + const toIdx = enc.dragodindes.findIndex(d => d.id === cmd.toDdId); + if (fromIdx < 0 || toIdx < 0) return; + const [item] = enc.dragodindes.splice(fromIdx, 1); + enc.dragodindes.splice(toIdx, 0, item!); + repo.save(state); + }; +} diff --git a/src/application/commands/EnclosActions.ts b/src/application/commands/EnclosActions.ts new file mode 100644 index 0000000..fe107da --- /dev/null +++ b/src/application/commands/EnclosActions.ts @@ -0,0 +1,56 @@ +import type { AppState, StateRepository } from '@domain/ports/StateRepository'; +import { createDragodinde } from '@domain/entities/Dragodinde'; + +export interface ClearEnclosCommand { type: 'clear-enclos'; enclosId: number; } +export interface RenameEnclosCommand { type: 'rename-enclos'; enclosId: number; name: string; } +export interface ResetTimerCommand { type: 'reset-timer'; enclosId: number; } +export interface NouvelleFourneeCommand { type: 'nouvelle-fournee'; enclosId: number; } + +export function createClearEnclosHandler(state: AppState, repo: StateRepository) { + return (cmd: ClearEnclosCommand): void => { + const enc = state.enclos.find(e => e.id === cmd.enclosId); + if (!enc) return; + enc.dragodindes = []; + enc.nextDdId = 1; + enc.activeGauges = []; + enc.gaugeLevels = { baffeur: 0, caresseur: 0, foudroyeur: 0, abreuvoir: 0, dragofesse: 0, mangeoire: 0 }; + enc.timer = { running: false, startTime: null, pausedAt: null, pausedMs: 0, snapGauges: {}, snapStats: {}, gaugeRecharges: {} }; + enc.alerted = {}; + repo.save(state); + }; +} + +export function createRenameEnclosHandler(state: AppState, repo: StateRepository) { + return (cmd: RenameEnclosCommand): void => { + const enc = state.enclos.find(e => e.id === cmd.enclosId); + if (!enc || !cmd.name.trim()) return; + enc.name = cmd.name.trim(); + repo.save(state); + }; +} + +export function createResetTimerHandler(state: AppState, repo: StateRepository) { + return (cmd: ResetTimerCommand): void => { + const enc = state.enclos.find(e => e.id === cmd.enclosId); + if (!enc) return; + enc.timer = { running: false, startTime: null, pausedAt: null, pausedMs: 0, snapGauges: {}, snapStats: {}, gaugeRecharges: {} }; + enc.alerted = {}; + repo.save(state); + }; +} + +export function createNouvelleFourneeHandler(state: AppState, repo: StateRepository) { + return (cmd: NouvelleFourneeCommand): void => { + const enc = state.enclos.find(e => e.id === cmd.enclosId); + if (!enc) return; + // Reset timer et état de session + enc.timer = { running: false, startTime: null, pausedAt: null, pausedMs: 0, snapGauges: {}, snapStats: {}, gaugeRecharges: {} }; + enc.alerted = {}; + // Remet les niveaux de jauges à 0 + enc.gaugeLevels = { baffeur: 0, caresseur: 0, foudroyeur: 0, abreuvoir: 0, dragofesse: 0, mangeoire: 0 }; + // Vide toutes les DDs et en ajoute une nouvelle avec les stats de base + enc.dragodindes = [createDragodinde(1)]; + enc.nextDdId = 2; + repo.save(state); + }; +} diff --git a/src/application/commands/ImportWorkflows.ts b/src/application/commands/ImportWorkflows.ts new file mode 100644 index 0000000..43b21e1 --- /dev/null +++ b/src/application/commands/ImportWorkflows.ts @@ -0,0 +1,25 @@ +import type { AppState, StateRepository } from '@domain/ports/StateRepository'; +import type { WorkflowItem } from '@application/queries/GetWorkflows'; + +export interface ImportWorkflowsCommand { + type: 'import-workflows'; + workflows: WorkflowItem[]; +} + +export function createImportWorkflowsHandler(state: AppState, repo: StateRepository) { + return (cmd: ImportWorkflowsCommand): void => { + const existing = state.workflows as WorkflowItem[]; + const existingIds = new Set(existing.map(w => w.id)); + + for (const wf of cmd.workflows) { + if (existingIds.has(wf.id)) { + // Réattribuer un nouvel id pour éviter les doublons + wf.id = Date.now() + Math.floor(Math.random() * 1000); + } + existing.push(wf); + existingIds.add(wf.id); + } + + repo.save(state); + }; +} diff --git a/src/application/commands/RechargeGauge.ts b/src/application/commands/RechargeGauge.ts new file mode 100644 index 0000000..fd70b6a --- /dev/null +++ b/src/application/commands/RechargeGauge.ts @@ -0,0 +1,35 @@ +import type { AppState, StateRepository } from '@domain/ports/StateRepository'; +import type { GaugeType } from '@domain/value-objects/GaugeType'; +import { elapsed } from '@domain/services/GaugeCalculator'; + +export interface RechargeGaugeCommand { + type: 'recharge-gauge'; + enclosId: number; + gaugeId: GaugeType; + level: number; +} + +export function createRechargeGaugeHandler(state: AppState, repo: StateRepository) { + return (cmd: RechargeGaugeCommand): void => { + const enc = state.enclos.find(e => e.id === cmd.enclosId); + if (!enc || !enc.timer.startTime) return; + + const atSec = elapsed(enc.timer); + if (!enc.timer.gaugeRecharges[cmd.gaugeId]) enc.timer.gaugeRecharges[cmd.gaugeId] = []; + + // Consolider : si la dernière recharge est à moins de 5s, la remplacer + // (évite de polluer le tableau lors de la saisie en temps réel) + const arr = enc.timer.gaugeRecharges[cmd.gaugeId]; + const last = arr.length > 0 ? arr[arr.length - 1] : null; + if (last && Math.abs(atSec - last.atSec) < 2) { + last.atSec = atSec; + last.level = cmd.level; + } else { + arr.push({ atSec, level: cmd.level }); + } + + // Mettre à jour gaugeLevels pour l'affichage de l'input + enc.gaugeLevels[cmd.gaugeId] = cmd.level; + repo.save(state); + }; +} diff --git a/src/application/commands/RegisterAccouplement.ts b/src/application/commands/RegisterAccouplement.ts new file mode 100644 index 0000000..9e61072 --- /dev/null +++ b/src/application/commands/RegisterAccouplement.ts @@ -0,0 +1,23 @@ +import type { AppState, StateRepository } from '@domain/ports/StateRepository'; +import { createAccouplement } from '@domain/entities/Accouplement'; +import type { EventBus } from '@domain/events/EventBus'; +import { RACE_GEN } from '@domain/value-objects/Race'; + +export interface RegisterAccouplementCommand { + type: 'register-accouplement'; + parentA: string; + parentB: string; + baby: string; + couples: number; + babiesObtained: number; +} + +export function createRegisterAccouplementHandler(state: AppState, repo: StateRepository, events: EventBus) { + return (cmd: RegisterAccouplementCommand): void => { + const gen = RACE_GEN[cmd.baby] ?? 0; + const acc = createAccouplement(cmd.parentA, cmd.parentB, cmd.baby, gen, cmd.couples, cmd.babiesObtained); + state.accouplements.push(acc); + events.emit({ type: 'accouplement-registered', accouplement: acc }); + repo.save(state); + }; +} diff --git a/src/application/commands/RemoveDragodinde.ts b/src/application/commands/RemoveDragodinde.ts new file mode 100644 index 0000000..079b44b --- /dev/null +++ b/src/application/commands/RemoveDragodinde.ts @@ -0,0 +1,13 @@ +import type { AppState, StateRepository } from '@domain/ports/StateRepository'; +import { removeDragodinde } from '@domain/entities/Enclos'; + +export interface RemoveDragodindeCommand { type: 'remove-dragodinde'; enclosId: number; ddId: number; } + +export function createRemoveDragodindeHandler(state: AppState, repo: StateRepository) { + return (cmd: RemoveDragodindeCommand): void => { + const idx = state.enclos.findIndex(e => e.id === cmd.enclosId); + if (idx < 0) return; + state.enclos[idx] = removeDragodinde(state.enclos[idx]!, cmd.ddId); + repo.save(state); + }; +} diff --git a/src/application/commands/ReorderEnclos.ts b/src/application/commands/ReorderEnclos.ts new file mode 100644 index 0000000..3001ca0 --- /dev/null +++ b/src/application/commands/ReorderEnclos.ts @@ -0,0 +1,17 @@ +import type { AppState, StateRepository } from '@domain/ports/StateRepository'; + +export interface ReorderEnclosCommand { + type: 'reorder-enclos'; + fromIndex: number; + toIndex: number; +} + +export function createReorderEnclosHandler(state: AppState, repo: StateRepository) { + return (cmd: ReorderEnclosCommand): void => { + if (cmd.fromIndex < 0 || cmd.toIndex < 0) return; + if (cmd.fromIndex >= state.enclos.length || cmd.toIndex >= state.enclos.length) return; + const [moved] = state.enclos.splice(cmd.fromIndex, 1); + if (moved) state.enclos.splice(cmd.toIndex, 0, moved); + repo.save(state); + }; +} diff --git a/src/application/commands/ResetStats.ts b/src/application/commands/ResetStats.ts new file mode 100644 index 0000000..92cdeab --- /dev/null +++ b/src/application/commands/ResetStats.ts @@ -0,0 +1,11 @@ +import type { AppState, StateRepository } from '@domain/ports/StateRepository'; + +export interface ResetStatsCommand { type: 'reset-stats'; } + +export function createResetStatsHandler(state: AppState, repo: StateRepository) { + return (_cmd: ResetStatsCommand): void => { + state.archivedStats = []; + state.accouplements = []; + repo.save(state); + }; +} diff --git a/src/application/commands/SaveWorkflow.ts b/src/application/commands/SaveWorkflow.ts new file mode 100644 index 0000000..ef25204 --- /dev/null +++ b/src/application/commands/SaveWorkflow.ts @@ -0,0 +1,56 @@ +import type { AppState, StateRepository } from '@domain/ports/StateRepository'; +import type { WorkflowItem } from '@application/queries/GetWorkflows'; + +export interface SaveWorkflowCommand { + type: 'save-workflow'; + target: string; + qty: number; + materials: { race: string; m: number; f: number }[]; + steps: { baby: string; parentA: string; parentB: string; couples: number; gen: number }[]; + repro: Record; +} + +export function createSaveWorkflowHandler(state: AppState, repo: StateRepository) { + return (cmd: SaveWorkflowCommand): void => { + const workflows = state.workflows as WorkflowItem[]; + + // Group steps by gen (already sorted gen2 → genN after calcAppro reverse) + const genMap = new Map(); + for (const step of cmd.steps) { + if (!genMap.has(step.gen)) genMap.set(step.gen, []); + genMap.get(step.gen)!.push(step); + } + + const wfSteps = Array.from(genMap.entries()) + .sort(([a], [b]) => a - b) + .map(([gen, steps]) => ({ + gen, + crossings: steps.map(s => ({ + race: s.baby, + needed: s.couples, + parentA: s.parentA, + parentB: s.parentB, + couples: s.couples, + repro: cmd.repro[s.baby] ?? 0, + done: 0, + })), + })); + + const wf: WorkflowItem = { + id: Date.now(), + name: `${cmd.target} ×${cmd.qty}`, + target: cmd.target, + qty: cmd.qty, + createdAt: Date.now(), + materials: cmd.materials.map(m => ({ + name: m.race, + needed: m.m + m.f, + done: 0, + })), + steps: wfSteps, + }; + + workflows.push(wf); + repo.save(state); + }; +} diff --git a/src/application/commands/StartTimer.ts b/src/application/commands/StartTimer.ts new file mode 100644 index 0000000..7e54a42 --- /dev/null +++ b/src/application/commands/StartTimer.ts @@ -0,0 +1,59 @@ +import type { AppState, StateRepository } from '@domain/ports/StateRepository'; +import { GAUGE_DEFS, STAT_DEFS } from '@domain/value-objects/GaugeType'; +import { xpForLevel } from '@domain/value-objects/XpTable'; + +export interface StartTimerCommand { type: 'start-timer'; enclosId: number; } + +export function createStartTimerHandler(state: AppState, repo: StateRepository) { + return (cmd: StartTimerCommand): void => { + const enc = state.enclos.find(e => e.id === cmd.enclosId); + if (!enc || enc.timer.running) return; + if (!enc.activeGauges.length || !enc.dragodindes.length) return; + + // Ne pas démarrer si toutes les jauges actives sont à 0 (aucun point ne sera gagné) + const allGaugesEmpty = enc.activeGauges.every(gid => (enc.gaugeLevels[gid] ?? 0) <= 0); + if (allGaugesEmpty) return; + + const now = Date.now(); + + if (enc.timer.startTime !== null && enc.timer.pausedAt !== null && !enc.alerted['__done__']) { + // Reprise depuis une pause manuelle : accumuler le temps pausé + enc.timer.pausedMs += now - enc.timer.pausedAt; + enc.timer.pausedAt = null; + enc.timer.running = true; + } else { + // Démarrage initial — vérifier qu'au moins une cible n'est pas déjà atteinte + const allTargetsAlreadyMet = enc.dragodindes.every(dd => { + return enc.activeGauges.every(gid => { + const def = GAUGE_DEFS[gid]; + const stat = (dd.stats as Record)[def.stat] as number; + if (def.isXp) { + const target = dd.levelTarget ?? 200; + return stat >= target; + } + const sd = STAT_DEFS[def.stat]; + if (def.stat === 'serenite' && dd.sereniteTarget !== null && dd.sereniteTarget !== undefined) { + return def.dir > 0 ? stat >= dd.sereniteTarget : stat <= dd.sereniteTarget; + } + return def.dir > 0 ? stat >= sd.max : stat <= sd.min; + }); + }); + if (allTargetsAlreadyMet) return; // Ne pas démarrer si tout est déjà atteint + + enc.timer.running = true; + enc.timer.startTime = now; + enc.timer.pausedAt = null; + enc.timer.pausedMs = 0; + enc.timer.snapGauges = { ...enc.gaugeLevels }; + enc.timer.gaugeRecharges = {}; + const snapStats: Record> = {}; + for (const dd of enc.dragodindes) { + snapStats[dd.id] = { ...dd.stats }; + } + enc.timer.snapStats = snapStats; + enc.alerted = {}; + } + + repo.save(state); + }; +} diff --git a/src/application/commands/StopTimer.ts b/src/application/commands/StopTimer.ts new file mode 100644 index 0000000..248aa40 --- /dev/null +++ b/src/application/commands/StopTimer.ts @@ -0,0 +1,13 @@ +import type { AppState, StateRepository } from '@domain/ports/StateRepository'; + +export interface StopTimerCommand { type: 'stop-timer'; enclosId: number; } + +export function createStopTimerHandler(state: AppState, repo: StateRepository) { + return (cmd: StopTimerCommand): void => { + const enc = state.enclos.find(e => e.id === cmd.enclosId); + if (!enc || !enc.timer.running) return; + enc.timer.running = false; + enc.timer.pausedAt = Date.now(); + repo.save(state); + }; +} diff --git a/src/application/commands/UpdateGauge.ts b/src/application/commands/UpdateGauge.ts new file mode 100644 index 0000000..0e95943 --- /dev/null +++ b/src/application/commands/UpdateGauge.ts @@ -0,0 +1,42 @@ +import type { AppState, StateRepository } from '@domain/ports/StateRepository'; +import type { GaugeType } from '@domain/value-objects/GaugeType'; +import { MAX_GAUGES } from '@domain/entities/Enclos'; + +export interface ToggleGaugeCommand { type: 'toggle-gauge'; enclosId: number; gaugeId: GaugeType; } +export interface UpdateGaugeLevelCommand { type: 'update-gauge-level'; enclosId: number; gaugeId: GaugeType; level: number; } + +export function createToggleGaugeHandler(state: AppState, repo: StateRepository) { + return (cmd: ToggleGaugeCommand): void => { + const enc = state.enclos.find(e => e.id === cmd.enclosId); + if (!enc || enc.timer.running) return; + const i = enc.activeGauges.indexOf(cmd.gaugeId); + if (i >= 0) { + enc.activeGauges.splice(i, 1); + } else { + // Exclusion mutuelle baffeur/caresseur : même stat, directions opposées + const SEREN_PAIR: Record = { baffeur: 'caresseur', caresseur: 'baffeur' }; + const opposite = SEREN_PAIR[cmd.gaugeId]; + if (opposite) { + const oi = enc.activeGauges.indexOf(opposite); + if (oi >= 0) enc.activeGauges.splice(oi, 1); + } + if (enc.activeGauges.length >= MAX_GAUGES) enc.activeGauges.shift(); + enc.activeGauges.push(cmd.gaugeId); + } + // Reset timer if gauge changed after a completed session + if (enc.timer.startTime && !enc.timer.running) { + enc.timer = { running: false, startTime: null, pausedAt: null, pausedMs: 0, snapGauges: {}, snapStats: {} }; + enc.alerted = {}; + } + repo.save(state); + }; +} + +export function createUpdateGaugeLevelHandler(state: AppState, repo: StateRepository) { + return (cmd: UpdateGaugeLevelCommand): void => { + const enc = state.enclos.find(e => e.id === cmd.enclosId); + if (!enc) return; + enc.gaugeLevels[cmd.gaugeId] = Math.max(0, Math.min(100000, cmd.level)); + repo.save(state); + }; +} diff --git a/src/application/commands/UpdateSettings.ts b/src/application/commands/UpdateSettings.ts new file mode 100644 index 0000000..abc90f9 --- /dev/null +++ b/src/application/commands/UpdateSettings.ts @@ -0,0 +1,19 @@ +import type { AppState, StateRepository } from '@domain/ports/StateRepository'; + +export interface UpdateSettingsCommand { + type: 'update-settings'; + alarmSound?: string; + notifsEnabled?: boolean; + ntfyTopic?: string; + inventaire?: Record; +} + +export function createUpdateSettingsHandler(state: AppState, repo: StateRepository) { + return (cmd: UpdateSettingsCommand): void => { + if (cmd.alarmSound !== undefined) state.alarmSound = cmd.alarmSound; + if (cmd.notifsEnabled !== undefined) state.notifsEnabled = cmd.notifsEnabled; + if (cmd.ntfyTopic !== undefined) state.ntfyTopic = cmd.ntfyTopic; + if (cmd.inventaire !== undefined) state.inventaire = cmd.inventaire; + repo.save(state); + }; +} diff --git a/src/application/commands/UpdateWorkflow.ts b/src/application/commands/UpdateWorkflow.ts new file mode 100644 index 0000000..a8b9f93 --- /dev/null +++ b/src/application/commands/UpdateWorkflow.ts @@ -0,0 +1,32 @@ +import type { AppState, StateRepository } from '@domain/ports/StateRepository'; +import type { WorkflowItem } from '@application/queries/GetWorkflows'; + +export interface UpdateWorkflowCommand { + type: 'update-workflow'; + workflowId: number; + materialIdx?: number; + stepIdx?: number; + crossingIdx?: number; + done: number; +} + +export function createUpdateWorkflowHandler(state: AppState, repo: StateRepository) { + return (cmd: UpdateWorkflowCommand): void => { + const workflows = state.workflows as WorkflowItem[]; + const wf = workflows.find(w => w.id === cmd.workflowId); + if (!wf) return; + + if (cmd.materialIdx !== undefined) { + const mat = wf.materials[cmd.materialIdx]; + if (mat) mat.done = Math.max(0, cmd.done); + } else if (cmd.stepIdx !== undefined && cmd.crossingIdx !== undefined) { + const step = wf.steps[cmd.stepIdx]; + if (step) { + const crossing = step.crossings[cmd.crossingIdx]; + if (crossing) crossing.done = Math.max(0, cmd.done); + } + } + + repo.save(state); + }; +} diff --git a/src/application/handlers/CommandBus.ts b/src/application/handlers/CommandBus.ts new file mode 100644 index 0000000..0e6e576 --- /dev/null +++ b/src/application/handlers/CommandBus.ts @@ -0,0 +1,24 @@ +export interface Command { + readonly type: string; + readonly [key: string]: unknown; +} + +type CommandHandler = (cmd: T) => void; + +export class CommandBus { + private handlers = new Map(); + + register(type: string, handler: CommandHandler): void { + this.handlers.set(type, handler as CommandHandler); + } + + execute(cmd: Command): void { + const handler = this.handlers.get(cmd.type); + if (!handler) throw new Error(`No handler for command: ${cmd.type}`); + handler(cmd); + } + + has(type: string): boolean { + return this.handlers.has(type); + } +} diff --git a/src/application/handlers/QueryBus.ts b/src/application/handlers/QueryBus.ts new file mode 100644 index 0000000..1abe9f9 --- /dev/null +++ b/src/application/handlers/QueryBus.ts @@ -0,0 +1,24 @@ +export interface Query { + readonly type: string; + readonly [key: string]: unknown; +} + +type QueryHandler = (query: T) => R; + +export class QueryBus { + private handlers = new Map(); + + register(type: string, handler: QueryHandler): void { + this.handlers.set(type, handler as QueryHandler); + } + + execute(query: Query): R { + const handler = this.handlers.get(query.type); + if (!handler) throw new Error(`No handler for query: ${query.type}`); + return handler(query) as R; + } + + has(type: string): boolean { + return this.handlers.has(type); + } +} diff --git a/src/application/queries/GetBreedingOptions.ts b/src/application/queries/GetBreedingOptions.ts new file mode 100644 index 0000000..f795beb --- /dev/null +++ b/src/application/queries/GetBreedingOptions.ts @@ -0,0 +1,15 @@ +import { BreedingService, type PartnerInfo } from '@domain/services/BreedingService'; + +export interface GetBreedingOptionsQuery { type: 'get-breeding-options'; race: string; } + +export interface BreedingOptionsResult { + partners: readonly PartnerInfo[]; +} + +const breedingSvc = new BreedingService(); + +export function createGetBreedingOptionsHandler() { + return (query: GetBreedingOptionsQuery): BreedingOptionsResult => { + return { partners: breedingSvc.getCompatiblePartners(query.race) }; + }; +} diff --git a/src/application/queries/GetDashboard.ts b/src/application/queries/GetDashboard.ts new file mode 100644 index 0000000..882978d --- /dev/null +++ b/src/application/queries/GetDashboard.ts @@ -0,0 +1,57 @@ +import type { AppState } from '@domain/ports/StateRepository'; +import { elapsed } from '@domain/services/GaugeCalculator'; + +export interface DashboardQuery { type: 'get-dashboard'; } + +export interface EnclosSummary { + id: number; + name: string; + ddCount: number; + running: boolean; + elapsedSec: number; + activeGauges: string[]; +} + +export interface DashboardResult { + enclosSummaries: EnclosSummary[]; + totalCouples: number; + totalBabies: number; + raceBreakdown: Record; + successRate: number; +} + +export function createGetDashboardHandler(state: AppState) { + return (_query: DashboardQuery): DashboardResult => { + const summaries: EnclosSummary[] = state.enclos.map(enc => ({ + id: enc.id, + name: enc.name, + ddCount: enc.dragodindes.length, + running: enc.timer.running, + elapsedSec: elapsed(enc.timer), + activeGauges: [...enc.activeGauges], + })); + + let totalCouples = 0, totalBabies = 0; + const raceBreakdown: Record = {}; + + // From accouplements + for (const acc of state.accouplements) { + totalCouples += acc.couples; + totalBabies += acc.babiesObtained; + raceBreakdown[acc.baby] = (raceBreakdown[acc.baby] ?? 0) + acc.babiesObtained; + } + + // From archived stats (legacy migration) + for (const arch of state.archivedStats as Array<{ baby?: string; couples?: number; babiesObtained?: number }>) { + if (arch.baby) { + totalCouples += arch.couples ?? 0; + totalBabies += arch.babiesObtained ?? 0; + raceBreakdown[arch.baby] = (raceBreakdown[arch.baby] ?? 0) + (arch.babiesObtained ?? 0); + } + } + + const successRate = totalCouples > 0 ? Math.round((totalBabies / totalCouples) * 100) : 0; + + return { enclosSummaries: summaries, totalCouples, totalBabies, raceBreakdown, successRate }; + }; +} diff --git a/src/application/queries/GetEnclosDetail.ts b/src/application/queries/GetEnclosDetail.ts new file mode 100644 index 0000000..f0350cd --- /dev/null +++ b/src/application/queries/GetEnclosDetail.ts @@ -0,0 +1,10 @@ +import type { AppState } from '@domain/ports/StateRepository'; +import type { Enclos } from '@domain/entities/Enclos'; + +export interface GetEnclosDetailQuery { type: 'get-enclos-detail'; enclosId: number; } + +export function createGetEnclosDetailHandler(state: AppState) { + return (query: GetEnclosDetailQuery): Enclos | null => { + return state.enclos.find(e => e.id === query.enclosId) ?? null; + }; +} diff --git a/src/application/queries/GetInventaire.ts b/src/application/queries/GetInventaire.ts new file mode 100644 index 0000000..caf8cf2 --- /dev/null +++ b/src/application/queries/GetInventaire.ts @@ -0,0 +1,9 @@ +import type { AppState } from '@domain/ports/StateRepository'; + +export interface GetInventaireQuery { type: 'get-inventaire'; } + +export function createGetInventaireHandler(state: AppState) { + return (_query: GetInventaireQuery): Record => { + return state.inventaire; + }; +} diff --git a/src/application/queries/GetReapproTree.ts b/src/application/queries/GetReapproTree.ts new file mode 100644 index 0000000..41516bc --- /dev/null +++ b/src/application/queries/GetReapproTree.ts @@ -0,0 +1,22 @@ +import { ReapproCalculator, type ReapproResult } from '@domain/services/ReapproCalculator'; + +export interface GetReapproTreeQuery { + type: 'get-reappro-tree'; + target: string; + qty: number; + repro: Record; + inverted: Record; +} + +const calculator = new ReapproCalculator(); + +export function createGetReapproTreeHandler() { + return (query: GetReapproTreeQuery): ReapproResult => { + return calculator.compute({ + target: query.target, + qty: query.qty, + repro: query.repro, + inverted: query.inverted, + }); + }; +} diff --git a/src/application/queries/GetSettings.ts b/src/application/queries/GetSettings.ts new file mode 100644 index 0000000..3798ff0 --- /dev/null +++ b/src/application/queries/GetSettings.ts @@ -0,0 +1,17 @@ +import type { AppState } from '@domain/ports/StateRepository'; + +export interface GetSettingsQuery { type: 'get-settings'; } + +export interface SettingsResult { + alarmSound: string; + notifsEnabled: boolean; + ntfyTopic: string; +} + +export function createGetSettingsHandler(state: AppState) { + return (_query: GetSettingsQuery): SettingsResult => ({ + alarmSound: state.alarmSound, + notifsEnabled: state.notifsEnabled, + ntfyTopic: state.ntfyTopic, + }); +} diff --git a/src/application/queries/GetStatistics.ts b/src/application/queries/GetStatistics.ts new file mode 100644 index 0000000..357742b --- /dev/null +++ b/src/application/queries/GetStatistics.ts @@ -0,0 +1,288 @@ +import type { AppState } from '@domain/ports/StateRepository'; +import { RACE_GEN } from '@domain/value-objects/Race'; + +// 66 races totales moins 3 Gen 1 (Rousse, Dorée, Amande) qui se capturent et ne se créent pas +export const TOTAL_RACES = 63; + +export interface GetStatisticsQuery { + type: 'get-statistics'; + days?: number; // 0 = tout l'historique +} + +export interface DailyBirths { + date: string; + label: string; + count: number; +} + +export interface RaceShare { + race: string; + count: number; + pct: number; +} + +export interface KpiDelta { + value: number; + delta: number | null; +} + +export interface RaceSuccessRate { + race: string; + couples: number; + babies: number; + rate: number; +} + +export interface BestCouple { + parentA: string; + parentB: string; + baby: string; + couples: number; + babies: number; + rate: number; +} + +export interface GenBreakdown { + gen: number; + babies: number; + couples: number; + races: number; +} + +export interface MissingRace { + name: string; + gen: number; +} + +export interface WeekdayActivity { + day: string; + count: number; +} + +export interface StatisticsResult { + totalBabies: KpiDelta; + totalCouples: KpiDelta; + successRate: KpiDelta; + racesCount: KpiDelta; + dailyBirths: DailyBirths[]; + raceShares: RaceShare[]; + raceSuccessRates: RaceSuccessRate[]; + bestCouples: BestCouple[]; + genBreakdown: GenBreakdown[]; + missingRaces: MissingRace[]; + weekdayActivity: WeekdayActivity[]; + days: number; +} + +interface AccEntry { + parentA: string; + parentB: string; + baby: string; + gen: number; + couples: number; + babiesObtained: number; + date: string; +} + +function toISO(d: Date): string { + return d.toISOString().slice(0, 10); +} + +function aggregate(entries: AccEntry[]) { + let couples = 0, babies = 0; + const races = new Set(); + for (const e of entries) { + couples += e.couples; + babies += e.babiesObtained; + if (e.babiesObtained > 0) races.add(e.baby); + } + const rate = couples > 0 ? Math.round((babies / couples) * 100) : 0; + return { couples, babies, rate, racesCount: races.size }; +} + +const WEEKDAY_NAMES = ['Dimanche', 'Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi']; + +export function createGetStatisticsHandler(state: AppState) { + return (query: GetStatisticsQuery): StatisticsResult => { + const days = query.days ?? 30; + + // Collecter toutes les entrées normalisées (exclure Gen 1 : non créables, seulement capturables) + const all: AccEntry[] = []; + for (const acc of state.accouplements) { + if (acc.gen === 1 || (RACE_GEN[acc.baby] ?? 0) === 1) continue; + all.push({ + parentA: acc.parentA, parentB: acc.parentB, + baby: acc.baby, gen: acc.gen, + couples: acc.couples, babiesObtained: acc.babiesObtained, + date: acc.date, + }); + } + for (const arch of state.archivedStats as Array<{ + parentA?: string; parentB?: string; baby?: string; gen?: number; + couples?: number; babiesObtained?: number; date?: string; + }>) { + if (arch.baby) { + const gen = arch.gen ?? (RACE_GEN[arch.baby] ?? 0); + if (gen === 1) continue; + all.push({ + parentA: arch.parentA ?? '', parentB: arch.parentB ?? '', + baby: arch.baby, gen, + couples: arch.couples ?? 0, + babiesObtained: arch.babiesObtained ?? 0, + date: arch.date ?? '', + }); + } + } + + const now = new Date(); + const todayISO = toISO(now); + + let current: AccEntry[]; + let previous: AccEntry[] | null; + + if (days === 0) { + current = all; + previous = null; + } else { + const startCurrent = new Date(now); + startCurrent.setDate(startCurrent.getDate() - days); + const startCurrentISO = toISO(startCurrent); + + const startPrevious = new Date(startCurrent); + startPrevious.setDate(startPrevious.getDate() - days); + const startPreviousISO = toISO(startPrevious); + + current = all.filter(e => { + const d = e.date.slice(0, 10); + return d >= startCurrentISO && d <= todayISO; + }); + previous = all.filter(e => { + const d = e.date.slice(0, 10); + return d >= startPreviousISO && d < startCurrentISO; + }); + } + + const cur = aggregate(current); + const prev = previous ? aggregate(previous) : null; + + function delta(curVal: number, prevVal: number | null): KpiDelta { + if (prevVal === null) return { value: curVal, delta: null }; + return { value: curVal, delta: curVal - prevVal }; + } + + // ── Naissances par jour ────────────────────────────────────── + const dailyMap: Record = {}; + for (const e of current) { + const day = e.date.slice(0, 10); + if (day) dailyMap[day] = (dailyMap[day] ?? 0) + e.babiesObtained; + } + const chartDays = days === 0 ? 30 : days; + const dailyBirths: DailyBirths[] = []; + for (let i = chartDays - 1; i >= 0; i--) { + const d = new Date(now); + d.setDate(d.getDate() - i); + const iso = toISO(d); + const label = `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}`; + dailyBirths.push({ date: iso, label, count: dailyMap[iso] ?? 0 }); + } + + // ── Répartition des races ──────────────────────────────────── + const raceBreakdown: Record = {}; + let totalBabiesForShares = 0; + for (const e of current) { + raceBreakdown[e.baby] = (raceBreakdown[e.baby] ?? 0) + e.babiesObtained; + totalBabiesForShares += e.babiesObtained; + } + const raceShares: RaceShare[] = Object.entries(raceBreakdown) + .sort((a, b) => b[1] - a[1]) + .map(([race, count]) => ({ + race, count, + pct: totalBabiesForShares > 0 ? Math.round((count / totalBabiesForShares) * 100) : 0, + })); + + // ── Taux de réussite par race ──────────────────────────────── + const raceAgg: Record = {}; + for (const e of current) { + if (!raceAgg[e.baby]) raceAgg[e.baby] = { couples: 0, babies: 0 }; + raceAgg[e.baby].couples += e.couples; + raceAgg[e.baby].babies += e.babiesObtained; + } + const raceSuccessRates: RaceSuccessRate[] = Object.entries(raceAgg) + .map(([race, { couples, babies }]) => ({ + race, couples, babies, + rate: couples > 0 ? Math.round((babies / couples) * 100) : 0, + })) + .sort((a, b) => b.rate - a.rate); + + // ── Meilleurs couples ──────────────────────────────────────── + const coupleAgg: Record = {}; + for (const e of current) { + if (!e.parentA || !e.parentB) continue; + // Clé normalisée (ordre alphabétique) pour éviter les doublons A+B / B+A + const key = [e.parentA, e.parentB].sort().join('|'); + if (!coupleAgg[key]) { + coupleAgg[key] = { parentA: e.parentA, parentB: e.parentB, baby: e.baby, couples: 0, babies: 0 }; + } + coupleAgg[key].couples += e.couples; + coupleAgg[key].babies += e.babiesObtained; + } + const bestCouples: BestCouple[] = Object.values(coupleAgg) + .map(c => ({ ...c, rate: c.couples > 0 ? Math.round((c.babies / c.couples) * 100) : 0 })) + .sort((a, b) => b.rate - a.rate || b.babies - a.babies) + .slice(0, 10); + + // ── Répartition par génération ─────────────────────────────── + const genAgg: Record }> = {}; + for (const e of current) { + const g = e.gen || (RACE_GEN[e.baby] ?? 0); + if (!g) continue; + if (!genAgg[g]) genAgg[g] = { babies: 0, couples: 0, races: new Set() }; + genAgg[g].babies += e.babiesObtained; + genAgg[g].couples += e.couples; + if (e.babiesObtained > 0) genAgg[g].races.add(e.baby); + } + const genBreakdown: GenBreakdown[] = Object.entries(genAgg) + .map(([g, v]) => ({ gen: Number(g), babies: v.babies, couples: v.couples, races: v.races.size })) + .sort((a, b) => a.gen - b.gen); + + // ── Races manquantes ───────────────────────────────────────── + const obtainedAll = new Set(); + for (const e of all) { + if (e.babiesObtained > 0) obtainedAll.add(e.baby); + } + const missingRaces: MissingRace[] = Object.entries(RACE_GEN) + .filter(([name, gen]) => gen !== 1 && !obtainedAll.has(name)) + .map(([name, gen]) => ({ name, gen })) + .sort((a, b) => a.gen - b.gen || a.name.localeCompare(b.name)); + + // ── Activité par jour de la semaine ────────────────────────── + const weekdayCounts = [0, 0, 0, 0, 0, 0, 0]; + for (const e of current) { + const d = e.date.slice(0, 10); + if (!d) continue; + const dt = new Date(d + 'T12:00:00'); + weekdayCounts[dt.getDay()] += e.babiesObtained; + } + // Réordonner : Lundi → Dimanche + const weekdayActivity: WeekdayActivity[] = []; + for (let i = 1; i <= 7; i++) { + const idx = i % 7; + weekdayActivity.push({ day: WEEKDAY_NAMES[idx], count: weekdayCounts[idx] }); + } + + return { + totalBabies: delta(cur.babies, prev?.babies ?? null), + totalCouples: delta(cur.couples, prev?.couples ?? null), + successRate: delta(cur.rate, prev?.rate ?? null), + racesCount: delta(cur.racesCount, prev?.racesCount ?? null), + dailyBirths, + raceShares, + raceSuccessRates, + bestCouples, + genBreakdown, + missingRaces, + weekdayActivity, + days, + }; + }; +} diff --git a/src/application/queries/GetTimerState.ts b/src/application/queries/GetTimerState.ts new file mode 100644 index 0000000..c7eb385 --- /dev/null +++ b/src/application/queries/GetTimerState.ts @@ -0,0 +1,22 @@ +import type { AppState } from '@domain/ports/StateRepository'; +import { elapsed } from '@domain/services/GaugeCalculator'; + +export interface GetTimerStateQuery { type: 'get-timer-state'; enclosId: number; } + +export interface TimerStateResult { + running: boolean; + elapsedSec: number; + startTime: number | null; +} + +export function createGetTimerStateHandler(state: AppState) { + return (query: GetTimerStateQuery): TimerStateResult | null => { + const enc = state.enclos.find(e => e.id === query.enclosId); + if (!enc) return null; + return { + running: enc.timer.running, + elapsedSec: elapsed(enc.timer), + startTime: enc.timer.startTime, + }; + }; +} diff --git a/src/application/queries/GetWorkflows.ts b/src/application/queries/GetWorkflows.ts new file mode 100644 index 0000000..005d0a3 --- /dev/null +++ b/src/application/queries/GetWorkflows.ts @@ -0,0 +1,20 @@ +import type { AppState } from '@domain/ports/StateRepository'; + +export interface GetWorkflowsQuery { type: 'get-workflows'; } + +export interface WorkflowItem { + id: number; + name: string; + target: string; + qty: number; + createdAt: number; + materials: Array<{ name: string; needed: number; done: number }>; + steps: Array<{ + gen: number; + crossings: Array<{ race: string; needed: number; parentA: string; parentB: string; couples: number; repro: number; done: number }>; + }>; +} + +export function createGetWorkflowsHandler(state: AppState) { + return (_query: GetWorkflowsQuery): WorkflowItem[] => state.workflows as WorkflowItem[]; +} diff --git a/src/domain/entities/Accouplement.ts b/src/domain/entities/Accouplement.ts new file mode 100644 index 0000000..1303d47 --- /dev/null +++ b/src/domain/entities/Accouplement.ts @@ -0,0 +1,16 @@ +export interface Accouplement { + readonly parentA: string; + readonly parentB: string; + readonly baby: string; + readonly gen: number; + readonly couples: number; + readonly babiesObtained: number; + readonly date: string; +} + +export function createAccouplement( + parentA: string, parentB: string, baby: string, gen: number, + couples: number, babiesObtained: number +): Accouplement { + return { parentA, parentB, baby, gen, couples, babiesObtained, date: new Date().toISOString() }; +} diff --git a/src/domain/entities/Dragodinde.ts b/src/domain/entities/Dragodinde.ts new file mode 100644 index 0000000..e636f08 --- /dev/null +++ b/src/domain/entities/Dragodinde.ts @@ -0,0 +1,37 @@ +import type { GaugeType } from '@domain/value-objects/GaugeType'; +import { DEFAULT_TARGETS } from '@domain/value-objects/GaugeType'; +import type { Gender } from '@domain/value-objects/Gender'; + +export interface DragodindeStats { + serenite: number; + endurance: number; + maturite: number; + amour: number; + xp: number; +} + +export interface Dragodinde { + readonly id: number; + name: string; + race: string; + gender: Gender; + stats: DragodindeStats; + targets: Record; + sereniteTarget: number | null; + levelTarget: number | null; + reproducteur: number; +} + +export function createDragodinde(id: number): Dragodinde { + return { + id, + name: `Dragodinde ${id}`, + race: '', + gender: 'n', + stats: { serenite: 0, endurance: 0, maturite: 0, amour: 0, xp: 1 }, + targets: { ...DEFAULT_TARGETS }, + sereniteTarget: null, + levelTarget: null, + reproducteur: 0, + }; +} diff --git a/src/domain/entities/Enclos.ts b/src/domain/entities/Enclos.ts new file mode 100644 index 0000000..0eba062 --- /dev/null +++ b/src/domain/entities/Enclos.ts @@ -0,0 +1,59 @@ +import type { Dragodinde } from './Dragodinde'; +import { createDragodinde } from './Dragodinde'; +import type { GaugeType } from '@domain/value-objects/GaugeType'; + +export const MAX_DD = 10; +export const MAX_ENCLOS = 6; +export const MAX_GAUGES = 2; + +export interface GaugeRecharge { + atSec: number; // secondes écoulées au moment de la recharge + level: number; // nouveau niveau de jauge +} + +export interface TimerData { + running: boolean; + startTime: number | null; + pausedAt: number | null; + pausedMs: number; + snapGauges: Record; + snapStats: Record>; + gaugeRecharges: Record; +} + +export interface Enclos { + readonly id: number; + name: string; + activeGauges: GaugeType[]; + gaugeLevels: Record; + dragodindes: Dragodinde[]; + nextDdId: number; + timer: TimerData; + alerted: Record; +} + +export function createEnclos(id: number, name?: string): Enclos { + return { + id, + name: name ?? `Enclos ${id}`, + activeGauges: [], + gaugeLevels: { baffeur: 0, caresseur: 0, foudroyeur: 0, abreuvoir: 0, dragofesse: 0, mangeoire: 0 }, + dragodindes: [], + nextDdId: 1, + timer: { running: false, startTime: null, pausedAt: null, pausedMs: 0, snapGauges: {}, snapStats: {}, gaugeRecharges: {} }, + alerted: {}, + }; +} + +export function addDragodinde(enc: Enclos): Enclos { + if (enc.dragodindes.length >= MAX_DD) return enc; + const usedIds = new Set(enc.dragodindes.map(d => d.id)); + let newId = 1; + while (usedIds.has(newId)) newId++; + const dd = createDragodinde(newId); + return { ...enc, dragodindes: [...enc.dragodindes, dd], nextDdId: newId + 1 }; +} + +export function removeDragodinde(enc: Enclos, ddId: number): Enclos { + return { ...enc, dragodindes: enc.dragodindes.filter(d => d.id !== ddId) }; +} diff --git a/src/domain/events/DomainEvent.ts b/src/domain/events/DomainEvent.ts new file mode 100644 index 0000000..44858c0 --- /dev/null +++ b/src/domain/events/DomainEvent.ts @@ -0,0 +1,6 @@ +export type DomainEventType = 'timer-completed' | 'gauge-threshold-reached' | 'accouplement-registered' | 'enclos-deleted'; + +export interface DomainEvent { + readonly type: DomainEventType; + readonly [key: string]: unknown; +} diff --git a/src/domain/events/EventBus.ts b/src/domain/events/EventBus.ts new file mode 100644 index 0000000..1b9cd86 --- /dev/null +++ b/src/domain/events/EventBus.ts @@ -0,0 +1,24 @@ +import type { DomainEvent, DomainEventType } from './DomainEvent'; + +type Handler = (event: DomainEvent) => void; + +export class EventBus { + private handlers = new Map(); + + on(type: DomainEventType, handler: Handler): void { + if (!this.handlers.has(type)) this.handlers.set(type, []); + this.handlers.get(type)!.push(handler); + } + + off(type: DomainEventType, handler: Handler): void { + const list = this.handlers.get(type); + if (!list) return; + const idx = list.indexOf(handler); + if (idx >= 0) list.splice(idx, 1); + } + + emit(event: DomainEvent): void { + const handlers = this.handlers.get(event.type) ?? []; + for (const h of handlers) h(event); + } +} diff --git a/src/domain/ports/AlarmPort.ts b/src/domain/ports/AlarmPort.ts new file mode 100644 index 0000000..487aefc --- /dev/null +++ b/src/domain/ports/AlarmPort.ts @@ -0,0 +1,4 @@ +export interface AlarmPort { + play(soundName: string): void; + stop(): void; +} diff --git a/src/domain/ports/NotificationPort.ts b/src/domain/ports/NotificationPort.ts new file mode 100644 index 0000000..4f01c17 --- /dev/null +++ b/src/domain/ports/NotificationPort.ts @@ -0,0 +1,4 @@ +export interface NotificationPort { + showNotification(title: string, body: string): void; + sendMobileNotification(url: string, title: string, message: string): void; +} diff --git a/src/domain/ports/StateRepository.ts b/src/domain/ports/StateRepository.ts new file mode 100644 index 0000000..33bf050 --- /dev/null +++ b/src/domain/ports/StateRepository.ts @@ -0,0 +1,20 @@ +import type { Enclos } from '@domain/entities/Enclos'; +import type { Accouplement } from '@domain/entities/Accouplement'; + +export interface AppState { + enclos: Enclos[]; + activeId: number | string | null; + nextEnclosId: number; + alarmSound: string; + notifsEnabled: boolean; + ntfyTopic: string; + archivedStats: unknown[]; + inventaire: Record; + workflows: unknown[]; + accouplements: Accouplement[]; +} + +export interface StateRepository { + load(): Promise; + save(state: AppState): void; +} diff --git a/src/domain/ports/UpdatePort.ts b/src/domain/ports/UpdatePort.ts new file mode 100644 index 0000000..c32a681 --- /dev/null +++ b/src/domain/ports/UpdatePort.ts @@ -0,0 +1,11 @@ +export interface UpdateInfo { + version: string; + downloadUrl: string; + assetName: string; + releaseNotes: string; +} + +export interface UpdatePort { + checkForUpdates(): Promise; + downloadAndInstall(info: UpdateInfo): void; +} diff --git a/src/domain/services/BreedingService.ts b/src/domain/services/BreedingService.ts new file mode 100644 index 0000000..e20a28c --- /dev/null +++ b/src/domain/services/BreedingService.ts @@ -0,0 +1,28 @@ +import { BREEDING_RECIPES, BREEDING_BY_PARENTS, COMPATIBLE_PARTNERS, RACE_GEN } from '@domain/value-objects/Race'; + +export interface PartnerInfo { + readonly partner: string; + readonly baby: string; + readonly gen: number; +} + +export class BreedingService { + deduceBaby(parent1: string, parent2: string): string | null { + return BREEDING_BY_PARENTS[`${parent1}|${parent2}`] + ?? BREEDING_BY_PARENTS[`${parent2}|${parent1}`] + ?? null; + } + + getCompatiblePartners(race: string): readonly PartnerInfo[] { + return (COMPATIBLE_PARTNERS[race] as PartnerInfo[] | undefined) ?? []; + } + + getParents(babyRace: string): readonly [string, string] | null { + const recipe = BREEDING_RECIPES[babyRace]; + return recipe ? [recipe[0], recipe[1]] : null; + } + + getGeneration(race: string): number { + return RACE_GEN[race] ?? 0; + } +} diff --git a/src/domain/services/GaugeCalculator.ts b/src/domain/services/GaugeCalculator.ts new file mode 100644 index 0000000..37a435e --- /dev/null +++ b/src/domain/services/GaugeCalculator.ts @@ -0,0 +1,151 @@ +export interface TimerState { + readonly startTime: number | null; + readonly running: boolean; + readonly pausedAt: number | null; + readonly pausedMs: number; +} + +/** + * Points gained when a gauge of given level runs for `sec` seconds. + * Gauge level decreases through tiers: 90k→40, 70k→30, 40k→20, 0→10 pts per 10sec tick. + */ +export function gainedIn(lvl: number, sec: number): number { + const T = [ + { lo: 90000, r: 40 }, + { lo: 70000, r: 30 }, + { lo: 40000, r: 20 }, + { lo: 0, r: 10 }, + ]; + let g = Math.min(Math.max(lvl, 0), 100000); + let tl = Math.floor(sec / 10); + let out = 0; + for (const { lo, r } of T) { + if (g <= lo || tl <= 0) continue; + const a = g - lo; + const m = Math.floor(a / r); + const u = Math.min(m, tl); + out += u * r; + tl -= u; + g = lo; + } + return out; +} + +/** + * Seconds needed to gain `pts` points starting from gauge level `lvl`. + * Returns Infinity if impossible (level too low). + */ +export function timeToGain(lvl: number, pts: number): number { + if (pts <= 0) return 0; + const T = [ + { lo: 90000, r: 40 }, + { lo: 70000, r: 30 }, + { lo: 40000, r: 20 }, + { lo: 0, r: 10 }, + ]; + let s = 0, rem = pts; + let g = Math.min(Math.max(lvl, 0), 100000); + for (const { lo, r } of T) { + if (g <= lo || rem <= 0) continue; + const a = g - lo; + const d = Math.min(rem, a); + s += Math.ceil(d / r) * 10; + rem -= d; + g = lo; + } + return rem > 0 ? Infinity : s; +} + +/** + * Gauge level after `sec` seconds of depletion. + */ +export function gaugeAfter(lvl: number, sec: number): number { + const T = [ + { lo: 90000, r: 40 }, + { lo: 70000, r: 30 }, + { lo: 40000, r: 20 }, + { lo: 0, r: 10 }, + ]; + let g = Math.min(Math.max(lvl, 0), 100000); + let tl = Math.floor(sec / 10); + for (const { lo, r } of T) { + if (g <= lo || tl <= 0) continue; + const a = g - lo; + const m = Math.floor(a / r); + const u = Math.min(m, tl); + g -= u * r; + tl -= u; + } + return g; +} + +export interface GaugeRecharge { + readonly atSec: number; + readonly level: number; +} + +export interface GaugeState { + gained: number; // points accumulés depuis le snapshot + curGl: number; // niveau de jauge actuel (après dépletion et recharges) + effectiveEl: number; // elapsed effectif (gelé au cap si atteint) +} + +/** + * Calcule les points accumulés et le niveau de jauge actuel en tenant compte + * des recharges intermédiaires et du cap absolu (ptsAllowed). + * + * Algorithme segment par segment : + * Pour chaque segment [prevEl → recharge.atSec] : + * - gainedIn(prevGl, segDur) points gagnés + * - Si le cap est atteint dans ce segment → freeze ici + * - Sinon, continuer avec le nouveau niveau après recharge + */ +export function computeGaugeState( + startGl: number, + recharges: readonly GaugeRecharge[], + ptsAllowed: number, + el: number, +): GaugeState { + let gained = 0; + let prevEl = 0; + let prevGl = startGl; + + const sorted = recharges.filter(r => r.atSec < el).sort((a, b) => a.atSec - b.atSec); + + for (const r of sorted) { + const segDur = r.atSec - prevEl; + const segGained = gainedIn(prevGl, segDur); + + if (isFinite(ptsAllowed) && gained + segGained >= ptsAllowed) { + const ptsNeeded = ptsAllowed - gained; + const secInSeg = timeToGain(prevGl, ptsNeeded); + return { gained: ptsAllowed, curGl: Math.max(0, gaugeAfter(prevGl, secInSeg)), effectiveEl: prevEl + secInSeg }; + } + + gained += segGained; + prevEl = r.atSec; + prevGl = r.level; + } + + // Dernier segment (de la dernière recharge jusqu'à el) + const lastDur = el - prevEl; + const lastGained = gainedIn(prevGl, lastDur); + + if (isFinite(ptsAllowed) && gained + lastGained >= ptsAllowed) { + const ptsNeeded = ptsAllowed - gained; + const secInSeg = timeToGain(prevGl, ptsNeeded); + return { gained: ptsAllowed, curGl: Math.max(0, gaugeAfter(prevGl, secInSeg)), effectiveEl: prevEl + secInSeg }; + } + + return { gained: gained + lastGained, curGl: Math.max(0, gaugeAfter(prevGl, lastDur)), effectiveEl: el }; +} + +/** + * Elapsed seconds for a timer state. + */ +export function elapsed(timer: TimerState): number { + if (!timer.startTime) return 0; + if (timer.running) return (Date.now() - timer.startTime - timer.pausedMs) / 1000; + if (timer.pausedAt) return (timer.pausedAt - timer.startTime - timer.pausedMs) / 1000; + return 0; +} diff --git a/src/domain/services/InventaireCalculator.ts b/src/domain/services/InventaireCalculator.ts new file mode 100644 index 0000000..05e2a64 --- /dev/null +++ b/src/domain/services/InventaireCalculator.ts @@ -0,0 +1,69 @@ +import { BREEDING_RECIPES, RACE_GEN } from '@domain/value-objects/Race'; + +export interface Stock { m: number; f: number; n: number; } +export interface CrossingDetail { aSex: string; bSex: string; } +export interface CrossingResult { name: string; qty: number; parents: readonly [string, string]; gen: number; details: CrossingDetail[]; } +export interface GenerationResult { gen: number; crossings: CrossingResult[]; } +export interface InventaireResult { generations: GenerationResult[]; remaining: Record; } + +export class InventaireCalculator { + compute(inventaire: Readonly>): InventaireResult { + const avail: Record = {}; + for (const [k, v] of Object.entries(inventaire)) { + if ((v.m + v.f) > 0) avail[k] = { m: v.m, f: v.f, n: 0 }; + } + + if (Object.keys(avail).length === 0) return { generations: [], remaining: {} }; + + const hasMale = (s: Stock) => s.m > 0 || s.n > 0; + const hasFemale = (s: Stock) => s.f > 0 || s.n > 0; + const takeMale = (s: Stock) => { if (s.m > 0) s.m--; else s.n--; }; + const takeFemale = (s: Stock) => { if (s.f > 0) s.f--; else s.n--; }; + const totalOf = (s: Stock) => s.m + s.f + s.n; + + const generations: GenerationResult[] = []; + + for (let gen = 2; gen <= 10; gen++) { + const crossingsAtGen = Object.entries(BREEDING_RECIPES) + .filter(([name]) => (RACE_GEN[name] ?? 0) === gen) + .map(([name, parents]) => ({ name, parents })); + + const genResults: CrossingResult[] = []; + let more = true; + while (more) { + more = false; + for (const cr of crossingsAtGen) { + const [a, b] = cr.parents; + const sa = avail[a] ?? { m: 0, f: 0, n: 0 }; + const sb = avail[b] ?? { m: 0, f: 0, n: 0 }; + let ok = false, aSex = '', bSex = ''; + + if (a === b) { + if (totalOf(sa) >= 2 && hasMale(sa) && hasFemale(sa)) { + takeMale(sa); takeFemale(sa); aSex = '\u2642'; bSex = '\u2640'; ok = true; + } + } else if (hasMale(sa) && hasFemale(sb)) { + takeMale(sa); takeFemale(sb); aSex = '\u2642'; bSex = '\u2640'; ok = true; + } else if (hasFemale(sa) && hasMale(sb)) { + takeFemale(sa); takeMale(sb); aSex = '\u2640'; bSex = '\u2642'; ok = true; + } + + if (ok) { + if (!avail[a]) avail[a] = sa; + if (!avail[b]) avail[b] = sb; + if (!avail[cr.name]) avail[cr.name] = { m: 0, f: 0, n: 0 }; + avail[cr.name]!.n++; + let entry = genResults.find(r => r.name === cr.name); + if (!entry) { entry = { name: cr.name, qty: 0, parents: cr.parents as [string, string], gen, details: [] }; genResults.push(entry); } + entry.qty++; + entry.details.push({ aSex, bSex }); + more = true; + } + } + } + if (genResults.length > 0) generations.push({ gen, crossings: genResults }); + } + + return { generations, remaining: avail }; + } +} diff --git a/src/domain/services/ReapproCalculator.ts b/src/domain/services/ReapproCalculator.ts new file mode 100644 index 0000000..aee327e --- /dev/null +++ b/src/domain/services/ReapproCalculator.ts @@ -0,0 +1,80 @@ +import { BREEDING_RECIPES, RACE_GEN } from '@domain/value-objects/Race'; + +export interface ReapproInput { + target: string; + qty: number; + repro: Readonly>; + inverted: Readonly>; +} + +export interface ReapproStep { + race: string; + gen: number; + qty: number; + couples: number; + parentA: string; + parentB: string; + repro: number; +} + +export interface Gen1Need { + name: string; + total: number; + m: number; + f: number; +} + +export interface ReapproResult { + steps: readonly ReapproStep[]; + gen1Needs: readonly Gen1Need[]; + totalGen1: number; +} + +export class ReapproCalculator { + compute(input: ReapproInput): ReapproResult { + const { target, qty, repro, inverted } = input; + if (!target || !BREEDING_RECIPES[target]) return { steps: [], gen1Needs: [], totalGen1: 0 }; + + const needs: Record = {}; + needs[target] = { total: qty, m: 0, f: 0 }; + const steps: ReapproStep[] = []; + const processed = new Set(); + const targetGen = RACE_GEN[target] ?? 2; + + for (let gen = targetGen; gen >= 2; gen--) { + const racesAtGen = Object.keys(needs).filter( + r => !processed.has(r) && (RACE_GEN[r] ?? 0) === gen && !!BREEDING_RECIPES[r] + ); + for (const race of racesAtGen) { + const Q = needs[race]!.total; + if (Q <= 0) { processed.add(race); continue; } + const R = repro[race] ?? 0; + const couplesReal = (2 * R >= Q) ? Math.ceil(Q / 2) : (R > 0 ? Q - R : Q); + + const isInv = inverted[race] ?? false; + const [rawA, rawB] = BREEDING_RECIPES[race]!; + const a = isInv ? rawB : rawA; + const b = isInv ? rawA : rawB; + + if (!needs[a]) needs[a] = { total: 0, m: 0, f: 0 }; + if (!needs[b]) needs[b] = { total: 0, m: 0, f: 0 }; + needs[a]!.total += couplesReal; + needs[a]!.m += couplesReal; + needs[b]!.total += couplesReal; + needs[b]!.f += couplesReal; + + steps.push({ race, gen, qty: Q, couples: couplesReal, parentA: a, parentB: b, repro: R }); + processed.add(race); + } + } + + const gen1Needs: Gen1Need[] = Object.entries(needs) + .filter(([n]) => !BREEDING_RECIPES[n] && needs[n]!.total > 0) + .sort((a, b) => b[1].total - a[1].total) + .map(([name, d]) => ({ name, total: d.total, m: d.m, f: d.f })); + + const totalGen1 = gen1Needs.reduce((s, n) => s + n.total, 0); + + return { steps, gen1Needs, totalGen1 }; + } +} diff --git a/src/domain/services/SerenityCalculator.ts b/src/domain/services/SerenityCalculator.ts new file mode 100644 index 0000000..2fe6b54 --- /dev/null +++ b/src/domain/services/SerenityCalculator.ts @@ -0,0 +1,34 @@ +import { timeToGain } from './GaugeCalculator'; +import type { GaugeType } from '@domain/value-objects/GaugeType'; + +export interface SerenityEtaInput { + currentSerenite: number; + target: number | null; + activeGauges: readonly string[]; + gaugeLevels: Readonly>; +} + +export interface EtaResult { + done: boolean; + seconds: number; + needsGauge?: GaugeType; +} + +export class SerenityCalculator { + computeEta(input: SerenityEtaInput): EtaResult { + const { currentSerenite, target, activeGauges, gaugeLevels } = input; + if (target === null || target === undefined) return { done: false, seconds: 0 }; + const diff = target - currentSerenite; + if (diff === 0) return { done: true, seconds: 0 }; + + const needUp = diff > 0; + const gid: GaugeType = needUp ? 'caresseur' : 'baffeur'; + + if (!activeGauges.includes(gid)) return { done: false, seconds: Infinity, needsGauge: gid }; + + const gl = gaugeLevels[gid] ?? 0; + const pts = Math.abs(diff); + const sec = timeToGain(gl, pts); + return { done: false, seconds: sec }; + } +} diff --git a/src/domain/services/StockSimulator.ts b/src/domain/services/StockSimulator.ts new file mode 100644 index 0000000..1ffa496 --- /dev/null +++ b/src/domain/services/StockSimulator.ts @@ -0,0 +1,108 @@ +import { RACES_DATA, BREEDING_RECIPES } from '@domain/value-objects/Race'; + +export interface SimulationCrossing { + parentA: string; + parentB: string; + baby: string; + gen: number; + count: number; + pAMale: number; + pAFemale: number; + pBMale: number; + pBFemale: number; +} + +export interface SimulationResult { + crossings: SimulationCrossing[]; + unusedStock: { race: string; m: number; f: number }[]; +} + +/** + * Simule tous les croisements possibles depuis un inventaire en stock. + * + * Algorithme d'allocation proportionnelle dynamique : + * Pour chaque croisement à une génération donnée, le stock de chaque parent + * est divisé équitablement par le nombre de croisements restants qui utilisent + * encore ce parent. Cela évite qu'un premier croisement épuise tout le stock + * et prive les suivants (ex. : 3 races Gen2 partagent les mêmes parents de base). + * + * Les deux configurations sont utilisées simultanément : + * c1 = min(♂A_alloué, ♀B_alloué) → ♂A × ♀B + * c2 = min(♀A_allouée, ♂B_alloué) → ♀A × ♂B + * bred = c1 + c2 + */ +export function simulateStock( + inventaire: Readonly>, +): SimulationResult { + const stock: Record = {}; + for (const [race, entry] of Object.entries(inventaire)) { + if (entry.m > 0 || entry.f > 0) stock[race] = { m: entry.m, f: entry.f }; + } + + const crossings: SimulationCrossing[] = []; + + for (let g = 2; g <= 10; g++) { + const racesAtGen = RACES_DATA[g]; + if (!racesAtGen) continue; + + // Crossings possibles à cette génération (les deux parents ont du stock) + const genCrossings: { baby: string; parentA: string; parentB: string }[] = []; + for (const raceData of racesAtGen) { + const recipe = BREEDING_RECIPES[raceData.name]; + if (!recipe) continue; + const [parentA, parentB] = recipe; + const sA = stock[parentA]; + const sB = stock[parentB]; + if (!sA || !sB || sA.m + sA.f <= 0 || sB.m + sB.f <= 0) continue; + genCrossings.push({ baby: raceData.name, parentA, parentB }); + } + + for (let i = 0; i < genCrossings.length; i++) { + const { baby, parentA, parentB } = genCrossings[i]!; + const sA = stock[parentA]; + const sB = stock[parentB]; + if (!sA || !sB || sA.m + sA.f <= 0 || sB.m + sB.f <= 0) continue; + + // Croisements restants dont les deux parents ont encore du stock + const remaining = genCrossings.slice(i).filter(c => { + const a = stock[c.parentA]; + const b = stock[c.parentB]; + return a && b && a.m + a.f > 0 && b.m + b.f > 0; + }); + + const countA = remaining.filter(c => c.parentA === parentA || c.parentB === parentA).length; + const countB = remaining.filter(c => c.parentA === parentB || c.parentB === parentB).length; + + const allocAm = Math.floor(sA.m / countA); + const allocAf = Math.floor(sA.f / countA); + const allocBm = Math.floor(sB.m / countB); + const allocBf = Math.floor(sB.f / countB); + + const c1 = Math.min(allocAm, allocBf); // ♂A × ♀B + const c2 = Math.min(allocAf, allocBm); // ♀A × ♂B + const bred = c1 + c2; + + if (bred > 0) { + sA.m -= c1; sB.f -= c1; + sA.f -= c2; sB.m -= c2; + + crossings.push({ + parentA, parentB, baby, gen: g, count: bred, + pAMale: c1, pAFemale: c2, + pBMale: c2, pBFemale: c1, + }); + + if (!stock[baby]) stock[baby] = { m: 0, f: 0 }; + const halfM = Math.ceil(bred / 2); + stock[baby].m += halfM; + stock[baby].f += bred - halfM; + } + } + } + + const unusedStock = Object.entries(stock) + .filter(([, s]) => s.m > 0 || s.f > 0) + .map(([race, s]) => ({ race, m: s.m, f: s.f })); + + return { crossings, unusedStock }; +} diff --git a/src/domain/services/XpCalculator.ts b/src/domain/services/XpCalculator.ts new file mode 100644 index 0000000..14b710a --- /dev/null +++ b/src/domain/services/XpCalculator.ts @@ -0,0 +1,32 @@ +import { timeToGain } from '@domain/services/GaugeCalculator'; +import { xpForLevel } from '@domain/value-objects/XpTable'; +import type { GaugeType } from '@domain/value-objects/GaugeType'; + +export interface XpEtaInput { + currentLevel: number; + target: number | null; + gaugeLevels: Readonly>; + activeGauges: readonly string[]; +} + +export interface XpEtaResult { + done: boolean; + seconds: number; + needsGauge?: GaugeType; +} + +export class XpCalculator { + computeEta(input: XpEtaInput): XpEtaResult { + const { currentLevel, target, gaugeLevels, activeGauges } = input; + if (target === null || target === undefined) return { done: false, seconds: 0 }; + if (currentLevel >= target) return { done: true, seconds: 0 }; + if (!activeGauges.includes('mangeoire')) return { done: false, seconds: Infinity, needsGauge: 'mangeoire' }; + + const gl = gaugeLevels['mangeoire'] ?? 0; + const xpNeeded = Math.max(0, xpForLevel(target) - xpForLevel(currentLevel)); + if (xpNeeded <= 0) return { done: true, seconds: 0 }; + + const sec = timeToGain(gl, xpNeeded); + return { done: false, seconds: sec }; + } +} diff --git a/src/domain/value-objects/GaugeType.ts b/src/domain/value-objects/GaugeType.ts new file mode 100644 index 0000000..945f8c0 --- /dev/null +++ b/src/domain/value-objects/GaugeType.ts @@ -0,0 +1,49 @@ +export type GaugeType = 'baffeur' | 'caresseur' | 'foudroyeur' | 'abreuvoir' | 'dragofesse' | 'mangeoire'; +export type StatType = 'serenite' | 'endurance' | 'maturite' | 'amour' | 'xp'; + +export interface GaugeDef { + readonly label: string; + readonly icon: string; + readonly cssVar: string; + readonly stat: StatType; + readonly dir: -1 | 1; + readonly isXp?: true; +} + +export const GAUGE_DEFS: Readonly> = { + baffeur: { label: 'Baffeur', icon: '➖', cssVar: '--ser', stat: 'serenite', dir: -1 }, + caresseur: { label: 'Caresseur', icon: '➕', cssVar: '--ser', stat: 'serenite', dir: 1 }, + foudroyeur: { label: 'Foudroyeur', icon: '⚡', cssVar: '--end', stat: 'endurance', dir: 1 }, + abreuvoir: { label: 'Abreuvoir', icon: '💧', cssVar: '--mat', stat: 'maturite', dir: 1 }, + dragofesse: { label: 'Dragofesse', icon: '❤', cssVar: '--amour', stat: 'amour', dir: 1 }, + mangeoire: { label: 'Mangeoire', icon: '🍖', cssVar: '--xp', stat: 'xp', dir: 1, isXp: true }, +}; + +export interface StatDef { + readonly label: string; + readonly min: number; + readonly max: number; + readonly cssVar: string; + readonly isLevel?: true; +} + +export const STAT_DEFS: Readonly> = { + serenite: { label: 'Sérénité', min: -5000, max: 5000, cssVar: '--ser' }, + endurance: { label: 'Endurance', min: 0, max: 20000, cssVar: '--end' }, + maturite: { label: 'Maturité', min: 0, max: 20000, cssVar: '--mat' }, + amour: { label: 'Amour', min: 0, max: 20000, cssVar: '--amour' }, + xp: { label: 'Niveau', min: 1, max: 200, cssVar: '--xp', isLevel: true }, +}; + +export const DEFAULT_TARGETS: Readonly> = { + baffeur: -5000, caresseur: 40, foudroyeur: 20000, abreuvoir: 20000, dragofesse: 20000, mangeoire: 100, +}; + +export function targetRange(gid: GaugeType): { min: number; max: number } { + const def = GAUGE_DEFS[gid]; + if (def.isXp) return { min: 1, max: 200 }; + const sd = STAT_DEFS[def.stat]; + if (def.dir < 0) return { min: sd.min, max: 0 }; + if (def.dir > 0 && sd.min < 0) return { min: 0, max: sd.max }; + return { min: sd.min, max: sd.max }; +} diff --git a/src/domain/value-objects/Gender.ts b/src/domain/value-objects/Gender.ts new file mode 100644 index 0000000..2505059 --- /dev/null +++ b/src/domain/value-objects/Gender.ts @@ -0,0 +1 @@ +export type Gender = 'm' | 'f' | 'n'; diff --git a/src/domain/value-objects/Race.ts b/src/domain/value-objects/Race.ts new file mode 100644 index 0000000..e6a25e6 --- /dev/null +++ b/src/domain/value-objects/Race.ts @@ -0,0 +1,257 @@ +// ══════════════════════════════════════════ +// Race Value Object — extracted from index.html +// ══════════════════════════════════════════ + +export interface RaceData { + name: string; + stats: string[]; + parents: string; + icon: string; +} + +// Generation → color +export const GEN_COLORS: Record = { + 1: '#c8622a', + 2: '#e8b820', + 3: '#6040b0', + 4: '#2a8acc', + 5: '#c03050', + 6: '#d040a0', + 7: '#c8c0a0', + 8: '#20a8b0', + 9: '#28a058', + 10: '#8050a0', +}; + +// Base race → primary color +export const RACE_BASE_COLORS: Record = { + 'Rousse': '#c8622a', + 'Amande': '#d4b48a', + 'Dorée': '#e8b820', + 'Ebène': '#2a2a2a', + 'Indigo': '#6040b0', + 'Pourpre': '#c03050', + 'Orchidée': '#d040a0', + 'Ivoire': '#c8c0a0', + 'Turquoise': '#20a8b0', + 'Emeraude': '#28a058', + 'Prune': '#8050a0', +}; + +// All race definitions by generation +export const RACES_DATA: Record = { + 2: [ + { name: 'Amande et Rousse', stats: ['400 Vitalité', '60 Soins', '1200 Initiative'], parents: 'Amande + Rousse', icon: '🐦' }, + { name: 'Dorée et Rousse', stats: ['400 Vitalité', '1 Invocation', '45 Soins'], parents: 'Dorée + Rousse', icon: '🐦' }, + { name: 'Amande et Dorée', stats: ['400 Vitalité', '1 Invocation', '1200 Initiative'], parents: 'Amande + Dorée', icon: '🐦' }, + ], + 3: [ + { name: 'Ebène', stats: ['400 Vitalité', '120 Agilité'], parents: 'Amande et Dorée + Dorée et Rousse', icon: '🐦' }, + { name: 'Indigo', stats: ['400 Vitalité', '120 Chance'], parents: 'Amande et Dorée + Amande et Rousse', icon: '🐦' }, + ], + 4: [ + { name: 'Indigo et Rousse', stats: ['400 Vitalité', '90 Chance', '45 Soins'], parents: 'Indigo + Rousse', icon: '🐦' }, + { name: 'Ebène et Rousse', stats: ['400 Vitalité', '90 Agilité', '45 Soins'], parents: 'Ebène + Rousse', icon: '🐦' }, + { name: 'Amande et Indigo', stats: ['400 Vitalité', '90 Chance', '1200 Initiative'], parents: 'Amande + Indigo', icon: '🐦' }, + { name: 'Amande et Ebène', stats: ['400 Vitalité', '120 Agilité', '1200 Initiative'], parents: 'Amande + Ebène', icon: '🐦' }, + { name: 'Dorée et Indigo', stats: ['400 Vitalité', '90 Chance', '1 Invocation'], parents: 'Dorée + Indigo', icon: '🐦' }, + { name: 'Dorée et Ebène', stats: ['400 Vitalité', '90 Agilité', '1 Invocation'], parents: 'Dorée + Ebène', icon: '🐦' }, + { name: 'Ebène et Indigo', stats: ['400 Vitalité', '90 Chance', '90 Agilité'], parents: 'Ebène + Indigo', icon: '🐦' }, + ], + 5: [ + { name: 'Pourpre', stats: ['400 Vitalité', '120 Force'], parents: 'Ebène et Indigo + Amande et Rousse', icon: '🐦' }, + { name: 'Orchidée', stats: ['400 Vitalité', '120 Intelligence'], parents: 'Ebène et Indigo + Dorée et Rousse', icon: '🐦' }, + ], + 6: [ + { name: 'Pourpre et Rousse', stats: ['400 Vitalité', '90 Force', '45 Soins'], parents: 'Pourpre + Rousse', icon: '🐦' }, + { name: 'Orchidée et Rousse', stats: ['400 Vitalité', '90 Intelligence', '45 Soins'], parents: 'Orchidée + Rousse', icon: '🐦' }, + { name: 'Amande et Pourpre', stats: ['400 Vitalité', '90 Force', '1200 Initiative'], parents: 'Amande + Pourpre', icon: '🐦' }, + { name: 'Amande et Orchidée', stats: ['400 Vitalité', '90 Intelligence', '1200 Initiative'], parents: 'Amande + Orchidée', icon: '🐦' }, + { name: 'Dorée et Pourpre', stats: ['400 Vitalité', '90 Force', '1 Invocation'], parents: 'Dorée + Pourpre', icon: '🐦' }, + { name: 'Dorée et Orchidée', stats: ['400 Vitalité', '90 Intelligence', '1 Invocation'], parents: 'Dorée + Orchidée', icon: '🐦' }, + { name: 'Indigo et Pourpre', stats: ['400 Vitalité', '90 Force', '90 Chance'], parents: 'Indigo + Pourpre', icon: '🐦' }, + { name: 'Indigo et Orchidée', stats: ['400 Vitalité', '90 Intelligence', '90 Chance'], parents: 'Indigo + Orchidée', icon: '🐦' }, + { name: 'Ebène et Pourpre', stats: ['400 Vitalité', '90 Force', '90 Agilité'], parents: 'Ebène + Pourpre', icon: '🐦' }, + { name: 'Ebène et Orchidée', stats: ['400 Vitalité', '90 Intelligence', '90 Agilité'], parents: 'Ebène + Orchidée', icon: '🐦' }, + { name: 'Orchidée et Pourpre', stats: ['400 Vitalité', '90 Force', '90 Intelligence'], parents: 'Orchidée + Pourpre', icon: '🐦' }, + ], + 7: [ + { name: 'Ivoire', stats: ['400 Vitalité', '90 Puissance'], parents: 'Orchidée et Pourpre + Indigo et Pourpre', icon: '🐦' }, + { name: 'Turquoise', stats: ['400 Vitalité', '90 Prospection'], parents: 'Orchidée et Pourpre + Ebène et Orchidée', icon: '🐦' }, + ], + 8: [ + { name: 'Ivoire et Rousse', stats: ['400 Vitalité', '70 Puissance', '45 Soins'], parents: 'Ivoire + Rousse', icon: '🐦' }, + { name: 'Turquoise et Rousse', stats: ['400 Vitalité', '45 Soins', '70 Prospection'], parents: 'Turquoise + Rousse', icon: '🐦' }, + { name: 'Amande et Ivoire', stats: ['400 Vitalité', '70 Puissance', '1200 Initiative'], parents: 'Amande + Ivoire', icon: '🐦' }, + { name: 'Amande et Turquoise', stats: ['400 Vitalité', '70 Prospection', '1200 Initiative'], parents: 'Amande + Turquoise', icon: '🐦' }, + { name: 'Dorée et Ivoire', stats: ['400 Vitalité', '70 Puissance', '1 Invocation'], parents: 'Dorée + Ivoire', icon: '🐦' }, + { name: 'Dorée et Turquoise', stats: ['400 Vitalité', '1 Invocation', '70 Prospection'], parents: 'Dorée + Turquoise', icon: '🐦' }, + { name: 'Indigo et Ivoire', stats: ['400 Vitalité', '90 Chance', '70 Puissance'], parents: 'Indigo + Ivoire', icon: '🐦' }, + { name: 'Indigo et Turquoise', stats: ['400 Vitalité', '90 Chance', '70 Prospection'], parents: 'Indigo + Turquoise', icon: '🐦' }, + { name: 'Ebène et Ivoire', stats: ['400 Vitalité', '90 Agilité', '70 Puissance'], parents: 'Ebène + Ivoire', icon: '🐦' }, + { name: 'Ebène et Turquoise', stats: ['400 Vitalité', '90 Agilité', '70 Prospection'], parents: 'Ebène + Turquoise', icon: '🐦' }, + { name: 'Ivoire et Pourpre', stats: ['400 Vitalité', '90 Force', '70 Puissance'], parents: 'Ivoire + Pourpre', icon: '🐦' }, + { name: 'Turquoise et Pourpre', stats: ['400 Vitalité', '90 Force', '70 Prospection'], parents: 'Turquoise + Pourpre', icon: '🐦' }, + { name: 'Ivoire et Orchidée', stats: ['400 Vitalité', '90 Intelligence', '70 Puissance'], parents: 'Ivoire + Orchidée', icon: '🐦' }, + { name: 'Turquoise et Orchidée', stats: ['400 Vitalité', '90 Intelligence', '70 Prospection'], parents: 'Turquoise + Orchidée', icon: '🐦' }, + { name: 'Ivoire et Turquoise', stats: ['400 Vitalité', '70 Puissance', '70 Prospection'], parents: 'Ivoire + Turquoise', icon: '🐦' }, + ], + 9: [ + { name: 'Emeraude', stats: ['400 Vitalité', '14% Critique'], parents: 'Ivoire et Turquoise + Ivoire et Pourpre', icon: '🐦' }, + { name: 'Prune', stats: ['400 Vitalité', '2 Portée'], parents: 'Ivoire et Turquoise + Turquoise et Orchidée', icon: '🐦' }, + ], + 10: [ + { name: 'Emeraude et Rousse', stats: ['400 Vitalité', '10% Critique', '45 Soins'], parents: 'Emeraude + Rousse', icon: '🐦' }, + { name: 'Prune et Rousse', stats: ['400 Vitalité', '1 Portée', '45 Soins'], parents: 'Prune + Rousse', icon: '🐦' }, + { name: 'Amande et Emeraude', stats: ['400 Vitalité', '10% Critique', '1200 Initiative'], parents: 'Amande + Emeraude', icon: '🐦' }, + { name: 'Prune et Amande', stats: ['400 Vitalité', '1 Portée', '1200 Initiative'], parents: 'Prune + Amande', icon: '🐦' }, + { name: 'Dorée et Emeraude', stats: ['400 Vitalité', '10% Critique', '1 Invocation'], parents: 'Dorée + Emeraude', icon: '🐦' }, + { name: 'Prune et Dorée', stats: ['400 Vitalité', '1 Portée', '1 Invocation'], parents: 'Prune + Dorée', icon: '🐦' }, + { name: 'Emeraude et Indigo', stats: ['400 Vitalité', '90 Chance', '10% Critique'], parents: 'Emeraude + Indigo', icon: '🐦' }, + { name: 'Prune et Indigo', stats: ['400 Vitalité', '90 Chance', '1 Portée'], parents: 'Prune + Indigo', icon: '🐦' }, + { name: 'Ebène et Emeraude', stats: ['400 Vitalité', '90 Agilité', '10% Critique'], parents: 'Ebène + Emeraude', icon: '🐦' }, + { name: 'Prune et Ebène', stats: ['400 Vitalité', '90 Agilité', '1 Portée'], parents: 'Prune + Ebène', icon: '🐦' }, + { name: 'Emeraude et Pourpre', stats: ['400 Vitalité', '90 Force', '10% Critique'], parents: 'Emeraude + Pourpre', icon: '🐦' }, + { name: 'Prune et Pourpre', stats: ['400 Vitalité', '90 Force', '1 Portée'], parents: 'Prune + Pourpre', icon: '🐦' }, + { name: 'Emeraude et Orchidée', stats: ['400 Vitalité', '90 Intelligence', '10% Critique'], parents: 'Emeraude + Orchidée', icon: '🐦' }, + { name: 'Prune et Orchidée', stats: ['400 Vitalité', '90 Intelligence', '1 Portée'], parents: 'Prune + Orchidée', icon: '🐦' }, + { name: 'Emeraude et Ivoire', stats: ['400 Vitalité', '70 Puissance', '10% Critique'], parents: 'Emeraude + Ivoire', icon: '🐦' }, + { name: 'Prune et Ivoire', stats: ['400 Vitalité', '70 Puissance', '1 Portée'], parents: 'Prune + Ivoire', icon: '🐦' }, + { name: 'Emeraude et Turquoise', stats: ['400 Vitalité', '10% Critique', '70 Prospection'], parents: 'Emeraude + Turquoise', icon: '🐦' }, + { name: 'Prune et Turquoise', stats: ['400 Vitalité', '1 Portée', '70 Prospection'], parents: 'Prune + Turquoise', icon: '🐦' }, + { name: 'Prune et Emeraude', stats: ['400 Vitalité', '10% Critique', '1 Portée'], parents: 'Prune + Emeraude', icon: '🐦' }, + ], +}; + +// Breeding recipes: baby race → [parentA, parentB] +export const BREEDING_RECIPES: Record = { + 'Dorée et Rousse': ['Rousse', 'Dorée'], + 'Amande et Dorée': ['Amande', 'Dorée'], + 'Amande et Rousse': ['Amande', 'Rousse'], + 'Ebène': ['Amande et Dorée', 'Dorée et Rousse'], + 'Indigo': ['Amande et Dorée', 'Amande et Rousse'], + 'Indigo et Rousse': ['Indigo', 'Rousse'], + 'Ebène et Rousse': ['Ebène', 'Rousse'], + 'Amande et Indigo': ['Amande', 'Indigo'], + 'Amande et Ebène': ['Amande', 'Ebène'], + 'Dorée et Indigo': ['Dorée', 'Indigo'], + 'Dorée et Ebène': ['Dorée', 'Ebène'], + 'Ebène et Indigo': ['Ebène', 'Indigo'], + 'Pourpre': ['Ebène et Indigo', 'Amande et Rousse'], + 'Orchidée': ['Ebène et Indigo', 'Dorée et Rousse'], + 'Pourpre et Rousse': ['Pourpre', 'Rousse'], + 'Orchidée et Rousse': ['Orchidée', 'Rousse'], + 'Amande et Pourpre': ['Amande', 'Pourpre'], + 'Amande et Orchidée': ['Amande', 'Orchidée'], + 'Dorée et Pourpre': ['Dorée', 'Pourpre'], + 'Dorée et Orchidée': ['Dorée', 'Orchidée'], + 'Indigo et Pourpre': ['Indigo', 'Pourpre'], + 'Indigo et Orchidée': ['Indigo', 'Orchidée'], + 'Ebène et Pourpre': ['Ebène', 'Pourpre'], + 'Ebène et Orchidée': ['Ebène', 'Orchidée'], + 'Orchidée et Pourpre': ['Orchidée', 'Pourpre'], + 'Ivoire': ['Orchidée et Pourpre', 'Indigo et Pourpre'], + 'Turquoise': ['Orchidée et Pourpre', 'Ebène et Orchidée'], + 'Ivoire et Rousse': ['Ivoire', 'Rousse'], + 'Turquoise et Rousse': ['Turquoise', 'Rousse'], + 'Amande et Ivoire': ['Amande', 'Ivoire'], + 'Amande et Turquoise': ['Amande', 'Turquoise'], + 'Dorée et Ivoire': ['Dorée', 'Ivoire'], + 'Dorée et Turquoise': ['Dorée', 'Turquoise'], + 'Indigo et Ivoire': ['Indigo', 'Ivoire'], + 'Indigo et Turquoise': ['Indigo', 'Turquoise'], + 'Ebène et Ivoire': ['Ebène', 'Ivoire'], + 'Ebène et Turquoise': ['Ebène', 'Turquoise'], + 'Ivoire et Pourpre': ['Ivoire', 'Pourpre'], + 'Turquoise et Pourpre': ['Turquoise', 'Pourpre'], + 'Ivoire et Orchidée': ['Ivoire', 'Orchidée'], + 'Turquoise et Orchidée': ['Turquoise', 'Orchidée'], + 'Ivoire et Turquoise': ['Ivoire', 'Turquoise'], + 'Emeraude': ['Ivoire et Turquoise', 'Ivoire et Pourpre'], + 'Prune': ['Ivoire et Turquoise', 'Turquoise et Orchidée'], + 'Emeraude et Rousse': ['Emeraude', 'Rousse'], + 'Prune et Rousse': ['Prune', 'Rousse'], + 'Amande et Emeraude': ['Amande', 'Emeraude'], + 'Prune et Amande': ['Prune', 'Amande'], + 'Dorée et Emeraude': ['Dorée', 'Emeraude'], + 'Prune et Dorée': ['Prune', 'Dorée'], + 'Emeraude et Indigo': ['Emeraude', 'Indigo'], + 'Prune et Indigo': ['Prune', 'Indigo'], + 'Ebène et Emeraude': ['Ebène', 'Emeraude'], + 'Prune et Ebène': ['Prune', 'Ebène'], + 'Emeraude et Pourpre': ['Emeraude', 'Pourpre'], + 'Prune et Pourpre': ['Prune', 'Pourpre'], + 'Emeraude et Orchidée': ['Emeraude', 'Orchidée'], + 'Prune et Orchidée': ['Prune', 'Orchidée'], + 'Emeraude et Ivoire': ['Emeraude', 'Ivoire'], + 'Prune et Ivoire': ['Prune', 'Ivoire'], + 'Emeraude et Turquoise': ['Emeraude', 'Turquoise'], + 'Prune et Turquoise': ['Prune', 'Turquoise'], + 'Prune et Emeraude': ['Prune', 'Emeraude'], +}; + +// Race name → generation number (computed) +export const RACE_GEN: Record = {}; +['Rousse', 'Dorée', 'Amande'].forEach((n) => (RACE_GEN[n] = 1)); +Object.entries(RACES_DATA).forEach(([g, rs]) => + rs.forEach((r) => (RACE_GEN[r.name] = parseInt(g))), +); + +// Reverse lookup: "ParentA|ParentB" → baby race +export const BREEDING_BY_PARENTS: Record = {}; +Object.entries(BREEDING_RECIPES).forEach(([baby, [a, b]]) => { + BREEDING_BY_PARENTS[a + '|' + b] = baby; + if (a !== b) BREEDING_BY_PARENTS[b + '|' + a] = baby; +}); + +// For a given parent, which partners are possible? +export const COMPATIBLE_PARTNERS: Record< + string, + { partner: string; baby: string; gen: number }[] +> = {}; +Object.entries(BREEDING_RECIPES).forEach(([baby, [a, b]]) => { + if (!COMPATIBLE_PARTNERS[a]) COMPATIBLE_PARTNERS[a] = []; + COMPATIBLE_PARTNERS[a].push({ partner: b, baby, gen: RACE_GEN[baby] }); + if (a !== b) { + if (!COMPATIBLE_PARTNERS[b]) COMPATIBLE_PARTNERS[b] = []; + COMPATIBLE_PARTNERS[b].push({ partner: a, baby, gen: RACE_GEN[baby] }); + } +}); + +// ── Helper functions ── + +const COLOR_ORDER = [ + 'Emeraude', 'Prune', 'Ivoire', 'Turquoise', + 'Orchidée', 'Pourpre', 'Indigo', 'Ebène', + 'Dorée', 'Amande', 'Rousse', +]; + +/** Returns the generation number for a race name. */ +export function generationOf(name: string): number { + return RACE_GEN[name] ?? 0; +} + +/** Returns true if the race is a base (gen 1) race. */ +export function isBaseRace(name: string): boolean { + return RACE_GEN[name] === 1; +} + +/** Returns the primary color for a race name. */ +export function raceColor(name: string): string { + for (const k of COLOR_ORDER) { + if (name.includes(k)) return RACE_BASE_COLORS[k]; + } + return '#888'; +} + +/** Returns the secondary color for gradient, or null if none. */ +export function raceColor2(name: string): string | null { + let found = false; + for (const k of COLOR_ORDER) { + if (name.includes(k)) { + if (found) return RACE_BASE_COLORS[k]; + found = true; + } + } + return null; +} diff --git a/src/domain/value-objects/Tier.ts b/src/domain/value-objects/Tier.ts new file mode 100644 index 0000000..e9fec1f --- /dev/null +++ b/src/domain/value-objects/Tier.ts @@ -0,0 +1,14 @@ +export const TIER_THRESHOLDS = [ + { lo: 90000, rate: 40, num: 4 }, + { lo: 70000, rate: 30, num: 3 }, + { lo: 40000, rate: 20, num: 2 }, + { lo: 0, rate: 10, num: 1 }, +] as const; + +export function tierRate(level: number): number { + return level > 90000 ? 40 : level > 70000 ? 30 : level > 40000 ? 20 : 10; +} + +export function tierNum(level: number): number { + return level > 90000 ? 4 : level > 70000 ? 3 : level > 40000 ? 2 : 1; +} diff --git a/src/domain/value-objects/XpTable.ts b/src/domain/value-objects/XpTable.ts new file mode 100644 index 0000000..cf7970d --- /dev/null +++ b/src/domain/value-objects/XpTable.ts @@ -0,0 +1,16 @@ +// Copy of XP_RAW from src/index.html line 357 +export const XP_RAW: Readonly> = { + 1:0,2:19,3:49,4:96,5:161,6:246,7:353,8:481,9:633,10:809,11:1011,12:1238,13:1491,14:1772,15:2081,16:2419,17:2786,18:3182,19:3609,20:4067,21:4557,22:5078,23:5632,24:6219,25:6839,26:7493,27:8182,28:8905,29:9664,30:10457,31:11287,32:12154,33:13057,34:13997,35:14974,36:15990,37:17043,38:18135,39:19266,40:20437,41:21646,42:22896,43:24186,44:25516,45:26887,46:28299,47:29753,48:31248,49:32785,50:34365,51:35987,52:37652,53:39360,54:41111,55:42906,56:44745,57:46628,58:48555,59:50527,60:52544,61:54607,62:56714,63:58868,64:61067,65:63312,66:65604,67:67942,68:70327,69:72760,70:75239,71:77766,72:80341,73:82964,74:85635,75:88355,76:91123,77:93940,78:96806,79:99721,80:102685,81:105700,82:108764,83:111878,84:115042,85:118257,86:121523,87:124840,88:128207,89:131626,90:135096,91:138618,92:142191,93:145817,94:149495,95:153225,96:157008,97:160843,98:164732,99:168673,100:172668,101:176716,102:180818,103:184974,104:189183,105:193447,106:197765,107:202137,108:206565,109:211046,110:215583,111:220176,112:224823,113:229526,114:234284,115:239099,116:243969,117:248895,118:253878,119:258917,120:264013,121:269165,122:274375,123:279641,124:284965,125:290346,126:295784,127:301280,128:306834,129:312446,130:318116,131:323845,132:329631,133:335477,134:341381,135:347343,136:353365,137:359446,138:365587,139:371786,140:378045,141:384364,142:390743,143:397182,144:403681,145:410240,146:416859,147:423539,148:430280,149:437082,150:443944,151:450868,152:457852,153:464898,154:472006,155:479175,156:486406,157:493699,158:501054,159:508470,160:515950,161:523491,162:531095,163:538762,164:546491,165:554283,166:562139,167:570057,168:578039,169:586084,170:594193,171:602365,172:610601,173:618901,174:627265,175:635693,176:644185,177:652742,178:661363,179:670049,180:678799,181:687615,182:696495,183:705440,184:714451,185:723527,186:732668,187:741875,188:751148,189:760486,190:769890,191:779361,192:788897,193:798500,194:808169,195:817904,196:827706,197:837575,198:847510,199:857513,200:867582, +}; + +export function xpForLevel(lvl: number): number { + return XP_RAW[Math.min(200, Math.max(1, Math.round(lvl)))] ?? 0; +} + +export function levelFromXp(xp: number): number { + if (xp >= XP_RAW[200]!) return 200; + for (let i = 199; i >= 1; i--) { + if (XP_RAW[i]! <= xp) return i; + } + return 1; +} diff --git a/src/index.html b/src/index.html index 6aea6b1..71ac076 100644 --- a/src/index.html +++ b/src/index.html @@ -1,3130 +1,16 @@ - - -Minuteur Dragodinde · Dofus 3 - - + + + + L'Archive d'Obsidienne · Dofus 3 + + + - - - -
-
-

⚔ MINUTEUR DRAGODINDE

-

Dofus 3 · Gestion multi-enclos en temps réel ·

-
- - - - -
-
-
- - - - - +
+ diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..cda9a15 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": false, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist-ts", + "rootDir": "src", + "baseUrl": ".", + "paths": { + "@domain/*": ["src/domain/*"], + "@application/*": ["src/application/*"], + "@infrastructure/*": ["src/infrastructure/*"], + "@presentation/*": ["src/presentation/*"] + } + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist", "dist-ts", "tests"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..5c86517 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,44 @@ +import { defineConfig } from 'vite'; +import electron from 'vite-plugin-electron'; +import renderer from 'vite-plugin-electron-renderer'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [ + electron([ + { + entry: resolve(__dirname, 'src/infrastructure/electron/main.ts'), + vite: { + build: { + outDir: resolve(__dirname, 'dist-electron'), + rollupOptions: { external: ['electron'] }, + }, + }, + }, + { + entry: resolve(__dirname, 'src/infrastructure/electron/preload.ts'), + onstart(args) { args.reload(); }, + vite: { + build: { + outDir: resolve(__dirname, 'dist-electron'), + rollupOptions: { external: ['electron'] }, + }, + }, + }, + ]), + renderer(), + ], + resolve: { + alias: { + '@domain': resolve(__dirname, 'src/domain'), + '@application': resolve(__dirname, 'src/application'), + '@infrastructure': resolve(__dirname, 'src/infrastructure'), + '@presentation': resolve(__dirname, 'src/presentation'), + }, + }, + root: 'src', + build: { + outDir: resolve(__dirname, 'dist-vite'), + emptyOutDir: true, + }, +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..b0fe22c --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,28 @@ +import { defineConfig } from 'vitest/config'; +import { resolve } from 'path'; + +export default defineConfig({ + resolve: { + alias: { + '@domain': resolve(__dirname, 'src/domain'), + '@application': resolve(__dirname, 'src/application'), + '@infrastructure': resolve(__dirname, 'src/infrastructure'), + '@presentation': resolve(__dirname, 'src/presentation'), + }, + }, + test: { + globals: true, + environment: 'node', + include: ['tests/**/*.test.ts'], + coverage: { + provider: 'v8', + include: ['src/domain/**', 'src/application/**'], + exclude: [ + 'src/domain/ports/**', + 'src/domain/events/DomainEvent.ts', + 'src/domain/value-objects/Gender.ts', + ], + thresholds: { branches: 80, functions: 80, lines: 80 }, + }, + }, +});