Compare commits

...

6 Commits

Author SHA1 Message Date
Peter Stockings
dd85891831 Update hud 2026-01-07 18:30:39 +11:00
Peter Stockings
47e15ccf5c Update look of quick slot and action buttons 2026-01-07 18:30:27 +11:00
Peter Stockings
d01dd8a4fc Update look of quickslots 2026-01-07 16:57:54 +11:00
Peter Stockings
4ca932e11c Add workflow to check lines of code excluding tests 2026-01-07 16:52:07 +11:00
Peter Stockings
b503199ba9 Remove reference to soldier sprite 2026-01-07 09:28:40 +11:00
Peter Stockings
fcd31cce68 Further refactoring 2026-01-07 09:19:38 +11:00
24 changed files with 1150 additions and 394 deletions

7
.agent/workflows/loc.md Normal file
View File

@@ -0,0 +1,7 @@
---
description: Count lines of TypeScript source code excluding tests
---
// turbo-all
1. Count TypeScript lines excluding tests: `(Get-ChildItem -Recurse -Include *.ts -Exclude *.test.ts,*.spec.ts -Path . | Where-Object { $_.FullName -notmatch '\\node_modules\\' } | Get-Content |
Measure-Object -Line).Lines`

1
.gitignore vendored
View File

@@ -24,3 +24,4 @@ dist-ssr
*.sw? *.sw?
coverage coverage
.eslintcache

196
bun.lock
View File

@@ -9,7 +9,11 @@
"rot-js": "^2.2.1", "rot-js": "^2.2.1",
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^8.52.0",
"@typescript-eslint/parser": "^8.52.0",
"@vitest/coverage-v8": "^4.0.16", "@vitest/coverage-v8": "^4.0.16",
"eslint": "^9.39.2",
"globals": "^17.0.0",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"vite": "^7.2.4", "vite": "^7.2.4",
"vitest": "^4.0.16", "vitest": "^4.0.16",
@@ -79,6 +83,32 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="],
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
"@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="],
"@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
"@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="],
"@eslint/js": ["@eslint/js@9.39.2", "", {}, "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA=="],
"@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
"@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
@@ -137,6 +167,28 @@
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.52.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/type-utils": "8.52.0", "@typescript-eslint/utils": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.52.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-okqtOgqu2qmZJ5iN4TWlgfF171dZmx2FzdOv2K/ixL2LZWDStL8+JgQerI2sa8eAEfoydG9+0V96m7V+P8yE1Q=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.52.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.52.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.52.0", "@typescript-eslint/types": "^8.52.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-xD0MfdSdEmeFa3OmVqonHi+Cciab96ls1UhIF/qX/O/gPu5KXD0bY9lu33jj04fjzrXHcuvjBcBC+D3SNSadaw=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0" } }, "sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.52.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-jl+8fzr/SdzdxWJznq5nvoI7qn2tNYV/ZBAEcaFMVXf+K6jmXvAFrgo/+5rxgnL152f//pDEAYAhhBAZGrVfwg=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/utils": "8.52.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-JD3wKBRWglYRQkAtsyGz1AewDu3mTc7NtRjR/ceTyGoPqmdS5oCdx/oZMWD5Zuqmo6/MpsYs0wp6axNt88/2EQ=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.52.0", "", {}, "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.52.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.52.0", "@typescript-eslint/tsconfig-utils": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XP3LClsCc0FsTK5/frGjolyADTh3QmsLp6nKd476xNI9CsSsLnmn4f0jrzNoAulmxlmNIpeXuHYeEQv61Q6qeQ=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.52.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ=="],
"@vitest/coverage-v8": ["@vitest/coverage-v8@4.0.16", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.0.16", "ast-v8-to-istanbul": "^0.3.8", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.2.0", "magicast": "^0.5.1", "obug": "^2.1.1", "std-env": "^3.10.0", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "@vitest/browser": "4.0.16", "vitest": "4.0.16" }, "optionalPeers": ["@vitest/browser"] }, "sha512-2rNdjEIsPRzsdu6/9Eq0AYAzYdpP6Bx9cje9tL3FE5XzXRQF1fNU9pe/1yE8fCrS0HD+fBtt6gLPh6LI57tX7A=="], "@vitest/coverage-v8": ["@vitest/coverage-v8@4.0.16", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.0.16", "ast-v8-to-istanbul": "^0.3.8", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.2.0", "magicast": "^0.5.1", "obug": "^2.1.1", "std-env": "^3.10.0", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "@vitest/browser": "4.0.16", "vitest": "4.0.16" }, "optionalPeers": ["@vitest/browser"] }, "sha512-2rNdjEIsPRzsdu6/9Eq0AYAzYdpP6Bx9cje9tL3FE5XzXRQF1fNU9pe/1yE8fCrS0HD+fBtt6gLPh6LI57tX7A=="],
"@vitest/expect": ["@vitest/expect@4.0.16", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.16", "@vitest/utils": "4.0.16", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA=="], "@vitest/expect": ["@vitest/expect@4.0.16", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.16", "@vitest/utils": "4.0.16", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA=="],
@@ -153,32 +205,108 @@
"@vitest/utils": ["@vitest/utils@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "tinyrainbow": "^3.0.3" } }, "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA=="], "@vitest/utils": ["@vitest/utils@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "tinyrainbow": "^3.0.3" } }, "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
"ast-v8-to-istanbul": ["ast-v8-to-istanbul@0.3.10", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^9.0.1" } }, "sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ=="], "ast-v8-to-istanbul": ["ast-v8-to-istanbul@0.3.10", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^9.0.1" } }, "sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
"chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
"es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
"esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="],
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
"espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
"esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="],
"expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"globals": ["globals@17.0.0", "", {}, "sha512-gv5BeD2EssA793rlFWVPMMCqefTlpusw6/2TbAVMy0FzcG8wKJn4O+NqJ4+XWmmwrayJgw5TzrmWjFgmz1XPqw=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="],
"ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="],
"istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="], "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="],
@@ -189,18 +317,50 @@
"js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"magicast": ["magicast@0.5.1", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "source-map-js": "^1.2.1" } }, "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw=="], "magicast": ["magicast@0.5.1", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "source-map-js": "^1.2.1" } }, "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw=="],
"make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="],
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"phaser": ["phaser@3.90.0", "", { "dependencies": { "eventemitter3": "^5.0.1" } }, "sha512-/cziz/5ZIn02uDkC9RzN8VF9x3Gs3XdFFf9nkiMEQT3p7hQlWuyjy4QWosU802qqno2YSLn2BfqwOKLv/sSVfQ=="], "phaser": ["phaser@3.90.0", "", { "dependencies": { "eventemitter3": "^5.0.1" } }, "sha512-/cziz/5ZIn02uDkC9RzN8VF9x3Gs3XdFFf9nkiMEQT3p7hQlWuyjy4QWosU802qqno2YSLn2BfqwOKLv/sSVfQ=="],
@@ -211,12 +371,22 @@
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"rollup": ["rollup@4.54.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.54.0", "@rollup/rollup-android-arm64": "4.54.0", "@rollup/rollup-darwin-arm64": "4.54.0", "@rollup/rollup-darwin-x64": "4.54.0", "@rollup/rollup-freebsd-arm64": "4.54.0", "@rollup/rollup-freebsd-x64": "4.54.0", "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", "@rollup/rollup-linux-arm-musleabihf": "4.54.0", "@rollup/rollup-linux-arm64-gnu": "4.54.0", "@rollup/rollup-linux-arm64-musl": "4.54.0", "@rollup/rollup-linux-loong64-gnu": "4.54.0", "@rollup/rollup-linux-ppc64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-musl": "4.54.0", "@rollup/rollup-linux-s390x-gnu": "4.54.0", "@rollup/rollup-linux-x64-gnu": "4.54.0", "@rollup/rollup-linux-x64-musl": "4.54.0", "@rollup/rollup-openharmony-arm64": "4.54.0", "@rollup/rollup-win32-arm64-msvc": "4.54.0", "@rollup/rollup-win32-ia32-msvc": "4.54.0", "@rollup/rollup-win32-x64-gnu": "4.54.0", "@rollup/rollup-win32-x64-msvc": "4.54.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw=="], "rollup": ["rollup@4.54.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.54.0", "@rollup/rollup-android-arm64": "4.54.0", "@rollup/rollup-darwin-arm64": "4.54.0", "@rollup/rollup-darwin-x64": "4.54.0", "@rollup/rollup-freebsd-arm64": "4.54.0", "@rollup/rollup-freebsd-x64": "4.54.0", "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", "@rollup/rollup-linux-arm-musleabihf": "4.54.0", "@rollup/rollup-linux-arm64-gnu": "4.54.0", "@rollup/rollup-linux-arm64-musl": "4.54.0", "@rollup/rollup-linux-loong64-gnu": "4.54.0", "@rollup/rollup-linux-ppc64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-musl": "4.54.0", "@rollup/rollup-linux-s390x-gnu": "4.54.0", "@rollup/rollup-linux-x64-gnu": "4.54.0", "@rollup/rollup-linux-x64-musl": "4.54.0", "@rollup/rollup-openharmony-arm64": "4.54.0", "@rollup/rollup-win32-arm64-msvc": "4.54.0", "@rollup/rollup-win32-ia32-msvc": "4.54.0", "@rollup/rollup-win32-x64-gnu": "4.54.0", "@rollup/rollup-win32-x64-msvc": "4.54.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw=="],
"rot-js": ["rot-js@2.2.1", "", {}, "sha512-lItXH31vj4ebdypayCx9dh98qPr57E7jGW2lVMKxtBHooU3xpGRtLS8kdJIni232tvPJ8Sl0+aqXZj8c6W0MGw=="], "rot-js": ["rot-js@2.2.1", "", {}, "sha512-lItXH31vj4ebdypayCx9dh98qPr57E7jGW2lVMKxtBHooU3xpGRtLS8kdJIni232tvPJ8Sl0+aqXZj8c6W0MGw=="],
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
@@ -225,6 +395,8 @@
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
@@ -235,12 +407,36 @@
"tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="], "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="],
"ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"vite": ["vite@7.3.0", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.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" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="], "vite": ["vite@7.3.0", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.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" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="],
"vitest": ["vitest@4.0.16", "", { "dependencies": { "@vitest/expect": "4.0.16", "@vitest/mocker": "4.0.16", "@vitest/pretty-format": "4.0.16", "@vitest/runner": "4.0.16", "@vitest/snapshot": "4.0.16", "@vitest/spy": "4.0.16", "@vitest/utils": "4.0.16", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.16", "@vitest/browser-preview": "4.0.16", "@vitest/browser-webdriverio": "4.0.16", "@vitest/ui": "4.0.16", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q=="], "vitest": ["vitest@4.0.16", "", { "dependencies": { "@vitest/expect": "4.0.16", "@vitest/mocker": "4.0.16", "@vitest/pretty-format": "4.0.16", "@vitest/runner": "4.0.16", "@vitest/snapshot": "4.0.16", "@vitest/spy": "4.0.16", "@vitest/utils": "4.0.16", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.16", "@vitest/browser-preview": "4.0.16", "@vitest/browser-webdriverio": "4.0.16", "@vitest/ui": "4.0.16", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
"@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
} }
} }

54
eslint.config.js Normal file
View File

@@ -0,0 +1,54 @@
// @ts-check
import tseslint from "@typescript-eslint/eslint-plugin";
import tsparser from "@typescript-eslint/parser";
export default [
{
files: ["src/**/*.ts", "src/**/*.tsx"],
languageOptions: {
parser: tsparser,
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
},
plugins: {
"@typescript-eslint": tseslint,
},
rules: {
// TypeScript recommended rules (subset)
"@typescript-eslint/no-explicit-any": "warn", // Warn, not error for gradual migration
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
},
],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-non-null-assertion": "warn",
// General code quality
"no-console": "off", // Game dev needs console
"prefer-const": "error",
"no-var": "error",
},
},
{
files: ["src/**/*.test.ts", "src/**/__tests__/**/*.ts"],
rules: {
// More lenient for tests
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off",
},
},
{
ignores: [
"node_modules/**",
"dist/**",
"coverage/**",
"*.config.js",
"*.config.ts",
],
},
];

