diff --git a/.gitignore b/.gitignore index 93c859f..90e656a 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ dist-ssr *.sln *.sw? -coverage \ No newline at end of file +coverage +.eslintcache \ No newline at end of file diff --git a/bun.lock b/bun.lock index dc9587b..30a3101 100644 --- a/bun.lock +++ b/bun.lock @@ -9,7 +9,11 @@ "rot-js": "^2.2.1", }, "devDependencies": { + "@typescript-eslint/eslint-plugin": "^8.52.0", + "@typescript-eslint/parser": "^8.52.0", "@vitest/coverage-v8": "^4.0.16", + "eslint": "^9.39.2", + "globals": "^17.0.0", "typescript": "~5.9.3", "vite": "^7.2.4", "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=="], + "@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/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/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/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=="], + "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=="], "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=="], + "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=="], + "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=="], "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=="], + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], "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=="], + "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=="], + "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=="], "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-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-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=="], "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=="], + "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=="], "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=="], + "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=="], "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=="], + "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=="], "rot-js": ["rot-js@2.2.1", "", {}, "sha512-lItXH31vj4ebdypayCx9dh98qPr57E7jGW2lVMKxtBHooU3xpGRtLS8kdJIni232tvPJ8Sl0+aqXZj8c6W0MGw=="], "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=="], "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=="], + "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=="], "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=="], + "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=="], + "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=="], "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=="], + + "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=="], } } diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..fe822e9 --- /dev/null +++ b/eslint.config.js @@ -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", + ], + }, +]; diff --git a/package.json b/package.json index 7c04866..d0fb161 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,16 @@ "test": "vitest", "test:coverage": "vitest run --coverage", "check": "tsc --noEmit", + "lint": "eslint src --ext .ts,.tsx", + "lint:fix": "eslint src --ext .ts,.tsx --fix", "verify": "bun run check && bun run test" }, "devDependencies": { + "@typescript-eslint/eslint-plugin": "^8.52.0", + "@typescript-eslint/parser": "^8.52.0", "@vitest/coverage-v8": "^4.0.16", + "eslint": "^9.39.2", + "globals": "^17.0.0", "typescript": "~5.9.3", "vite": "^7.2.4", "vitest": "^4.0.16" diff --git a/src/core/config/GameConfig.ts b/src/core/config/GameConfig.ts index ff178d8..93ce628 100644 --- a/src/core/config/GameConfig.ts +++ b/src/core/config/GameConfig.ts @@ -121,7 +121,11 @@ export const GAME_CONFIG = { minimapPanelHeight: 220, minimapPadding: 20, menuPanelWidth: 340, - menuPanelHeight: 220 + menuPanelHeight: 220, + // Targeting + targetingLineColor: 0xff0000, + targetingLineWidth: 2, + targetingLineAlpha: 0.7 }, gameplay: { diff --git a/src/rendering/FovManager.ts b/src/rendering/FovManager.ts index e4710e6..acad408 100644 --- a/src/rendering/FovManager.ts +++ b/src/rendering/FovManager.ts @@ -1,4 +1,5 @@ import { FOV } from "rot-js"; +import type ROT from "rot-js"; import { type World, type EntityId } from "../core/types"; import { idx, inBounds } from "../engine/world/world-logic"; import { blocksSight } from "../core/terrain"; @@ -6,7 +7,7 @@ import { GAME_CONFIG } from "../core/config/GameConfig"; import Phaser from "phaser"; export class FovManager { - private fov!: any; + private fov!: InstanceType; private seen!: Uint8Array; private visible!: Uint8Array; private visibleStrength!: Float32Array; @@ -51,12 +52,12 @@ export class FovManager { } 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; } 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; } diff --git a/src/scenes/GameScene.ts b/src/scenes/GameScene.ts index dfdaa76..883a6c4 100644 --- a/src/scenes/GameScene.ts +++ b/src/scenes/GameScene.ts @@ -7,8 +7,6 @@ import { type RunState, type World, type CombatantActor, - type Item, - type ItemDropActor, type UIUpdatePayload } from "../core/types"; import { TILE_SIZE } from "../core/constants"; @@ -16,14 +14,14 @@ import { inBounds, isBlocked, isPlayerOnExit, tryDestructTile } from "../engine/ import { findPathAStar } from "../engine/world/pathfinding"; import { applyAction, stepUntilPlayerTurn } from "../engine/simulation/simulation"; import { generateWorld } from "../engine/world/generator"; -import { traceProjectile, getClosestVisibleEnemy } from "../engine/gameplay/CombatLogic"; - - - import { DungeonRenderer } from "../rendering/DungeonRenderer"; import { GAME_CONFIG } from "../core/config/GameConfig"; import { EntityManager } from "../engine/EntityManager"; 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 { private world!: World; @@ -40,22 +38,18 @@ export class GameScene extends Phaser.Scene { private playerPath: Vec2[] = []; private awaitingPlayer = false; - private followPlayer = true; // Sub-systems private dungeonRenderer!: DungeonRenderer; + private cameraController!: CameraController; + private itemManager!: ItemManager; private isMenuOpen = false; private isInventoryOpen = false; private isCharacterOpen = false; private entityManager!: EntityManager; private progressionManager: ProgressionManager = new ProgressionManager(); - - // Targeting Mode - private isTargeting = false; - private targetingItem: string | null = null; - private targetCursor: { x: number, y: number } | null = null; - private targetingGraphics!: Phaser.GameObjects.Graphics; + private targetingSystem!: TargetingSystem; private turnCount = 0; // Track turns for mana regen @@ -72,7 +66,10 @@ export class GameScene extends Phaser.Scene { // Initialize Sub-systems 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 this.scene.launch("GameUI"); @@ -177,82 +174,48 @@ export class GameScene extends Phaser.Scene { if (itemIdx === -1) return; const item = player.inventory.items[itemIdx]; - if (item.stats && item.stats.hp && item.stats.hp > 0) { - 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 - player.inventory.items.splice(itemIdx, 1); - - this.dungeonRenderer.showHeal(player.pos.x, player.pos.y, healAmount); - this.commitPlayerAction({ type: "wait" }); - this.emitUIUpdate(); - } - } else if (item.throwable) { - // Check if already targeting this item -> verify intent to throw - if (this.isTargeting && this.targetingItem === item.id) { - if (this.targetCursor) { - this.executeThrow(this.targetCursor.x, this.targetCursor.y); + const result = this.itemManager.handleUse(data.itemId, player); + + if (result.success && result.consumed) { + 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, actualHeal); + this.commitPlayerAction({ type: "wait" }); + this.emitUIUpdate(); + } else if (result.success && !result.consumed) { + // Throwable item - start targeting + if (this.targetingSystem.isActive && this.targetingSystem.itemId === item.id) { + // Already targeting - execute throw + if (this.targetingSystem.cursorPos) { + this.executeThrow(this.targetingSystem.cursorPos.x, this.targetingSystem.cursorPos.y); } return; } - this.targetingItem = item.id; - this.isTargeting = true; - - // Auto-target closest visible enemy - const closest = getClosestVisibleEnemy( - this.world, - player.pos, - this.dungeonRenderer.seenArray, + this.targetingSystem.startTargeting( + item.id, + player.pos, + this.world, + this.dungeonRenderer.seenArray, 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(); } }); // Right Clicks to cancel targeting this.input.on('pointerdown', (p: Phaser.Input.Pointer) => { - if (p.rightButtonDown() && this.isTargeting) { - this.cancelTargeting(); + if (p.rightButtonDown() && this.targetingSystem.isActive) { + this.targetingSystem.cancel(); + this.emitUIUpdate(); } }); // Zoom Control - this.input.on( - "wheel", - ( - _pointer: Phaser.Input.Pointer, - _gameObjects: any, - _deltaX: number, - deltaY: number, - _deltaZ: number - ) => { + this.input.on("wheel", (_pointer: Phaser.Input.Pointer, _gameObjects: Phaser.GameObjects.GameObject[], _deltaX: number, deltaY: number, _deltaZ: number) => { if (this.isMenuOpen || this.isInventoryOpen || this.dungeonRenderer.isMinimapVisible()) return; - - 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); - } - ); + this.cameraController.handleWheel(deltaY); + }); // Disable context menu for right-click panning this.input.mouse?.disableContextMenu(); @@ -260,12 +223,13 @@ export class GameScene extends Phaser.Scene { // Camera Panning this.input.on("pointermove", (p: Phaser.Input.Pointer) => { 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 ty = Math.floor(p.worldY / TILE_SIZE); - // Only update if changed to avoid jitter if needed, but simple assignment is fine - this.targetCursor = { x: tx, y: ty }; - this.drawTargetingLine(); + const player = this.world.actors.get(this.playerId) as CombatantActor; + if (player) { + this.targetingSystem.updateCursor({ x: tx, y: ty }, player.pos); + } } return; } @@ -283,28 +247,27 @@ export class GameScene extends Phaser.Scene { const dx = (x - prevX) / this.cameras.main.zoom; const dy = (y - prevY) / this.cameras.main.zoom; - this.cameras.main.scrollX -= dx; - this.cameras.main.scrollY -= dy; - - this.followPlayer = false; + this.cameraController.handlePan(dx, dy); } - if (this.isTargeting) { + if (this.targetingSystem.isActive) { const tx = Math.floor(p.worldX / TILE_SIZE); const ty = Math.floor(p.worldY / TILE_SIZE); - this.targetCursor = { x: tx, y: ty }; - this.drawTargetingLine(); + const player = this.world.actors.get(this.playerId) as CombatantActor; + if (player) { + this.targetingSystem.updateCursor({ x: tx, y: ty }, player.pos); + } } }); // Mouse click -> this.input.on("pointerdown", (p: Phaser.Input.Pointer) => { // Targeting Click - if (this.isTargeting) { + if (this.targetingSystem.isActive) { // Only Left Click throws if (p.button === 0) { - if (this.targetCursor) { - this.executeThrow(this.targetCursor.x, this.targetCursor.y); + if (this.targetingSystem.cursorPos) { + this.executeThrow(this.targetingSystem.cursorPos.x, this.targetingSystem.cursorPos.y); } } return; @@ -313,7 +276,7 @@ export class GameScene extends Phaser.Scene { // Movement Click if (p.button !== 0) return; - this.followPlayer = true; + this.cameraController.enableFollowMode(); if (!this.awaitingPlayer) 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 (dx !== 0 || dy !== 0) { - if (this.isTargeting) { - this.cancelTargeting(); + if (this.targetingSystem.isActive) { + this.targetingSystem.cancel(); + this.emitUIUpdate(); } const player = this.world.actors.get(this.playerId) as CombatantActor; const targetX = player.pos.x + dx; @@ -443,7 +407,7 @@ export class GameScene extends Phaser.Scene { playerId: this.playerId, floorIndex: this.floorIndex, uiState: { - targetingItemId: this.targetingItem + targetingItemId: this.targetingSystem.itemId } }; this.events.emit("update-ui", payload); @@ -457,11 +421,15 @@ export class GameScene extends Phaser.Scene { } this.awaitingPlayer = false; - this.followPlayer = true; + this.cameraController.enableFollowMode(); // Check for pickups right after move (before enemy turn, so you get it efficiently) 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); @@ -509,8 +477,8 @@ export class GameScene extends Phaser.Scene { if (!this.world.actors.has(this.playerId)) { this.syncRunStateFromPlayer(); - const uiScene = this.scene.get("GameUI") as any; - if (uiScene) { + const uiScene = this.scene.get("GameUI") as GameUI; + if (uiScene && 'showDeathScreen' in uiScene) { uiScene.showDeathScreen({ floor: this.floorIndex, gold: this.runState.inventory.gold, @@ -528,8 +496,9 @@ export class GameScene extends Phaser.Scene { } this.dungeonRenderer.computeFov(this.playerId); - if (this.followPlayer) { - this.centerCameraOnPlayer(); + if (this.cameraController.isFollowing) { + 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.emitUIUpdate(); @@ -537,18 +506,19 @@ export class GameScene extends Phaser.Scene { private loadFloor(floor: number) { this.floorIndex = floor; - this.followPlayer = true; + this.cameraController.enableFollowMode(); const { world, playerId } = generateWorld(floor, this.runState); this.world = world; this.playerId = playerId; this.entityManager = new EntityManager(this.world); + this.itemManager.updateWorld(this.world, this.entityManager); this.playerPath = []; 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); @@ -557,7 +527,8 @@ export class GameScene extends Phaser.Scene { 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.emitUIUpdate(); @@ -584,139 +555,49 @@ export class GameScene extends Phaser.Scene { 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; - const startY = player.pos.y * TILE_SIZE + TILE_SIZE / 2; - - const endX = this.targetCursor.x * TILE_SIZE + TILE_SIZE / 2; - const endY = this.targetCursor.y * TILE_SIZE + TILE_SIZE / 2; - 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, - () => { + private executeThrow(_targetX: number, _targetY: number) { + const success = this.targetingSystem.executeThrow( + this.world, + this.playerId, + this.entityManager, + (blockedPos, hitActorId, item) => { if (hitActorId !== undefined) { const victim = this.world.actors.get(hitActorId) as CombatantActor; if (victim) { - const dmg = item.stats?.attack ?? 1; // Use item stats + const dmg = item.stats?.attack ?? 1; victim.stats.hp -= dmg; this.dungeonRenderer.showDamage(victim.pos.x, victim.pos.y, dmg); this.dungeonRenderer.shakeCamera(); - - if (victim.stats.hp <= 0) { - // Force kill handled by simulation - } } } - // Drop the actual item at the landing spot - this.spawnItem(item, blockedPos.x, blockedPos.y); + const player = this.world.actors.get(this.playerId) as CombatantActor; + 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 - // e.g. breaking grass, opening items - if (tryDestructTile(this.world, blockedPos.x, blockedPos.y)) { - this.dungeonRenderer.updateTile(blockedPos.x, blockedPos.y); - } + // Trigger destruction/interaction + if (tryDestructTile(this.world, blockedPos.x, blockedPos.y)) { + this.dungeonRenderer.updateTile(blockedPos.x, blockedPos.y); + } - this.cancelTargeting(); - this.commitPlayerAction({ type: "throw" }); - this.emitUIUpdate(); + this.targetingSystem.cancel(); + this.commitPlayerAction({ type: "throw" }); + this.emitUIUpdate(); + } + ); } ); - } - private spawnItem(item: Item, x: number, y: number) { - 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 + if (!success) { this.emitUIUpdate(); } } diff --git a/src/scenes/systems/CameraController.ts b/src/scenes/systems/CameraController.ts new file mode 100644 index 0000000..aece70c --- /dev/null +++ b/src/scenes/systems/CameraController.ts @@ -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); + } +} diff --git a/src/scenes/systems/EventBridge.ts b/src/scenes/systems/EventBridge.ts new file mode 100644 index 0000000..ab38f26 --- /dev/null +++ b/src/scenes/systems/EventBridge.ts @@ -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"); + } +} diff --git a/src/scenes/systems/ItemManager.ts b/src/scenes/systems/ItemManager.ts new file mode 100644 index 0000000..b1113bf --- /dev/null +++ b/src/scenes/systems/ItemManager.ts @@ -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; + } +} diff --git a/src/scenes/systems/TargetingSystem.ts b/src/scenes/systems/TargetingSystem.ts new file mode 100644 index 0000000..3efdf46 --- /dev/null +++ b/src/scenes/systems/TargetingSystem.ts @@ -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 + ); + } +}