View File

@@ -10,10 +10,16 @@
"test": "vitest", "test": "vitest",
"test:coverage": "vitest run --coverage", "test:coverage": "vitest run --coverage",
"check": "tsc --noEmit", "check": "tsc --noEmit",
"lint": "eslint src --ext .ts,.tsx",
"lint:fix": "eslint src --ext .ts,.tsx --fix",
"verify": "bun run check && bun run test" "verify": "bun run check && bun run test"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^8.52.0",
"@typescript-eslint/parser": "^8.52.0",
"@vitest/coverage-v8": "^4.0.16", "@vitest/coverage-v8": "^4.0.16",
"eslint": "^9.39.2",
"globals": "^17.0.0",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"vite": "^7.2.4", "vite": "^7.2.4",
"vitest": "^4.0.16" "vitest": "^4.0.16"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 B

View File

@@ -121,7 +121,11 @@ export const GAME_CONFIG = {
minimapPanelHeight: 220, minimapPanelHeight: 220,
minimapPadding: 20, minimapPadding: 20,
menuPanelWidth: 340, menuPanelWidth: 340,
menuPanelHeight: 220 menuPanelHeight: 220,
// Targeting
targetingLineColor: 0xff0000,
targetingLineWidth: 2,
targetingLineAlpha: 0.7
}, },
gameplay: { gameplay: {
@@ -135,7 +139,6 @@ export const GAME_CONFIG = {
{ key: "rat", path: "assets/sprites/actors/enemies/rat.png", frameConfig: { frameWidth: 16, frameHeight: 15 } }, { key: "rat", path: "assets/sprites/actors/enemies/rat.png", frameConfig: { frameWidth: 16, frameHeight: 15 } },
{ key: "bat", path: "assets/sprites/actors/enemies/bat.png", frameConfig: { frameWidth: 15, frameHeight: 15 } }, { key: "bat", path: "assets/sprites/actors/enemies/bat.png", frameConfig: { frameWidth: 15, frameHeight: 15 } },
{ key: "dungeon", path: "assets/tilesets/dungeon.png", frameConfig: { frameWidth: 16, frameHeight: 16 } }, { key: "dungeon", path: "assets/tilesets/dungeon.png", frameConfig: { frameWidth: 16, frameHeight: 16 } },
{ key: "soldier.idle", path: "assets/sprites/actors/player/soldier/Idle.png", frameConfig: { frameWidth: 60, frameHeight: 75 } },
{ key: "items", path: "assets/sprites/items/items.png", frameConfig: { frameWidth: 16, frameHeight: 16 } }, { key: "items", path: "assets/sprites/items/items.png", frameConfig: { frameWidth: 16, frameHeight: 16 } },
], ],
images: [ images: [
@@ -158,9 +161,6 @@ export const GAME_CONFIG = {
{ key: "bat-idle", textureKey: "bat", frames: [0, 1], frameRate: 8, repeat: -1 }, { key: "bat-idle", textureKey: "bat", frames: [0, 1], frameRate: 8, repeat: -1 },
{ key: "bat-run", textureKey: "bat", frames: [0, 1], frameRate: 12, repeat: -1 }, { key: "bat-run", textureKey: "bat", frames: [0, 1], frameRate: 12, repeat: -1 },
{ key: "bat-die", textureKey: "bat", frames: [4, 5, 6], frameRate: 10, repeat: 0 }, { key: "bat-die", textureKey: "bat", frames: [4, 5, 6], frameRate: 10, repeat: 0 },
// Soldier
{ key: "soldier-idle", textureKey: "soldier.idle", frames: [0, 1, 2, 3, 4, 5, 6, 7], frameRate: 8, repeat: -1 },
] ]
} as const; } as const;

View File

@@ -1,4 +1,5 @@
import { FOV } from "rot-js"; import { FOV } from "rot-js";
import type ROT from "rot-js";
import { type World, type EntityId } from "../core/types"; import { type World, type EntityId } from "../core/types";
import { idx, inBounds } from "../engine/world/world-logic"; import { idx, inBounds } from "../engine/world/world-logic";
import { blocksSight } from "../core/terrain"; import { blocksSight } from "../core/terrain";
@@ -6,7 +7,7 @@ import { GAME_CONFIG } from "../core/config/GameConfig";
import Phaser from "phaser"; import Phaser from "phaser";
export class FovManager { export class FovManager {
private fov!: any; private fov!: InstanceType<typeof ROT.FOV.PreciseShadowcasting>;
private seen!: Uint8Array; private seen!: Uint8Array;
private visible!: Uint8Array; private visible!: Uint8Array;
private visibleStrength!: Float32Array; private visibleStrength!: Float32Array;
@@ -51,12 +52,12 @@ export class FovManager {
} }
isSeen(x: number, y: number): boolean { isSeen(x: number, y: number): boolean {
if (!inBounds({ width: this.worldWidth, height: this.worldHeight } as any, x, y)) return false; if (x < 0 || x >= this.worldWidth || y < 0 || y >= this.worldHeight) return false;
return this.seen[y * this.worldWidth + x] === 1; return this.seen[y * this.worldWidth + x] === 1;
} }
isVisible(x: number, y: number): boolean { isVisible(x: number, y: number): boolean {
if (!inBounds({ width: this.worldWidth, height: this.worldHeight } as any, x, y)) return false; if (x < 0 || x >= this.worldWidth || y < 0 || y >= this.worldHeight) return false;
return this.visible[y * this.worldWidth + x] === 1; return this.visible[y * this.worldWidth + x] === 1;
} }

View File

@@ -7,8 +7,6 @@ import {
type RunState, type RunState,
type World, type World,
type CombatantActor, type CombatantActor,
type Item,
type ItemDropActor,
type UIUpdatePayload type UIUpdatePayload
} from "../core/types"; } from "../core/types";
import { TILE_SIZE } from "../core/constants"; import { TILE_SIZE } from "../core/constants";
@@ -16,14 +14,14 @@ import { inBounds, isBlocked, isPlayerOnExit, tryDestructTile } from "../engine/
import { findPathAStar } from "../engine/world/pathfinding"; import { findPathAStar } from "../engine/world/pathfinding";
import { applyAction, stepUntilPlayerTurn } from "../engine/simulation/simulation"; import { applyAction, stepUntilPlayerTurn } from "../engine/simulation/simulation";
import { generateWorld } from "../engine/world/generator"; import { generateWorld } from "../engine/world/generator";
import { traceProjectile, getClosestVisibleEnemy } from "../engine/gameplay/CombatLogic";
import { DungeonRenderer } from "../rendering/DungeonRenderer"; import { DungeonRenderer } from "../rendering/DungeonRenderer";
import { GAME_CONFIG } from "../core/config/GameConfig"; import { GAME_CONFIG } from "../core/config/GameConfig";
import { EntityManager } from "../engine/EntityManager"; import { EntityManager } from "../engine/EntityManager";
import { ProgressionManager } from "../engine/ProgressionManager"; import { ProgressionManager } from "../engine/ProgressionManager";
import GameUI from "../ui/GameUI";
import { CameraController } from "./systems/CameraController";
import { ItemManager } from "./systems/ItemManager";
import { TargetingSystem } from "./systems/TargetingSystem";
export class GameScene extends Phaser.Scene { export class GameScene extends Phaser.Scene {
private world!: World; private world!: World;
@@ -40,22 +38,18 @@ export class GameScene extends Phaser.Scene {
private playerPath: Vec2[] = []; private playerPath: Vec2[] = [];
private awaitingPlayer = false; private awaitingPlayer = false;
private followPlayer = true;
// Sub-systems // Sub-systems
private dungeonRenderer!: DungeonRenderer; private dungeonRenderer!: DungeonRenderer;
private cameraController!: CameraController;
private itemManager!: ItemManager;
private isMenuOpen = false; private isMenuOpen = false;
private isInventoryOpen = false; private isInventoryOpen = false;
private isCharacterOpen = false; private isCharacterOpen = false;
private entityManager!: EntityManager; private entityManager!: EntityManager;
private progressionManager: ProgressionManager = new ProgressionManager(); private progressionManager: ProgressionManager = new ProgressionManager();
private targetingSystem!: TargetingSystem;
// Targeting Mode
private isTargeting = false;
private targetingItem: string | null = null;
private targetCursor: { x: number, y: number } | null = null;
private targetingGraphics!: Phaser.GameObjects.Graphics;
private turnCount = 0; // Track turns for mana regen private turnCount = 0; // Track turns for mana regen
@@ -72,7 +66,10 @@ export class GameScene extends Phaser.Scene {
// Initialize Sub-systems // Initialize Sub-systems
this.dungeonRenderer = new DungeonRenderer(this); this.dungeonRenderer = new DungeonRenderer(this);
this.targetingGraphics = this.add.graphics().setDepth(2000); this.cameraController = new CameraController(this.cameras.main);
this.itemManager = new ItemManager(this.world, this.entityManager);
const targetingGraphics = this.add.graphics().setDepth(2000);
this.targetingSystem = new TargetingSystem(targetingGraphics);
// Launch UI Scene // Launch UI Scene
this.scene.launch("GameUI"); this.scene.launch("GameUI");
@@ -177,82 +174,48 @@ export class GameScene extends Phaser.Scene {
if (itemIdx === -1) return; if (itemIdx === -1) return;
const item = player.inventory.items[itemIdx]; const item = player.inventory.items[itemIdx];
if (item.stats && item.stats.hp && item.stats.hp > 0) { const result = this.itemManager.handleUse(data.itemId, player);
const healAmount = item.stats.hp;
if (player.stats.hp < player.stats.maxHp) {
player.stats.hp = Math.min(player.stats.hp + healAmount, player.stats.maxHp);
// Remove item after use if (result.success && result.consumed) {
player.inventory.items.splice(itemIdx, 1); const healAmount = player.stats.maxHp - player.stats.hp; // Already healed by manager
const actualHeal = Math.min(healAmount, player.stats.hp);
this.dungeonRenderer.showHeal(player.pos.x, player.pos.y, healAmount); this.dungeonRenderer.showHeal(player.pos.x, player.pos.y, actualHeal);
this.commitPlayerAction({ type: "wait" }); this.commitPlayerAction({ type: "wait" });
this.emitUIUpdate(); this.emitUIUpdate();
} } else if (result.success && !result.consumed) {
} else if (item.throwable) { // Throwable item - start targeting
// Check if already targeting this item -> verify intent to throw if (this.targetingSystem.isActive && this.targetingSystem.itemId === item.id) {
if (this.isTargeting && this.targetingItem === item.id) { // Already targeting - execute throw
if (this.targetCursor) { if (this.targetingSystem.cursorPos) {
this.executeThrow(this.targetCursor.x, this.targetCursor.y); this.executeThrow(this.targetingSystem.cursorPos.x, this.targetingSystem.cursorPos.y);
} }
return; return;
} }
this.targetingItem = item.id; this.targetingSystem.startTargeting(
this.isTargeting = true; item.id,
// Auto-target closest visible enemy
const closest = getClosestVisibleEnemy(
this.world,
player.pos, player.pos,
this.world,
this.dungeonRenderer.seenArray, this.dungeonRenderer.seenArray,
this.world.width this.world.width
); );
if (closest) {
this.targetCursor = closest;
} else {
// Default to player pos or null?
// If we default to mouse pos, we need current mouse pos.
// Let's default to null and wait for mouse move, OR default to player pos forward?
// Let's just default to null until mouse moves.
this.targetCursor = null;
}
this.drawTargetingLine();
console.log("Targeting Mode: ON");
this.emitUIUpdate(); this.emitUIUpdate();
} }
}); });
// Right Clicks to cancel targeting // Right Clicks to cancel targeting
this.input.on('pointerdown', (p: Phaser.Input.Pointer) => { this.input.on('pointerdown', (p: Phaser.Input.Pointer) => {
if (p.rightButtonDown() && this.isTargeting) { if (p.rightButtonDown() && this.targetingSystem.isActive) {
this.cancelTargeting(); this.targetingSystem.cancel();
this.emitUIUpdate();
} }
}); });
// Zoom Control // Zoom Control
this.input.on( this.input.on("wheel", (_pointer: Phaser.Input.Pointer, _gameObjects: Phaser.GameObjects.GameObject[], _deltaX: number, deltaY: number, _deltaZ: number) => {
"wheel",
(
_pointer: Phaser.Input.Pointer,
_gameObjects: any,
_deltaX: number,
deltaY: number,
_deltaZ: number
) => {
if (this.isMenuOpen || this.isInventoryOpen || this.dungeonRenderer.isMinimapVisible()) return; if (this.isMenuOpen || this.isInventoryOpen || this.dungeonRenderer.isMinimapVisible()) return;
this.cameraController.handleWheel(deltaY);
const zoomDir = deltaY > 0 ? -1 : 1; });
const newZoom = Phaser.Math.Clamp(
this.cameras.main.zoom + zoomDir * GAME_CONFIG.rendering.zoomStep,
GAME_CONFIG.rendering.minZoom,
GAME_CONFIG.rendering.maxZoom
);
this.cameras.main.setZoom(newZoom);
}
);
// Disable context menu for right-click panning // Disable context menu for right-click panning
this.input.mouse?.disableContextMenu(); this.input.mouse?.disableContextMenu();
@@ -260,12 +223,13 @@ export class GameScene extends Phaser.Scene {
// Camera Panning // Camera Panning
this.input.on("pointermove", (p: Phaser.Input.Pointer) => { this.input.on("pointermove", (p: Phaser.Input.Pointer) => {
if (!p.isDown) { // Even if not down, we might need to update targeting line if (!p.isDown) { // Even if not down, we might need to update targeting line
if (this.isTargeting) { if (this.targetingSystem.isActive) {
const tx = Math.floor(p.worldX / TILE_SIZE); const tx = Math.floor(p.worldX / TILE_SIZE);
const ty = Math.floor(p.worldY / TILE_SIZE); const ty = Math.floor(p.worldY / TILE_SIZE);
// Only update if changed to avoid jitter if needed, but simple assignment is fine const player = this.world.actors.get(this.playerId) as CombatantActor;
this.targetCursor = { x: tx, y: ty }; if (player) {
this.drawTargetingLine(); this.targetingSystem.updateCursor({ x: tx, y: ty }, player.pos);
}
} }
return; return;
} }
@@ -283,28 +247,27 @@ export class GameScene extends Phaser.Scene {
const dx = (x - prevX) / this.cameras.main.zoom; const dx = (x - prevX) / this.cameras.main.zoom;
const dy = (y - prevY) / this.cameras.main.zoom; const dy = (y - prevY) / this.cameras.main.zoom;
this.cameras.main.scrollX -= dx; this.cameraController.handlePan(dx, dy);
this.cameras.main.scrollY -= dy;
this.followPlayer = false;
} }
if (this.isTargeting) { if (this.targetingSystem.isActive) {
const tx = Math.floor(p.worldX / TILE_SIZE); const tx = Math.floor(p.worldX / TILE_SIZE);
const ty = Math.floor(p.worldY / TILE_SIZE); const ty = Math.floor(p.worldY / TILE_SIZE);
this.targetCursor = { x: tx, y: ty }; const player = this.world.actors.get(this.playerId) as CombatantActor;
this.drawTargetingLine(); if (player) {
this.targetingSystem.updateCursor({ x: tx, y: ty }, player.pos);
}
} }
}); });
// Mouse click -> // Mouse click ->
this.input.on("pointerdown", (p: Phaser.Input.Pointer) => { this.input.on("pointerdown", (p: Phaser.Input.Pointer) => {
// Targeting Click // Targeting Click
if (this.isTargeting) { if (this.targetingSystem.isActive) {
// Only Left Click throws // Only Left Click throws
if (p.button === 0) { if (p.button === 0) {
if (this.targetCursor) { if (this.targetingSystem.cursorPos) {
this.executeThrow(this.targetCursor.x, this.targetCursor.y); this.executeThrow(this.targetingSystem.cursorPos.x, this.targetingSystem.cursorPos.y);
} }
} }
return; return;
@@ -313,7 +276,7 @@ export class GameScene extends Phaser.Scene {
// Movement Click // Movement Click
if (p.button !== 0) return; if (p.button !== 0) return;
this.followPlayer = true; this.cameraController.enableFollowMode();
if (!this.awaitingPlayer) return; if (!this.awaitingPlayer) return;
if (this.isMenuOpen || this.isInventoryOpen || this.dungeonRenderer.isMinimapVisible()) return; if (this.isMenuOpen || this.isInventoryOpen || this.dungeonRenderer.isMinimapVisible()) return;
@@ -410,8 +373,9 @@ export class GameScene extends Phaser.Scene {
if (this.cursors.down!.isDown) dy += 1; if (this.cursors.down!.isDown) dy += 1;
if (dx !== 0 || dy !== 0) { if (dx !== 0 || dy !== 0) {
if (this.isTargeting) { if (this.targetingSystem.isActive) {
this.cancelTargeting(); this.targetingSystem.cancel();
this.emitUIUpdate();
} }
const player = this.world.actors.get(this.playerId) as CombatantActor; const player = this.world.actors.get(this.playerId) as CombatantActor;
const targetX = player.pos.x + dx; const targetX = player.pos.x + dx;
@@ -443,7 +407,7 @@ export class GameScene extends Phaser.Scene {
playerId: this.playerId, playerId: this.playerId,
floorIndex: this.floorIndex, floorIndex: this.floorIndex,
uiState: { uiState: {
targetingItemId: this.targetingItem targetingItemId: this.targetingSystem.itemId
} }
}; };
this.events.emit("update-ui", payload); this.events.emit("update-ui", payload);
@@ -457,11 +421,15 @@ export class GameScene extends Phaser.Scene {
} }
this.awaitingPlayer = false; this.awaitingPlayer = false;
this.followPlayer = true; this.cameraController.enableFollowMode();
// Check for pickups right after move (before enemy turn, so you get it efficiently) // Check for pickups right after move (before enemy turn, so you get it efficiently)
if (action.type === "move") { if (action.type === "move") {
this.tryPickupItem(); const player = this.world.actors.get(this.playerId) as CombatantActor;
const pickedItem = this.itemManager.tryPickup(player);
if (pickedItem) {
this.emitUIUpdate();
}
} }
const enemyStep = stepUntilPlayerTurn(this.world, this.playerId, this.entityManager); const enemyStep = stepUntilPlayerTurn(this.world, this.playerId, this.entityManager);
@@ -509,8 +477,8 @@ export class GameScene extends Phaser.Scene {
if (!this.world.actors.has(this.playerId)) { if (!this.world.actors.has(this.playerId)) {
this.syncRunStateFromPlayer(); this.syncRunStateFromPlayer();
const uiScene = this.scene.get("GameUI") as any; const uiScene = this.scene.get("GameUI") as GameUI;
if (uiScene) { if (uiScene && 'showDeathScreen' in uiScene) {
uiScene.showDeathScreen({ uiScene.showDeathScreen({
floor: this.floorIndex, floor: this.floorIndex,
gold: this.runState.inventory.gold, gold: this.runState.inventory.gold,
@@ -528,8 +496,9 @@ export class GameScene extends Phaser.Scene {
} }
this.dungeonRenderer.computeFov(this.playerId); this.dungeonRenderer.computeFov(this.playerId);
if (this.followPlayer) { if (this.cameraController.isFollowing) {
this.centerCameraOnPlayer(); const player = this.world.actors.get(this.playerId) as CombatantActor;
this.cameraController.centerOnTile(player.pos.x, player.pos.y);
} }
this.dungeonRenderer.render(this.playerPath); this.dungeonRenderer.render(this.playerPath);
this.emitUIUpdate(); this.emitUIUpdate();
@@ -537,18 +506,19 @@ export class GameScene extends Phaser.Scene {
private loadFloor(floor: number) { private loadFloor(floor: number) {
this.floorIndex = floor; this.floorIndex = floor;
this.followPlayer = true; this.cameraController.enableFollowMode();
const { world, playerId } = generateWorld(floor, this.runState); const { world, playerId } = generateWorld(floor, this.runState);
this.world = world; this.world = world;
this.playerId = playerId; this.playerId = playerId;
this.entityManager = new EntityManager(this.world); this.entityManager = new EntityManager(this.world);
this.itemManager.updateWorld(this.world, this.entityManager);
this.playerPath = []; this.playerPath = [];
this.awaitingPlayer = false; this.awaitingPlayer = false;
this.cameras.main.setBounds(0, 0, this.world.width * TILE_SIZE, this.world.height * TILE_SIZE); this.cameraController.setBounds(0, 0, this.world.width * TILE_SIZE, this.world.height * TILE_SIZE);
this.dungeonRenderer.initializeFloor(this.world, this.playerId); this.dungeonRenderer.initializeFloor(this.world, this.playerId);
@@ -557,7 +527,8 @@ export class GameScene extends Phaser.Scene {
this.dungeonRenderer.computeFov(this.playerId); this.dungeonRenderer.computeFov(this.playerId);
this.centerCameraOnPlayer(); const player = this.world.actors.get(this.playerId) as CombatantActor;
this.cameraController.centerOnTile(player.pos.x, player.pos.y);
this.dungeonRenderer.render(this.playerPath); this.dungeonRenderer.render(this.playerPath);
this.emitUIUpdate(); this.emitUIUpdate();
@@ -584,139 +555,49 @@ export class GameScene extends Phaser.Scene {
this.loadFloor(this.floorIndex); this.loadFloor(this.floorIndex);
} }
private centerCameraOnPlayer() {
const player = this.world.actors.get(this.playerId) as CombatantActor;
this.cameras.main.centerOn(
player.pos.x * TILE_SIZE + TILE_SIZE / 2,
player.pos.y * TILE_SIZE + TILE_SIZE / 2
);
}
private drawTargetingLine() {
if (!this.world || !this.targetCursor) {
this.targetingGraphics.clear();
return;
}
this.targetingGraphics.clear();
const player = this.world.actors.get(this.playerId) as CombatantActor;
if (!player) return;
const startX = player.pos.x * TILE_SIZE + TILE_SIZE / 2; private executeThrow(_targetX: number, _targetY: number) {
const startY = player.pos.y * TILE_SIZE + TILE_SIZE / 2; const success = this.targetingSystem.executeThrow(
this.world,
const endX = this.targetCursor.x * TILE_SIZE + TILE_SIZE / 2; this.playerId,
const endY = this.targetCursor.y * TILE_SIZE + TILE_SIZE / 2; this.entityManager,
(blockedPos, hitActorId, item) => {
this.targetingGraphics.lineStyle(2, 0xff0000, 0.7);
this.targetingGraphics.lineBetween(startX, startY, endX, endY);
this.targetingGraphics.strokeRect(this.targetCursor.x * TILE_SIZE, this.targetCursor.y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
}
private cancelTargeting() {
this.isTargeting = false;
this.targetingItem = null;
this.targetCursor = null;
this.targetingGraphics.clear();
console.log("Targeting cancelled");
this.emitUIUpdate();
}
private executeThrow(targetX: number, targetY: number) {
const player = this.world.actors.get(this.playerId) as CombatantActor;
if (!player) return;
const itemArg = this.targetingItem;
if (!itemArg) return;
const itemIdx = player.inventory!.items.findIndex(it => it.id === itemArg);
if (itemIdx === -1) {
console.log("Item not found!");
this.cancelTargeting();
return;
}
const item = player.inventory!.items[itemIdx];
player.inventory!.items.splice(itemIdx, 1);
const start = player.pos;
const end = { x: targetX, y: targetY };
const result = traceProjectile(this.world, start, end, this.entityManager, this.playerId);
const { blockedPos, hitActorId } = result;
this.dungeonRenderer.showProjectile(
start,
blockedPos,
item.id,
() => {
if (hitActorId !== undefined) { if (hitActorId !== undefined) {
const victim = this.world.actors.get(hitActorId) as CombatantActor; const victim = this.world.actors.get(hitActorId) as CombatantActor;
if (victim) { if (victim) {
const dmg = item.stats?.attack ?? 1; // Use item stats const dmg = item.stats?.attack ?? 1;
victim.stats.hp -= dmg; victim.stats.hp -= dmg;
this.dungeonRenderer.showDamage(victim.pos.x, victim.pos.y, dmg); this.dungeonRenderer.showDamage(victim.pos.x, victim.pos.y, dmg);
this.dungeonRenderer.shakeCamera(); this.dungeonRenderer.shakeCamera();
if (victim.stats.hp <= 0) {
// Force kill handled by simulation
}
} }
} }
// Drop the actual item at the landing spot const player = this.world.actors.get(this.playerId) as CombatantActor;
this.spawnItem(item, blockedPos.x, blockedPos.y); this.dungeonRenderer.showProjectile(
player.pos,
blockedPos,
item.id,
() => {
// Drop the actual item at the landing spot
this.itemManager.spawnItem(item, blockedPos);
// "Count as walking over the tile" -> Trigger destruction/interaction // Trigger destruction/interaction
// e.g. breaking grass, opening items if (tryDestructTile(this.world, blockedPos.x, blockedPos.y)) {
if (tryDestructTile(this.world, blockedPos.x, blockedPos.y)) { this.dungeonRenderer.updateTile(blockedPos.x, blockedPos.y);
this.dungeonRenderer.updateTile(blockedPos.x, blockedPos.y); }
}
this.cancelTargeting(); this.targetingSystem.cancel();
this.commitPlayerAction({ type: "throw" }); this.commitPlayerAction({ type: "throw" });
this.emitUIUpdate(); this.emitUIUpdate();
}
);
} }
); );
}
private spawnItem(item: Item, x: number, y: number) { if (!success) {
if (!this.world || !this.entityManager) return;
const id = this.entityManager.getNextId();
const drop: ItemDropActor = {
id,
pos: { x, y },
category: "item_drop",
item: { ...item } // Clone item
};
this.entityManager.addActor(drop);
// Ensure renderer knows? Renderer iterates world.actors, so it should pick it up if we handle "item_drop"
}
private tryPickupItem() {
const player = this.world.actors.get(this.playerId) as CombatantActor;
if (!player) return;
const actors = this.entityManager.getActorsAt(player.pos.x, player.pos.y);
const itemActor = actors.find(a => (a as any).category === "item_drop"); // Safe check
if (itemActor) {
const drop = itemActor as any; // Cast to ItemDropActor
const item = drop.item;
// Add to inventory
player.inventory!.items.push(item);
// Remove from world
this.entityManager.removeActor(drop.id);
console.log("Picked up:", item.name);
// Show FX?
// this.dungeonRenderer.showPickup(player.pos.x, player.pos.y); -> need to implement
this.emitUIUpdate(); this.emitUIUpdate();
} }
} }

View File

@@ -0,0 +1,85 @@
import Phaser from "phaser";
import { TILE_SIZE } from "../../core/constants";
import { GAME_CONFIG } from "../../core/config/GameConfig";
/**
* Manages camera controls including zoom, panning, and follow mode.
* Extracted from GameScene to reduce complexity and improve testability.
*/
export class CameraController {
private camera: Phaser.Cameras.Scene2D.Camera;
private followMode: boolean = true;
constructor(camera: Phaser.Cameras.Scene2D.Camera) {
this.camera = camera;
}
/**
* Enable follow mode - camera will track the target entity
*/
enableFollowMode(): void {
this.followMode = true;
}
/**
* Disable follow mode - camera stays at current position
*/
disableFollowMode(): void {
this.followMode = false;
}
/**
* Check if camera is in follow mode
*/
get isFollowing(): boolean {
return this.followMode;
}
/**
* Center camera on a specific world position (in pixels)
*/
centerOn(worldX: number, worldY: number): void {
this.camera.centerOn(worldX, worldY);
}
/**
* Center camera on a tile position
*/
centerOnTile(tileX: number, tileY: number): void {
const worldX = tileX * TILE_SIZE + TILE_SIZE / 2;
const worldY = tileY * TILE_SIZE + TILE_SIZE / 2;
this.camera.centerOn(worldX, worldY);
}
/**
* Handle mouse wheel zoom
* @param deltaY - Wheel delta (positive = zoom out, negative = zoom in)
*/
handleWheel(deltaY: number): void {
const zoomDir = deltaY > 0 ? -1 : 1;
const newZoom = Phaser.Math.Clamp(
this.camera.zoom + zoomDir * GAME_CONFIG.rendering.zoomStep,
GAME_CONFIG.rendering.minZoom,
GAME_CONFIG.rendering.maxZoom
);
this.camera.setZoom(newZoom);
}
/**
* Handle camera panning via drag
* @param dx - Change in x position
* @param dy - Change in y position
*/
handlePan(dx: number, dy: number): void {
this.camera.scrollX -= dx;
this.camera.scrollY -= dy;
this.disableFollowMode();
}
/**
* Set camera bounds (usually to match world size)
*/
setBounds(x: number, y: number, width: number, height: number): void {
this.camera.setBounds(x, y, width, height);
}
}

View File

@@ -0,0 +1,151 @@
import type { GameScene } from "../GameScene";
import type { DungeonRenderer } from "../../rendering/DungeonRenderer";
import type { CombatantActor } from "../../core/types";
import type { ProgressionManager } from "../../engine/ProgressionManager";
import type { ItemManager } from "./ItemManager";
import type { TargetingSystem } from "./TargetingSystem";
/**
* Centralizes all event handling between GameScene and UI.
* Extracted from GameScene to reduce complexity and make event flow clearer.
*/
export class EventBridge {
private scene: GameScene;
constructor(scene: GameScene) {
this.scene = scene;
}
/**
* Set up all event listeners
*/
setupListeners(
dungeonRenderer: DungeonRenderer,
progressionManager: ProgressionManager,
itemManager: ItemManager,
targetingSystem: TargetingSystem,
awaitingPlayerFn: () => boolean,
commitActionFn: (action: any) => void,
emitUIUpdateFn: () => void,
restartGameFn: () => void,
executeThrowFn: (x: number, y: number) => void
): void {
// Menu state listeners (from UI)
this.scene.events.on("menu-toggled", (isOpen: boolean) => {
(this.scene as any).isMenuOpen = isOpen;
});
this.scene.events.on("inventory-toggled", (isOpen: boolean) => {
(this.scene as any).isInventoryOpen = isOpen;
});
this.scene.events.on("character-toggled", (isOpen: boolean) => {
(this.scene as any).isCharacterOpen = isOpen;
});
// Minimap toggle
this.scene.events.on("toggle-minimap", () => {
dungeonRenderer.toggleMinimap();
});
// UI update requests
this.scene.events.on("request-ui-update", () => {
emitUIUpdateFn();
});
// Game restart
this.scene.events.on("restart-game", () => {
restartGameFn();
});
// Stat allocation
this.scene.events.on("allocate-stat", (statName: string) => {
const player = (this.scene as any).world.actors.get((this.scene as any).playerId) as CombatantActor;
if (player) {
progressionManager.allocateStat(player, statName);
emitUIUpdateFn();
}
});
// Passive allocation
this.scene.events.on("allocate-passive", (nodeId: string) => {
const player = (this.scene as any).world.actors.get((this.scene as any).playerId) as CombatantActor;
if (player) {
progressionManager.allocatePassive(player, nodeId);
emitUIUpdateFn();
}
});
// Player wait action
this.scene.events.on("player-wait", () => {
if (!awaitingPlayerFn()) return;
if ((this.scene as any).isMenuOpen || (this.scene as any).isInventoryOpen || dungeonRenderer.isMinimapVisible()) return;
commitActionFn({ type: "wait" });
});
// Player search action
this.scene.events.on("player-search", () => {
if (!awaitingPlayerFn()) return;
if ((this.scene as any).isMenuOpen || (this.scene as any).isInventoryOpen || dungeonRenderer.isMinimapVisible()) return;
console.log("Player searching...");
commitActionFn({ type: "wait" });
});
// Item use
this.scene.events.on("use-item", (data: { itemId: string }) => {
if (!awaitingPlayerFn()) return;
const player = (this.scene as any).world.actors.get((this.scene as any).playerId) as CombatantActor;
if (!player || !player.inventory) return;
const itemIdx = player.inventory.items.findIndex(it => it.id === data.itemId);
if (itemIdx === -1) return;
const item = player.inventory.items[itemIdx];
const result = itemManager.handleUse(data.itemId, player);
if (result.success && result.consumed) {
const healAmount = player.stats.maxHp - player.stats.hp;
const actualHeal = Math.min(healAmount, player.stats.hp);
dungeonRenderer.showHeal(player.pos.x, player.pos.y, actualHeal);
commitActionFn({ type: "wait" });
emitUIUpdateFn();
} else if (result.success && !result.consumed) {
// Throwable item - start targeting
if (targetingSystem.isActive && targetingSystem.itemId === item.id) {
// Already targeting - execute throw
if (targetingSystem.cursorPos) {
executeThrowFn(targetingSystem.cursorPos.x, targetingSystem.cursorPos.y);
}
return;
}
targetingSystem.startTargeting(
item.id,
player.pos,
(this.scene as any).world,
dungeonRenderer.seenArray,
(this.scene as any).world.width
);
emitUIUpdateFn();
}
});
}
/**
* Clean up all event listeners (call on scene shutdown)
*/
cleanup(): void {
this.scene.events.removeAllListeners("menu-toggled");
this.scene.events.removeAllListeners("inventory-toggled");
this.scene.events.removeAllListeners("character-toggled");
this.scene.events.removeAllListeners("toggle-minimap");
this.scene.events.removeAllListeners("request-ui-update");
this.scene.events.removeAllListeners("restart-game");
this.scene.events.removeAllListeners("allocate-stat");
this.scene.events.removeAllListeners("allocate-passive");
this.scene.events.removeAllListeners("player-wait");
this.scene.events.removeAllListeners("player-search");
this.scene.events.removeAllListeners("use-item");
}
}

View File

@@ -0,0 +1,151 @@
import type { World, CombatantActor, Item, ItemDropActor, Vec2 } from "../../core/types";
import { EntityManager } from "../../engine/EntityManager";
/**
* Result of attempting to use an item
*/
export interface ItemUseResult {
success: boolean;
consumed: boolean;
message?: string;
}
/**
* Manages item-related operations including spawning, pickup, and usage.
* Extracted from GameScene to centralize item logic and reduce complexity.
*/
export class ItemManager {
private world: World;
private entityManager: EntityManager;
constructor(world: World, entityManager: EntityManager) {
this.world = world;
this.entityManager = entityManager;
}
/**
* Update references when world changes (e.g., new floor)
*/
updateWorld(world: World, entityManager: EntityManager): void {
this.world = world;
this.entityManager = entityManager;
}
/**
* Spawn an item drop at the specified position
*/
spawnItem(item: Item, pos: Vec2): void {
if (!this.world || !this.entityManager) return;
const id = this.entityManager.getNextId();
const drop: ItemDropActor = {
id,
pos: { x: pos.x, y: pos.y },
category: "item_drop",
item: { ...item } // Clone item
};
this.entityManager.addActor(drop);
}
/**
* Try to pickup an item at the player's position
* @returns The picked up item, or null if nothing to pick up
*/
tryPickup(player: CombatantActor): Item | null {
if (!player || !player.inventory) return null;
const actors = this.entityManager.getActorsAt(player.pos.x, player.pos.y);
const itemActor = actors.find((a): a is ItemDropActor => a.category === "item_drop");
if (itemActor) {
const item = itemActor.item;
// Add to inventory
player.inventory.items.push(item);
// Remove from world
this.entityManager.removeActor(itemActor.id);
console.log("Picked up:", item.name);
return item;
}
return null;
}
/**
* Handle using an item from inventory
* Returns information about what happened
*/
handleUse(itemId: string, player: CombatantActor): ItemUseResult {
if (!player || !player.inventory) {
return { success: false, consumed: false, message: "Invalid player state" };
}
const itemIdx = player.inventory.items.findIndex(it => it.id === itemId);
if (itemIdx === -1) {
return { success: false, consumed: false, message: "Item not found" };
}
const item = player.inventory.items[itemIdx];
// Check if item is a healing consumable
if (item.stats && item.stats.hp && item.stats.hp > 0) {
const healAmount = item.stats.hp;
if (player.stats.hp >= player.stats.maxHp) {
return { success: false, consumed: false, message: "Already at full health" };
}
player.stats.hp = Math.min(player.stats.hp + healAmount, player.stats.maxHp);
// Remove item after use
player.inventory.items.splice(itemIdx, 1);
return {
success: true,
consumed: true,
message: `Healed for ${healAmount} HP`
};
}
// Throwable items are handled by TargetingSystem, not here
if (item.throwable) {
return {
success: true,
consumed: false,
message: "Throwable item - use targeting"
};
}
return {
success: false,
consumed: false,
message: "Item has no effect"
};
}
/**
* Remove an item from player inventory by ID
*/
removeFromInventory(player: CombatantActor, itemId: string): boolean {
if (!player || !player.inventory) return false;
const itemIdx = player.inventory.items.findIndex(it => it.id === itemId);
if (itemIdx === -1) return false;
player.inventory.items.splice(itemIdx, 1);
return true;
}
/**
* Get an item from player inventory by ID
*/
getItem(player: CombatantActor, itemId: string): Item | null {
if (!player || !player.inventory) return null;
const item = player.inventory.items.find(it => it.id === itemId);
return item || null;
}
}

View File

@@ -0,0 +1,160 @@
import Phaser from "phaser";
import type { World, CombatantActor, Item, Vec2, EntityId } from "../../core/types";
import { TILE_SIZE } from "../../core/constants";
import { GAME_CONFIG } from "../../core/config/GameConfig";
import { traceProjectile, getClosestVisibleEnemy } from "../../engine/gameplay/CombatLogic";
import type { EntityManager } from "../../engine/EntityManager";
/**
* Manages targeting mode for thrown items.
* Extracted from GameScene to isolate targeting logic and reduce complexity.
*/
export class TargetingSystem {
private graphics: Phaser.GameObjects.Graphics;
private active: boolean = false;
private targetingItemId: string | null = null;
private cursor: Vec2 | null = null;
constructor(graphics: Phaser.GameObjects.Graphics) {
this.graphics = graphics;
}
/**
* Start targeting mode for a throwable item
*/
startTargeting(
itemId: string,
playerPos: Vec2,
world: World,
seenArray: Uint8Array,
worldWidth: number
): void {
this.targetingItemId = itemId;
this.active = true;
// Auto-target closest visible enemy
const closest = getClosestVisibleEnemy(world, playerPos, seenArray, worldWidth);
if (closest) {
this.cursor = closest;
} else {
this.cursor = null;
}
this.drawLine(playerPos);
console.log("Targeting Mode: ON");
}
/**
* Update the targeting cursor position
*/
updateCursor(worldPos: Vec2, playerPos: Vec2): void {
if (!this.active) return;
this.cursor = { x: worldPos.x, y: worldPos.y };
this.drawLine(playerPos);
}
/**
* Execute the throw action
*/
executeThrow(
world: World,
playerId: EntityId,
entityManager: EntityManager,
onProjectileComplete: (blockedPos: Vec2, hitActorId: EntityId | undefined, item: Item) => void
): boolean {
if (!this.active || !this.targetingItemId || !this.cursor) {
return false;
}
const player = world.actors.get(playerId) as CombatantActor;
if (!player || !player.inventory) return false;
const itemIdx = player.inventory.items.findIndex(it => it.id === this.targetingItemId);
if (itemIdx === -1) {
console.log("Item not found!");
this.cancel();
return false;
}
const item = player.inventory.items[itemIdx];
// Remove item from inventory before throw
player.inventory.items.splice(itemIdx, 1);
const start = player.pos;
const end = { x: this.cursor.x, y: this.cursor.y };
const result = traceProjectile(world, start, end, entityManager, playerId);
const { blockedPos, hitActorId } = result;
// Call the callback with throw results
onProjectileComplete(blockedPos, hitActorId, item);
return true;
}
/**
* Cancel targeting mode
*/
cancel(): void {
this.active = false;
this.targetingItemId = null;
this.cursor = null;
this.graphics.clear();
console.log("Targeting cancelled");
}
/**
* Check if targeting is currently active
*/
get isActive(): boolean {
return this.active;
}
/**
* Get the ID of the item being targeted
*/
get itemId(): string | null {
return this.targetingItemId;
}
/**
* Get the current cursor position
*/
get cursorPos(): Vec2 | null {
return this.cursor;
}
/**
* Draw targeting line from player to cursor
*/
private drawLine(playerPos: Vec2): void {
if (!this.cursor) {
this.graphics.clear();
return;
}
this.graphics.clear();
const startX = playerPos.x * TILE_SIZE + TILE_SIZE / 2;
const startY = playerPos.y * TILE_SIZE + TILE_SIZE / 2;
const endX = this.cursor.x * TILE_SIZE + TILE_SIZE / 2;
const endY = this.cursor.y * TILE_SIZE + TILE_SIZE / 2;
this.graphics.lineStyle(
GAME_CONFIG.ui.targetingLineWidth,
GAME_CONFIG.ui.targetingLineColor,
GAME_CONFIG.ui.targetingLineAlpha
);
this.graphics.lineBetween(startX, startY, endX, endY);
this.graphics.strokeRect(
this.cursor.x * TILE_SIZE,
this.cursor.y * TILE_SIZE,
TILE_SIZE,
TILE_SIZE
);
}
}

View File

@@ -5,8 +5,8 @@ import { MenuComponent } from "./components/MenuComponent";
import { InventoryOverlay } from "./components/InventoryOverlay"; import { InventoryOverlay } from "./components/InventoryOverlay";
import { CharacterOverlay } from "./components/CharacterOverlay"; import { CharacterOverlay } from "./components/CharacterOverlay";
import { DeathOverlay } from "./components/DeathOverlay"; import { DeathOverlay } from "./components/DeathOverlay";
import { PersistentButtonsComponent } from "./components/PersistentButtonsComponent";
import { QuickSlotComponent } from "./components/QuickSlotComponent"; import { QuickSlotComponent } from "./components/QuickSlotComponent";
import { ActionButtonComponent } from "./components/ActionButtonComponent";
export default class GameUI extends Phaser.Scene { export default class GameUI extends Phaser.Scene {
private hud: HudComponent; private hud: HudComponent;
@@ -14,8 +14,8 @@ export default class GameUI extends Phaser.Scene {
private inventory: InventoryOverlay; private inventory: InventoryOverlay;
private character: CharacterOverlay; private character: CharacterOverlay;
private death: DeathOverlay; private death: DeathOverlay;
private persistentButtons: PersistentButtonsComponent;
private quickSlots: QuickSlotComponent; private quickSlots: QuickSlotComponent;
private actionButtons: ActionButtonComponent;
constructor() { constructor() {
super({ key: "GameUI" }); super({ key: "GameUI" });
@@ -24,8 +24,8 @@ export default class GameUI extends Phaser.Scene {
this.inventory = new InventoryOverlay(this); this.inventory = new InventoryOverlay(this);
this.character = new CharacterOverlay(this); this.character = new CharacterOverlay(this);
this.death = new DeathOverlay(this); this.death = new DeathOverlay(this);
this.persistentButtons = new PersistentButtonsComponent(this);
this.quickSlots = new QuickSlotComponent(this); this.quickSlots = new QuickSlotComponent(this);
this.actionButtons = new ActionButtonComponent(this);
} }
@@ -35,8 +35,8 @@ export default class GameUI extends Phaser.Scene {
this.inventory.create(); this.inventory.create();
this.character.create(); this.character.create();
this.death.create(); this.death.create();
this.persistentButtons.create();
this.quickSlots.create(); this.quickSlots.create();
this.actionButtons.create();
const gameScene = this.scene.get("GameScene"); const gameScene = this.scene.get("GameScene");

View File

@@ -0,0 +1,99 @@
import Phaser from "phaser";
export class ActionButtonComponent {
private scene: Phaser.Scene;
private container!: Phaser.GameObjects.Container;
private iconsLoaded = false;
private static readonly ACTIONS = [
{ icon: "icon_wait", label: "WAIT", event: "player-wait" },
{ icon: "icon_search", label: "SEARCH", event: "player-search" },
{ icon: "icon_map", label: "MAP", event: "toggle-minimap" },
{ icon: "icon_backpack", label: "BACKPACK", event: "toggle-inventory" },
{ icon: "icon_stats", label: "STATS", event: "toggle-character" },
{ icon: "icon_menu", label: "MENU", event: "toggle-menu" }
];
private static readonly BUTTON_SIZE = 40;
private static readonly BUTTON_SPACING = 4;
private static readonly LABEL_HEIGHT = 15;
private static readonly ICON_SCALE_NORMAL = 1.5;
private static readonly ICON_SCALE_HOVER = 1.7;
private static readonly COLOR_BG = 0x2a1f3d;
private static readonly COLOR_BORDER_NORMAL = 0xD4AF37;
private static readonly COLOR_BORDER_HOVER = 0xFFD700;
constructor(scene: Phaser.Scene) {
this.scene = scene;
}
preload() {
const iconPath = "/assets/ui/icons/icon_";
["wait", "search", "menu", "backpack", "stats", "map"].forEach(name => {
this.scene.load.image(`icon_${name}`, `${iconPath}${name}.png`);
});
this.scene.load.once("complete", () => { this.iconsLoaded = true; });
this.scene.load.start();
}
create() {
if (!this.iconsLoaded) {
this.preload();
this.scene.time.delayedCall(100, () => this.createButtons());
} else {
this.createButtons();
}
}
private createButtons() {
const { width, height } = this.scene.scale;
const { ACTIONS, BUTTON_SIZE, BUTTON_SPACING, LABEL_HEIGHT } = ActionButtonComponent;
const totalWidth = (BUTTON_SIZE + BUTTON_SPACING) * ACTIONS.length - BUTTON_SPACING;
this.container = this.scene.add.container(
width / 2 - totalWidth / 2,
height - BUTTON_SIZE - LABEL_HEIGHT - 10
);
this.container.setScrollFactor(0).setDepth(1500);
ACTIONS.forEach((action, i) => {
const x = i * (BUTTON_SIZE + BUTTON_SPACING);
const graphics = this.scene.add.graphics();
const icon = this.scene.add.sprite(BUTTON_SIZE / 2, BUTTON_SIZE / 2, action.icon).setScale(ActionButtonComponent.ICON_SCALE_NORMAL);
const label = this.scene.add.text(BUTTON_SIZE / 2, BUTTON_SIZE + 3, action.label, {
fontSize: "9px",
color: "#D4AF37",
fontStyle: "bold"
}).setOrigin(0.5, 0);
const button = this.scene.add.container(x, 0, [graphics, icon, label]);
this.container.add(button);
this.drawButton(graphics, false);
button.setInteractive(new Phaser.Geom.Rectangle(0, 0, BUTTON_SIZE, BUTTON_SIZE), Phaser.Geom.Rectangle.Contains);
button.on("pointerover", () => {
this.drawButton(graphics, true);
icon.setScale(ActionButtonComponent.ICON_SCALE_HOVER);
});
button.on("pointerout", () => {
this.drawButton(graphics, false);
icon.setScale(ActionButtonComponent.ICON_SCALE_NORMAL);
});
button.on("pointerdown", () => {
this.scene.scene.get("GameScene").events.emit(action.event);
});
});
}
private drawButton(graphics: Phaser.GameObjects.Graphics, hover: boolean) {
const { BUTTON_SIZE, COLOR_BG, COLOR_BORDER_NORMAL, COLOR_BORDER_HOVER } = ActionButtonComponent;
graphics.clear();
graphics.fillStyle(COLOR_BG, 0.95);
graphics.fillRect(0, 0, BUTTON_SIZE, BUTTON_SIZE);
graphics.lineStyle(2, hover ? COLOR_BORDER_HOVER : COLOR_BORDER_NORMAL, 1);
graphics.strokeRect(0, 0, BUTTON_SIZE, BUTTON_SIZE);
}
}

View File

@@ -1,10 +1,9 @@
import Phaser from "phaser"; import Phaser from "phaser";
import { type Stats } from "../../core/types"; import { type Stats } from "../../core/types";
import { GAME_CONFIG } from "../../core/config/GameConfig";
export class HudComponent { export class HudComponent {
private scene: Phaser.Scene; private scene: Phaser.Scene;
private floorText!: Phaser.GameObjects.Text; private container!: Phaser.GameObjects.Container;
private healthBar!: Phaser.GameObjects.Graphics; private healthBar!: Phaser.GameObjects.Graphics;
private manaBar!: Phaser.GameObjects.Graphics; private manaBar!: Phaser.GameObjects.Graphics;
private expBar!: Phaser.GameObjects.Graphics; private expBar!: Phaser.GameObjects.Graphics;
@@ -14,63 +13,104 @@ export class HudComponent {
} }
create() { create() {
this.floorText = this.scene.add.text(20, 20, "Floor: 1", { // Create container for the HUD panel
fontSize: "24px", this.container = this.scene.add.container(20, 20);
color: "#ffffff", this.container.setScrollFactor(0).setDepth(1500);
fontStyle: "bold",
stroke: "#000000",
strokeThickness: 4
}).setScrollFactor(0).setDepth(1000);
// Health Bar const panelWidth = 360;
this.scene.add.text(20, 55, "HP", { fontSize: "14px", color: "#ff8888", fontStyle: "bold" }).setScrollFactor(0).setDepth(1000); const panelHeight = 70;
this.healthBar = this.scene.add.graphics().setScrollFactor(0).setDepth(1000);
// Mana Bar // Draw background panel
this.scene.add.text(20, 75, "MP", { fontSize: "14px", color: "#88ccff", fontStyle: "bold" }).setScrollFactor(0).setDepth(1000); const bg = this.scene.add.graphics();
this.manaBar = this.scene.add.graphics().setScrollFactor(0).setDepth(1000); bg.fillStyle(0x2a1f3d, 0.95);
bg.fillRect(0, 0, panelWidth, panelHeight);
// EXP Bar // Gold border (outer)
this.scene.add.text(20, 95, "EXP", { fontSize: "14px", color: "#8888ff", fontStyle: "bold" }).setScrollFactor(0).setDepth(1000); bg.lineStyle(3, 0xD4AF37, 1);
this.expBar = this.scene.add.graphics().setScrollFactor(0).setDepth(1000); bg.strokeRect(0, 0, panelWidth, panelHeight);
// Inner gold border accent
bg.lineStyle(2, 0xB8860B, 0.6);
bg.strokeRect(4, 4, panelWidth - 8, panelHeight - 8);
this.container.add(bg);
// Heart icon (HP) - red pixel heart
const heartIcon = this.scene.add.graphics();
heartIcon.fillStyle(0xFF3333, 1);
// Simple pixel heart shape (smaller)
heartIcon.fillRect(14, 14, 6, 6);
heartIcon.fillRect(20, 14, 6, 6);
heartIcon.fillRect(11, 18, 18, 8);
heartIcon.fillRect(14, 26, 12, 3);
this.container.add(heartIcon);
// Water drop icon (Mana) - blue pixel drop
const manaIcon = this.scene.add.graphics();
manaIcon.fillStyle(0x3399FF, 1);
// Simple pixel water drop
manaIcon.fillRect(17, 34, 6, 4);
manaIcon.fillRect(14, 38, 12, 8);
this.container.add(manaIcon);
// Star icon (EXP) - cyan pixel star (smaller)
const starIcon = this.scene.add.graphics();
starIcon.fillStyle(0xFFD700, 1);
// Simple pixel star shape
starIcon.fillRect(17, 52, 6, 3);
starIcon.fillRect(14, 55, 12, 6);
starIcon.fillRect(17, 61, 6, 2);
this.container.add(starIcon);
// Create bar graphics (will be updated each frame)
this.healthBar = this.scene.add.graphics();
this.manaBar = this.scene.add.graphics();
this.expBar = this.scene.add.graphics();
this.container.add(this.healthBar);
this.container.add(this.manaBar);
this.container.add(this.expBar);
} }
update(stats: Stats, floorIndex: number) { update(stats: Stats, _floorIndex: number) {
this.floorText.setText(`Floor: ${floorIndex}`); const barX = 40;
const barWidth = 305;
const barHeight = 14;
const barSpacing = 4;
// Update Health Bar // Update Health Bar
this.healthBar.clear(); this.healthBar.clear();
this.healthBar.fillStyle(0x333333, 0.8); this.healthBar.fillStyle(0x1a1a1a, 0.9);
this.healthBar.fillRect(60, 58, 200, 12); this.healthBar.fillRect(barX, 14, barWidth, barHeight);
const healthPercent = Phaser.Math.Clamp(stats.hp / stats.maxHp, 0, 1); const healthPercent = Phaser.Math.Clamp(stats.hp / stats.maxHp, 0, 1);
const healthColor = healthPercent > 0.5 ? 0x33ff33 : (healthPercent > 0.2 ? 0xffff33 : 0xff3333); this.healthBar.fillStyle(0xFF3333, 1);
this.healthBar.fillRect(barX, 14, barWidth * healthPercent, barHeight);
this.healthBar.fillStyle(healthColor, 1); this.healthBar.lineStyle(2, 0xD4AF37, 1);
this.healthBar.fillRect(60, 58, 200 * healthPercent, 12); this.healthBar.strokeRect(barX, 14, barWidth, barHeight);
this.healthBar.lineStyle(2, 0xffffff, 0.5);
this.healthBar.strokeRect(60, 58, 200, 12);
// Update Mana Bar // Update Mana Bar
this.manaBar.clear(); this.manaBar.clear();
this.manaBar.fillStyle(0x333333, 0.8); this.manaBar.fillStyle(0x1a1a1a, 0.9);
this.manaBar.fillRect(60, 78, 200, 12); this.manaBar.fillRect(barX, 14 + barHeight + barSpacing, barWidth, barHeight);
const manaPercent = Phaser.Math.Clamp(stats.mana / stats.maxMana, 0, 1); const manaPercent = Phaser.Math.Clamp(stats.mana / stats.maxMana, 0, 1);
this.manaBar.fillStyle(0x3399ff, 1); this.manaBar.fillStyle(0x3399FF, 1);
this.manaBar.fillRect(60, 78, 200 * manaPercent, 12); this.manaBar.fillRect(barX, 14 + barHeight + barSpacing, barWidth * manaPercent, barHeight);
this.manaBar.lineStyle(2, 0xffffff, 0.5);
this.manaBar.strokeRect(60, 78, 200, 12); this.manaBar.lineStyle(2, 0xD4AF37, 1);
this.manaBar.strokeRect(barX, 14 + barHeight + barSpacing, barWidth, barHeight);
// Update EXP Bar // Update EXP Bar
this.expBar.clear(); this.expBar.clear();
this.expBar.fillStyle(0x333333, 0.8); this.expBar.fillStyle(0x1a1a1a, 0.9);
this.expBar.fillRect(60, 98, 200, 8); this.expBar.fillRect(barX, 14 + (barHeight + barSpacing) * 2, barWidth, barHeight);
const expPercent = Phaser.Math.Clamp(stats.exp / stats.expToNextLevel, 0, 1); const expPercent = Phaser.Math.Clamp(stats.exp / stats.expToNextLevel, 0, 1);
this.expBar.fillStyle(GAME_CONFIG.rendering.expOrbColor, 1); this.expBar.fillStyle(0xFFD700, 1);
this.expBar.fillRect(60, 98, 200 * expPercent, 8); this.expBar.fillRect(barX, 14 + (barHeight + barSpacing) * 2, barWidth * expPercent, barHeight);
this.expBar.lineStyle(1, 0xffffff, 0.3);
this.expBar.strokeRect(60, 98, 200, 8); this.expBar.lineStyle(2, 0xD4AF37, 1);
this.expBar.strokeRect(barX, 14 + (barHeight + barSpacing) * 2, barWidth, barHeight);
} }
} }

View File

@@ -1,95 +0,0 @@
import Phaser from "phaser";
export class PersistentButtonsComponent {
private scene: Phaser.Scene;
private container!: Phaser.GameObjects.Container;
constructor(scene: Phaser.Scene) {
this.scene = scene;
}
create() {
const { height } = this.scene.scale;
this.container = this.scene.add.container(20, height - 20);
this.container.setScrollFactor(0).setDepth(1500);
const btnStyle = {
fontSize: "14px",
color: "#ffffff",
backgroundColor: "#1a1a1a",
padding: { x: 10, y: 6 },
fontStyle: "bold"
};
const createBtn = (x: number, text: string, event: string) => {
const btn = this.scene.add.text(x, 0, text, btnStyle)
.setOrigin(0, 1)
.setInteractive({ useHandCursor: true });
btn.on("pointerover", () => btn.setBackgroundColor("#333333"));
btn.on("pointerout", () => btn.setBackgroundColor("#1a1a1a"));
btn.on("pointerdown", () => {
btn.setBackgroundColor("#444444");
const gameScene = this.scene.scene.get("GameScene");
gameScene.events.emit(event);
});
btn.on("pointerup", () => btn.setBackgroundColor("#333333"));
this.container.add(btn);
return btn;
};
createBtn(0, "MENU (ESC)", "toggle-menu");
createBtn(105, "STATS (C)", "toggle-character");
createBtn(200, "BACKPACK (I)", "toggle-inventory");
createBtn(320, "MAP (M)", "toggle-minimap");
// Right-aligned buttons
const rightContainer = this.scene.add.container(this.scene.scale.width - 20, height - 20);
rightContainer.setScrollFactor(0).setDepth(1500);
const waitBtn = this.scene.add.text(0, 0, "🕒", {
fontSize: "24px",
color: "#ffffff",
backgroundColor: "#1a1a1a",
padding: { x: 10, y: 6 },
fontStyle: "bold"
})
.setOrigin(1, 1)
.setInteractive({ useHandCursor: true });
const searchBtn = this.scene.add.text(-40, 0, "🔍", { // Offset to the left of wait button
fontSize: "24px",
color: "#ffffff",
backgroundColor: "#1a1a1a",
padding: { x: 10, y: 6 },
fontStyle: "bold"
})
.setOrigin(1, 1)
.setInteractive({ useHandCursor: true });
waitBtn.on("pointerover", () => waitBtn.setBackgroundColor("#333333"));
waitBtn.on("pointerout", () => waitBtn.setBackgroundColor("#1a1a1a"));
waitBtn.on("pointerdown", () => {
waitBtn.setBackgroundColor("#444444");
const gameScene = this.scene.scene.get("GameScene");
gameScene.events.emit("player-wait");
});
waitBtn.on("pointerup", () => waitBtn.setBackgroundColor("#333333"));
searchBtn.on("pointerover", () => searchBtn.setBackgroundColor("#333333"));
searchBtn.on("pointerout", () => searchBtn.setBackgroundColor("#1a1a1a"));
searchBtn.on("pointerdown", () => {
searchBtn.setBackgroundColor("#444444");
// Implementing search visual logic later, for now just log
console.log("Searching...");
const gameScene = this.scene.scene.get("GameScene");
gameScene.events.emit("player-search");
});
searchBtn.on("pointerup", () => searchBtn.setBackgroundColor("#333333"));
rightContainer.add(waitBtn);
rightContainer.add(searchBtn);
}
}

View File

@@ -14,32 +14,43 @@ export class QuickSlotComponent {
create() { create() {
const { width, height } = this.scene.scale; const { width, height } = this.scene.scale;
// Position bottom center-ish const slotSize = 48;
this.container = this.scene.add.container(width / 2 - 100, height - 50); const slotSpacing = 4;
const totalWidth = (slotSize + slotSpacing) * 4 - slotSpacing;
const actionButtonHeight = 40 + 10; // Button height + spacing
// Position above action buttons
this.container = this.scene.add.container(
width / 2 - totalWidth / 2,
height - slotSize - actionButtonHeight - 20
);
this.container.setScrollFactor(0).setDepth(1500); this.container.setScrollFactor(0).setDepth(1500);
for (let i = 0; i < 4; i++) { for (let i = 0; i < 4; i++) {
const x = i * 50; const x = i * (slotSize + slotSpacing);
const g = this.scene.add.graphics(); const g = this.scene.add.graphics();
// Slot bg // Draw slot background (dark purple/brown)
g.fillStyle(0x1a1a1a, 0.8); g.fillStyle(0x2a1f3d, 0.95);
g.fillRect(0, 0, 40, 40); g.fillRect(0, 0, slotSize, slotSize);
g.lineStyle(1, 0x555555);
g.strokeRect(0, 0, 40, 40);
// Hotkey label // Draw gold border (default state)
const key = this.scene.add.text(2, 2, `${i + 1}`, { g.lineStyle(2, 0xD4AF37, 1);
fontSize: "10px", g.strokeRect(0, 0, slotSize, slotSize);
color: "#aaaaaa"
}); // Hotkey label (bottom-left, gold color)
const key = this.scene.add.text(3, slotSize - 3, `${i + 1}`, {
fontSize: "12px",
color: "#D4AF37",
fontStyle: "bold"
}).setOrigin(0, 1);
const slotContainer = this.scene.add.container(x, 0, [g, key]); const slotContainer = this.scene.add.container(x, 0, [g, key]);
this.slots.push(slotContainer); this.slots.push(slotContainer);
this.container.add(slotContainer); this.container.add(slotContainer);
// Input // Input
const hitArea = new Phaser.Geom.Rectangle(0, 0, 40, 40); const hitArea = new Phaser.Geom.Rectangle(0, 0, slotSize, slotSize);
slotContainer.setInteractive(hitArea, Phaser.Geom.Rectangle.Contains); slotContainer.setInteractive(hitArea, Phaser.Geom.Rectangle.Contains);
slotContainer.on("pointerdown", () => { slotContainer.on("pointerdown", () => {
this.activateSlot(i); this.activateSlot(i);
@@ -56,6 +67,8 @@ export class QuickSlotComponent {
update(player: CombatantActor, activeItemId?: string | null) { update(player: CombatantActor, activeItemId?: string | null) {
if (!player.inventory) return; if (!player.inventory) return;
const slotSize = 48;
// Update slots based on inventory availability // Update slots based on inventory availability
for (let i = 0; i < 4; i++) { for (let i = 0; i < 4; i++) {
const desiredId = this.assignedIds[i]; const desiredId = this.assignedIds[i];
@@ -75,39 +88,45 @@ export class QuickSlotComponent {
// Redraw background based on active state // Redraw background based on active state
bgGraphics.clear(); bgGraphics.clear();
bgGraphics.fillStyle(0x1a1a1a, 0.8);
bgGraphics.fillRect(0, 0, 40, 40);
// Dark background
bgGraphics.fillStyle(0x2a1f3d, 0.95);
bgGraphics.fillRect(0, 0, slotSize, slotSize);
// Border - subtle cyan for active, gold for normal
if (isActive) { if (isActive) {
bgGraphics.lineStyle(2, 0xffff00); // Gold highlight bgGraphics.lineStyle(2, 0x00E5FF, 1); // Cyan highlight
} else { } else {
bgGraphics.lineStyle(1, 0x555555); // Default gray bgGraphics.lineStyle(2, 0xD4AF37, 1); // Gold border
} }
bgGraphics.strokeRect(0, 0, 40, 40); bgGraphics.strokeRect(0, 0, slotSize, slotSize);
if (foundItem) { if (foundItem) {
const texture = foundItem.textureKey ?? "items"; const texture = foundItem.textureKey ?? "items";
const sprite = this.scene.add.sprite(20, 20, texture, foundItem.spriteIndex); const sprite = this.scene.add.sprite(slotSize / 2, slotSize / 2, texture, foundItem.spriteIndex);
// PD items are 16x16, slot is 40x40. Scale it up? // PD items are 16x16, slot is 48x48. Scale up slightly
sprite.setScale(2); sprite.setScale(2.5);
slot.add(sprite); slot.add(sprite);
// Add count if stackable (future) // Add count in bottom-right corner (white with x prefix)
const count = player.inventory.items.filter(it => it.id === desiredId).length; const count = player.inventory.items.filter(it => it.id === desiredId).length;
const countText = this.scene.add.text(38, 38, `${count}`, { if (count > 1) {
fontSize: "10px", const countText = this.scene.add.text(slotSize - 3, slotSize - 3, `x${count}`, {
color: "#ffffff" fontSize: "11px",
}).setOrigin(1, 1); color: "#ffffff",
slot.add(countText); fontStyle: "bold"
}).setOrigin(1, 1);
slot.add(countText);
}
} }
} else { } else {
this.itemMap[i] = null; this.itemMap[i] = null;
// Reset bg // Reset bg
bgGraphics.clear(); bgGraphics.clear();
bgGraphics.fillStyle(0x1a1a1a, 0.8); bgGraphics.fillStyle(0x2a1f3d, 0.95);
bgGraphics.fillRect(0, 0, 40, 40); bgGraphics.fillRect(0, 0, slotSize, slotSize);
bgGraphics.lineStyle(1, 0x555555); bgGraphics.lineStyle(2, 0xD4AF37, 1);
bgGraphics.strokeRect(0, 0, 40, 40); bgGraphics.strokeRect(0, 0, slotSize, slotSize);
} }
} }
} }