Initial commit
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
201
bun.lock
Normal file
201
bun.lock
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "rogue",
|
||||||
|
"dependencies": {
|
||||||
|
"phaser": "^3.90.0",
|
||||||
|
"rot-js": "^2.2.1",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "~5.9.3",
|
||||||
|
"vite": "^7.2.4",
|
||||||
|
"vitest": "^4.0.16",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="],
|
||||||
|
|
||||||
|
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="],
|
||||||
|
|
||||||
|
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="],
|
||||||
|
|
||||||
|
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="],
|
||||||
|
|
||||||
|
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="],
|
||||||
|
|
||||||
|
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="],
|
||||||
|
|
||||||
|
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="],
|
||||||
|
|
||||||
|
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="],
|
||||||
|
|
||||||
|
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.54.0", "", { "os": "android", "cpu": "arm" }, "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.54.0", "", { "os": "android", "cpu": "arm64" }, "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.54.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.54.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.54.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.54.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.54.0", "", { "os": "linux", "cpu": "arm" }, "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.54.0", "", { "os": "linux", "cpu": "arm" }, "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.54.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.54.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.54.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.54.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.54.0", "", { "os": "linux", "cpu": "x64" }, "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.54.0", "", { "os": "linux", "cpu": "x64" }, "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.54.0", "", { "os": "none", "cpu": "arm64" }, "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.54.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.54.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.54.0", "", { "os": "win32", "cpu": "x64" }, "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.54.0", "", { "os": "win32", "cpu": "x64" }, "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg=="],
|
||||||
|
|
||||||
|
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
|
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
|
||||||
|
|
||||||
|
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
||||||
|
|
||||||
|
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
|
|
||||||
|
"@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/mocker": ["@vitest/mocker@4.0.16", "", { "dependencies": { "@vitest/spy": "4.0.16", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg=="],
|
||||||
|
|
||||||
|
"@vitest/pretty-format": ["@vitest/pretty-format@4.0.16", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA=="],
|
||||||
|
|
||||||
|
"@vitest/runner": ["@vitest/runner@4.0.16", "", { "dependencies": { "@vitest/utils": "4.0.16", "pathe": "^2.0.3" } }, "sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q=="],
|
||||||
|
|
||||||
|
"@vitest/snapshot": ["@vitest/snapshot@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA=="],
|
||||||
|
|
||||||
|
"@vitest/spy": ["@vitest/spy@4.0.16", "", {}, "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw=="],
|
||||||
|
|
||||||
|
"@vitest/utils": ["@vitest/utils@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "tinyrainbow": "^3.0.3" } }, "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA=="],
|
||||||
|
|
||||||
|
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
|
||||||
|
|
||||||
|
"chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
|
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||||
|
|
||||||
|
"eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="],
|
||||||
|
|
||||||
|
"expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
|
||||||
|
|
||||||
|
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||||
|
|
||||||
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
|
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||||
|
|
||||||
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
|
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
|
||||||
|
|
||||||
|
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||||
|
|
||||||
|
"phaser": ["phaser@3.90.0", "", { "dependencies": { "eventemitter3": "^5.0.1" } }, "sha512-/cziz/5ZIn02uDkC9RzN8VF9x3Gs3XdFFf9nkiMEQT3p7hQlWuyjy4QWosU802qqno2YSLn2BfqwOKLv/sSVfQ=="],
|
||||||
|
|
||||||
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
|
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
|
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
|
||||||
|
|
||||||
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
|
||||||
|
|
||||||
|
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
|
||||||
|
|
||||||
|
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
|
||||||
|
|
||||||
|
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
||||||
|
|
||||||
|
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||||
|
|
||||||
|
"tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>rogue</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
21
package.json
Normal file
21
package.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "rogue",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"test": "vitest"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "~5.9.3",
|
||||||
|
"vite": "^7.2.4",
|
||||||
|
"vitest": "^4.0.16"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"phaser": "^3.90.0",
|
||||||
|
"rot-js": "^2.2.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/favicon.png
Normal file
BIN
public/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 398 KiB |
BIN
public/splash_bg.png
Normal file
BIN
public/splash_bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 616 KiB |
62
src/game/generator.ts
Normal file
62
src/game/generator.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { type World, type EntityId, type RunState, type Tile, type Actor, type Vec2 } from "./types";
|
||||||
|
import { idx } from "./world";
|
||||||
|
|
||||||
|
export function makeTestWorld(level: number, runState: RunState): { world: World; playerId: EntityId } {
|
||||||
|
const width = 30;
|
||||||
|
const height = 18;
|
||||||
|
const tiles: Tile[] = new Array(width * height).fill(0);
|
||||||
|
|
||||||
|
const fakeWorldForIdx: World = { width, height, tiles, actors: new Map(), exit: { x: 0, y: 0 } };
|
||||||
|
|
||||||
|
// Border walls
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
tiles[idx(fakeWorldForIdx, x, 0)] = 1;
|
||||||
|
tiles[idx(fakeWorldForIdx, x, height - 1)] = 1;
|
||||||
|
}
|
||||||
|
for (let y = 0; y < height; y++) {
|
||||||
|
tiles[idx(fakeWorldForIdx, 0, y)] = 1;
|
||||||
|
tiles[idx(fakeWorldForIdx, width - 1, y)] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal walls (vary slightly with level so it feels different)
|
||||||
|
const shift = level % 4;
|
||||||
|
for (let x = 6; x < 22; x++) tiles[idx(fakeWorldForIdx, x, 7 + (shift % 2))] = 1;
|
||||||
|
for (let y = 4; y < 14; y++) tiles[idx(fakeWorldForIdx, 14 + ((shift + 1) % 2), y)] = 1;
|
||||||
|
|
||||||
|
// Exit (stairs)
|
||||||
|
const exit: Vec2 = { x: width - 3, y: height - 3 };
|
||||||
|
tiles[idx(fakeWorldForIdx, exit.x, exit.y)] = 0;
|
||||||
|
|
||||||
|
const actors = new Map<EntityId, Actor>();
|
||||||
|
|
||||||
|
const playerId = 1;
|
||||||
|
actors.set(playerId, {
|
||||||
|
id: playerId,
|
||||||
|
isPlayer: true,
|
||||||
|
pos: { x: 3, y: 3 },
|
||||||
|
speed: 100,
|
||||||
|
energy: 0,
|
||||||
|
stats: { ...runState.stats },
|
||||||
|
inventory: { gold: runState.inventory.gold, items: [...runState.inventory.items] }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enemies
|
||||||
|
actors.set(2, {
|
||||||
|
id: 2,
|
||||||
|
isPlayer: false,
|
||||||
|
pos: { x: 24, y: 13 },
|
||||||
|
speed: 90,
|
||||||
|
energy: 0,
|
||||||
|
stats: { maxHp: 10, hp: 10, attack: 3, defense: 1 }
|
||||||
|
});
|
||||||
|
actors.set(3, {
|
||||||
|
id: 3,
|
||||||
|
isPlayer: false,
|
||||||
|
pos: { x: 20, y: 4 },
|
||||||
|
speed: 130,
|
||||||
|
energy: 0,
|
||||||
|
stats: { maxHp: 8, hp: 8, attack: 4, defense: 0 }
|
||||||
|
});
|
||||||
|
|
||||||
|
return { world: { width, height, tiles, actors, exit }, playerId };
|
||||||
|
}
|
||||||
102
src/game/pathfinding.ts
Normal file
102
src/game/pathfinding.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import type { World, Vec2 } from "./types";
|
||||||
|
import { key, manhattan } from "./utils";
|
||||||
|
import { inBounds, isWall, isBlocked, idx } from "./world";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple 4-dir A* pathfinding.
|
||||||
|
* Returns an array of positions INCLUDING start and end. If no path, returns [].
|
||||||
|
*
|
||||||
|
* Exploration rule:
|
||||||
|
* - You cannot path THROUGH unseen tiles.
|
||||||
|
* - You cannot path TO an unseen target tile.
|
||||||
|
*/
|
||||||
|
export function findPathAStar(w: World, seen: Uint8Array, start: Vec2, end: Vec2, options: { ignoreBlockedTarget?: boolean } = {}): Vec2[] {
|
||||||
|
if (!inBounds(w, end.x, end.y)) return [];
|
||||||
|
if (isWall(w, end.x, end.y)) return [];
|
||||||
|
|
||||||
|
// If not ignoring target block, fail if blocked
|
||||||
|
if (!options.ignoreBlockedTarget && isBlocked(w, end.x, end.y)) return [];
|
||||||
|
|
||||||
|
if (seen[idx(w, end.x, end.y)] !== 1) return [];
|
||||||
|
|
||||||
|
const open: Vec2[] = [start];
|
||||||
|
const cameFrom = new Map<string, string>();
|
||||||
|
|
||||||
|
const gScore = new Map<string, number>();
|
||||||
|
const fScore = new Map<string, number>();
|
||||||
|
|
||||||
|
const startK = key(start.x, start.y);
|
||||||
|
gScore.set(startK, 0);
|
||||||
|
fScore.set(startK, manhattan(start, end));
|
||||||
|
|
||||||
|
const inOpen = new Set<string>([startK]);
|
||||||
|
|
||||||
|
const dirs = [
|
||||||
|
{ x: 1, y: 0 },
|
||||||
|
{ x: -1, y: 0 },
|
||||||
|
{ x: 0, y: 1 },
|
||||||
|
{ x: 0, y: -1 }
|
||||||
|
];
|
||||||
|
|
||||||
|
while (open.length > 0) {
|
||||||
|
// Pick node with lowest fScore
|
||||||
|
let bestIdx = 0;
|
||||||
|
let bestF = Infinity;
|
||||||
|
for (let i = 0; i < open.length; i++) {
|
||||||
|
const k = key(open[i].x, open[i].y);
|
||||||
|
const f = fScore.get(k) ?? Infinity;
|
||||||
|
if (f < bestF) {
|
||||||
|
bestF = f;
|
||||||
|
bestIdx = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = open.splice(bestIdx, 1)[0];
|
||||||
|
const currentK = key(current.x, current.y);
|
||||||
|
inOpen.delete(currentK);
|
||||||
|
|
||||||
|
if (current.x === end.x && current.y === end.y) {
|
||||||
|
// Reconstruct path
|
||||||
|
const path: Vec2[] = [end];
|
||||||
|
let k = currentK;
|
||||||
|
while (cameFrom.has(k)) {
|
||||||
|
const prevK = cameFrom.get(k)!;
|
||||||
|
const [px, py] = prevK.split(",").map(Number);
|
||||||
|
path.push({ x: px, y: py });
|
||||||
|
k = prevK;
|
||||||
|
}
|
||||||
|
path.reverse();
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const d of dirs) {
|
||||||
|
const nx = current.x + d.x;
|
||||||
|
const ny = current.y + d.y;
|
||||||
|
if (!inBounds(w, nx, ny)) continue;
|
||||||
|
if (isWall(w, nx, ny)) continue;
|
||||||
|
|
||||||
|
// Exploration rule: cannot path through unseen (except start)
|
||||||
|
if (!(nx === start.x && ny === start.y) && seen[idx(w, nx, ny)] !== 1) continue;
|
||||||
|
|
||||||
|
// Avoid walking through other actors (except standing on start, OR if it is the target and we ignore block)
|
||||||
|
const isTarget = nx === end.x && ny === end.y;
|
||||||
|
if (!isTarget && !(nx === start.x && ny === start.y) && isBlocked(w, nx, ny)) continue;
|
||||||
|
|
||||||
|
const nK = key(nx, ny);
|
||||||
|
const tentativeG = (gScore.get(currentK) ?? Infinity) + 1;
|
||||||
|
|
||||||
|
if (tentativeG < (gScore.get(nK) ?? Infinity)) {
|
||||||
|
cameFrom.set(nK, currentK);
|
||||||
|
gScore.set(nK, tentativeG);
|
||||||
|
fScore.set(nK, tentativeG + manhattan({ x: nx, y: ny }, end));
|
||||||
|
|
||||||
|
if (!inOpen.has(nK)) {
|
||||||
|
open.push({ x: nx, y: ny });
|
||||||
|
inOpen.add(nK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
120
src/game/simulation.ts
Normal file
120
src/game/simulation.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import { ACTION_COST, ENERGY_THRESHOLD } from "./types";
|
||||||
|
import type { World, EntityId, Action, SimEvent, Actor } from "./types";
|
||||||
|
import { isBlocked } from "./world";
|
||||||
|
|
||||||
|
export function applyAction(w: World, actorId: EntityId, action: Action): SimEvent[] {
|
||||||
|
const actor = w.actors.get(actorId);
|
||||||
|
if (!actor) return [];
|
||||||
|
|
||||||
|
const events: SimEvent[] = [];
|
||||||
|
|
||||||
|
if (action.type === "move") {
|
||||||
|
const from = { ...actor.pos };
|
||||||
|
const nx = actor.pos.x + action.dx;
|
||||||
|
const ny = actor.pos.y + action.dy;
|
||||||
|
|
||||||
|
if (!isBlocked(w, nx, ny)) {
|
||||||
|
actor.pos.x = nx;
|
||||||
|
actor.pos.y = ny;
|
||||||
|
const to = { ...actor.pos };
|
||||||
|
events.push({ type: "moved", actorId, from, to });
|
||||||
|
} else {
|
||||||
|
events.push({ type: "waited", actorId });
|
||||||
|
}
|
||||||
|
} else if (action.type === "attack") {
|
||||||
|
console.log("Sim: Processing Attack on", action.targetId);
|
||||||
|
const target = w.actors.get(action.targetId);
|
||||||
|
if (target && target.stats && actor.stats) {
|
||||||
|
events.push({ type: "attacked", attackerId: actorId, targetId: action.targetId });
|
||||||
|
|
||||||
|
const dmg = Math.max(1, actor.stats.attack - target.stats.defense);
|
||||||
|
console.log("Sim: calculated damage:", dmg);
|
||||||
|
target.stats.hp -= dmg;
|
||||||
|
|
||||||
|
events.push({
|
||||||
|
type: "damaged",
|
||||||
|
targetId: action.targetId,
|
||||||
|
amount: dmg,
|
||||||
|
hp: target.stats.hp,
|
||||||
|
x: target.pos.x,
|
||||||
|
y: target.pos.y
|
||||||
|
});
|
||||||
|
|
||||||
|
if (target.stats.hp <= 0) {
|
||||||
|
w.actors.delete(target.id);
|
||||||
|
events.push({ type: "killed", targetId: target.id, killerId: actorId });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
events.push({ type: "waited", actorId }); // Missed or invalid target
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
events.push({ type: "waited", actorId });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spend energy for any action (move/wait/attack)
|
||||||
|
actor.energy -= ACTION_COST;
|
||||||
|
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Very basic enemy AI:
|
||||||
|
* - if adjacent to player, "wait" (placeholder for attack)
|
||||||
|
* - else step toward player using greedy Manhattan
|
||||||
|
*/
|
||||||
|
export function decideEnemyAction(w: World, enemy: Actor, player: Actor): Action {
|
||||||
|
const dx = player.pos.x - enemy.pos.x;
|
||||||
|
const dy = player.pos.y - enemy.pos.y;
|
||||||
|
const dist = Math.abs(dx) + Math.abs(dy);
|
||||||
|
|
||||||
|
if (dist === 1) {
|
||||||
|
return { type: "attack", targetId: player.id };
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: { dx: number; dy: number }[] = [];
|
||||||
|
if (Math.abs(dx) >= Math.abs(dy)) {
|
||||||
|
options.push({ dx: Math.sign(dx), dy: 0 });
|
||||||
|
options.push({ dx: 0, dy: Math.sign(dy) });
|
||||||
|
} else {
|
||||||
|
options.push({ dx: 0, dy: Math.sign(dy) });
|
||||||
|
options.push({ dx: Math.sign(dx), dy: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
options.push({ dx: -options[0].dx, dy: -options[0].dy });
|
||||||
|
|
||||||
|
for (const o of options) {
|
||||||
|
if (o.dx === 0 && o.dy === 0) continue;
|
||||||
|
const nx = enemy.pos.x + o.dx;
|
||||||
|
const ny = enemy.pos.y + o.dy;
|
||||||
|
if (!isBlocked(w, nx, ny)) return { type: "move", dx: o.dx, dy: o.dy };
|
||||||
|
}
|
||||||
|
return { type: "wait" };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Energy/speed scheduler: runs until it's the player's turn and the game needs input.
|
||||||
|
* Returns enemy events accumulated along the way.
|
||||||
|
*/
|
||||||
|
export function stepUntilPlayerTurn(w: World, playerId: EntityId): { awaitingPlayerId: EntityId; events: SimEvent[] } {
|
||||||
|
const player = w.actors.get(playerId);
|
||||||
|
if (!player) throw new Error("Player missing");
|
||||||
|
|
||||||
|
const events: SimEvent[] = [];
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
while (![...w.actors.values()].some(a => a.energy >= ENERGY_THRESHOLD)) {
|
||||||
|
for (const a of w.actors.values()) a.energy += a.speed;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ready = [...w.actors.values()].filter(a => a.energy >= ENERGY_THRESHOLD);
|
||||||
|
ready.sort((a, b) => (b.energy - a.energy) || (a.id - b.id));
|
||||||
|
const actor = ready[0];
|
||||||
|
|
||||||
|
if (actor.isPlayer) {
|
||||||
|
return { awaitingPlayerId: actor.id, events };
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = decideEnemyAction(w, actor, player);
|
||||||
|
events.push(...applyAction(w, actor.id, action));
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/game/types.ts
Normal file
58
src/game/types.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
export type EntityId = number;
|
||||||
|
|
||||||
|
export type Vec2 = { x: number; y: number };
|
||||||
|
|
||||||
|
export type Tile = 0 | 1; // 0 = floor, 1 = wall
|
||||||
|
|
||||||
|
export type Action =
|
||||||
|
| { type: "move"; dx: number; dy: number }
|
||||||
|
| { type: "attack"; targetId: EntityId }
|
||||||
|
| { type: "wait" };
|
||||||
|
|
||||||
|
export type SimEvent =
|
||||||
|
| { type: "moved"; actorId: EntityId; from: Vec2; to: Vec2 }
|
||||||
|
| { type: "moved"; actorId: EntityId; from: Vec2; to: Vec2 }
|
||||||
|
| { type: "attacked"; attackerId: EntityId; targetId: EntityId }
|
||||||
|
| { type: "damaged"; targetId: EntityId; amount: number; hp: number; x: number; y: number }
|
||||||
|
| { type: "killed"; targetId: EntityId; killerId: EntityId }
|
||||||
|
| { type: "waited"; actorId: EntityId };
|
||||||
|
|
||||||
|
export type Stats = {
|
||||||
|
maxHp: number;
|
||||||
|
hp: number;
|
||||||
|
attack: number;
|
||||||
|
defense: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Inventory = {
|
||||||
|
gold: number;
|
||||||
|
items: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RunState = {
|
||||||
|
stats: Stats;
|
||||||
|
inventory: Inventory;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Actor = {
|
||||||
|
id: EntityId;
|
||||||
|
isPlayer: boolean;
|
||||||
|
pos: Vec2;
|
||||||
|
speed: number;
|
||||||
|
energy: number;
|
||||||
|
|
||||||
|
stats?: Stats;
|
||||||
|
inventory?: Inventory;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type World = {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
tiles: Tile[];
|
||||||
|
actors: Map<EntityId, Actor>;
|
||||||
|
exit: Vec2;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TILE_SIZE = 24;
|
||||||
|
export const ENERGY_THRESHOLD = 100;
|
||||||
|
export const ACTION_COST = 100;
|
||||||
9
src/game/utils.ts
Normal file
9
src/game/utils.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import type { Vec2 } from "./types";
|
||||||
|
|
||||||
|
export function key(x: number, y: number) {
|
||||||
|
return `${x},${y}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function manhattan(a: Vec2, b: Vec2) {
|
||||||
|
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
|
||||||
|
}
|
||||||
29
src/game/world.ts
Normal file
29
src/game/world.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import type { World, EntityId } from "./types";
|
||||||
|
|
||||||
|
export function inBounds(w: World, x: number, y: number) {
|
||||||
|
return x >= 0 && y >= 0 && x < w.width && y < w.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function idx(w: World, x: number, y: number) {
|
||||||
|
return y * w.width + x;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isWall(w: World, x: number, y: number) {
|
||||||
|
return w.tiles[idx(w, x, y)] === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isBlocked(w: World, x: number, y: number) {
|
||||||
|
if (!inBounds(w, x, y)) return true;
|
||||||
|
if (isWall(w, x, y)) return true;
|
||||||
|
|
||||||
|
for (const a of w.actors.values()) {
|
||||||
|
if (a.pos.x === x && a.pos.y === y) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPlayerOnExit(w: World, playerId: EntityId) {
|
||||||
|
const p = w.actors.get(playerId);
|
||||||
|
if (!p) return false;
|
||||||
|
return p.pos.x === w.exit.x && p.pos.y === w.exit.y;
|
||||||
|
}
|
||||||
15
src/main.ts
Normal file
15
src/main.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import GameUI from "./scenes/GameUI";
|
||||||
|
import { GameScene } from "./scenes/GameScene";
|
||||||
|
import { SplashScene } from "./scenes/SplashScene";
|
||||||
|
import { StartScene } from "./scenes/StartScene";
|
||||||
|
|
||||||
|
new Phaser.Game({
|
||||||
|
type: Phaser.AUTO,
|
||||||
|
parent: "app",
|
||||||
|
width: 960,
|
||||||
|
height: 540,
|
||||||
|
backgroundColor: "#111",
|
||||||
|
pixelArt: true,
|
||||||
|
roundPixels: true,
|
||||||
|
scene: [SplashScene, StartScene, GameScene, GameUI]
|
||||||
|
});
|
||||||
161
src/scenes/DungeonRenderer.ts
Normal file
161
src/scenes/DungeonRenderer.ts
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import Phaser from "phaser";
|
||||||
|
import { FOV } from "rot-js";
|
||||||
|
import { type World, type EntityId, type Vec2, TILE_SIZE } from "../game/types";
|
||||||
|
import { idx, inBounds, isWall } from "../game/world";
|
||||||
|
|
||||||
|
export class DungeonRenderer {
|
||||||
|
private scene: Phaser.Scene;
|
||||||
|
private gfx: Phaser.GameObjects.Graphics;
|
||||||
|
|
||||||
|
// FOV
|
||||||
|
private fov!: any;
|
||||||
|
private seen!: Uint8Array;
|
||||||
|
private visible!: Uint8Array;
|
||||||
|
private visibleStrength!: Float32Array;
|
||||||
|
private viewRadius = 8;
|
||||||
|
|
||||||
|
// State refs
|
||||||
|
private world!: World;
|
||||||
|
|
||||||
|
constructor(scene: Phaser.Scene) {
|
||||||
|
this.scene = scene;
|
||||||
|
this.gfx = this.scene.add.graphics();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeLevel(world: World) {
|
||||||
|
this.world = world;
|
||||||
|
this.seen = new Uint8Array(this.world.width * this.world.height);
|
||||||
|
this.visible = new Uint8Array(this.world.width * this.world.height);
|
||||||
|
this.visibleStrength = new Float32Array(this.world.width * this.world.height);
|
||||||
|
|
||||||
|
this.fov = new FOV.PreciseShadowcasting((x: number, y: number) => {
|
||||||
|
if (!inBounds(this.world, x, y)) return false;
|
||||||
|
return !isWall(this.world, x, y);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
computeFov(playerId: EntityId) {
|
||||||
|
this.visible.fill(0);
|
||||||
|
this.visibleStrength.fill(0);
|
||||||
|
|
||||||
|
const player = this.world.actors.get(playerId)!;
|
||||||
|
const ox = player.pos.x;
|
||||||
|
const oy = player.pos.y;
|
||||||
|
|
||||||
|
this.fov.compute(ox, oy, this.viewRadius, (x: number, y: number, r: number, v: number) => {
|
||||||
|
if (!inBounds(this.world, x, y)) return;
|
||||||
|
|
||||||
|
const i = idx(this.world, x, y);
|
||||||
|
this.visible[i] = 1;
|
||||||
|
this.seen[i] = 1;
|
||||||
|
|
||||||
|
// falloff: 1 at center, ~0.4 at radius edge
|
||||||
|
const radiusT = Phaser.Math.Clamp(r / this.viewRadius, 0, 1);
|
||||||
|
const falloff = 1 - radiusT * 0.6;
|
||||||
|
const strength = Phaser.Math.Clamp(v * falloff, 0, 1);
|
||||||
|
|
||||||
|
if (strength > this.visibleStrength[i]) this.visibleStrength[i] = strength;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isSeen(x: number, y: number): boolean {
|
||||||
|
if (!this.world || !inBounds(this.world, x, y)) return false;
|
||||||
|
return this.seen[idx(this.world, x, y)] === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
get seenArray() {
|
||||||
|
return this.seen;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(playerPath: Vec2[]) {
|
||||||
|
this.gfx.clear();
|
||||||
|
|
||||||
|
if (!this.world) return;
|
||||||
|
|
||||||
|
// Tiles w/ fog + falloff + silhouettes
|
||||||
|
for (let y = 0; y < this.world.height; y++) {
|
||||||
|
for (let x = 0; x < this.world.width; x++) {
|
||||||
|
const i = idx(this.world, x, y);
|
||||||
|
|
||||||
|
const isSeen = this.seen[i] === 1;
|
||||||
|
const isVis = this.visible[i] === 1;
|
||||||
|
|
||||||
|
if (!isSeen) {
|
||||||
|
this.gfx.fillStyle(0x000000, 1);
|
||||||
|
this.gfx.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wall = isWall(this.world, x, y);
|
||||||
|
const base = wall ? 0x2b2b2b : 0x161616;
|
||||||
|
|
||||||
|
let alpha: number;
|
||||||
|
if (isVis) {
|
||||||
|
const s = this.visibleStrength[i];
|
||||||
|
alpha = Phaser.Math.Clamp(0.35 + s * 0.65, 0.35, 1.0);
|
||||||
|
} else {
|
||||||
|
alpha = wall ? 0.35 : 0.15;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.gfx.fillStyle(base, alpha);
|
||||||
|
this.gfx.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (stairs) if seen
|
||||||
|
{
|
||||||
|
const ex = this.world.exit.x;
|
||||||
|
const ey = this.world.exit.y;
|
||||||
|
const i = idx(this.world, ex, ey);
|
||||||
|
if (this.seen[i] === 1) {
|
||||||
|
const alpha = this.visible[i] === 1 ? 1.0 : 0.35;
|
||||||
|
this.gfx.fillStyle(0xffd166, alpha);
|
||||||
|
this.gfx.fillRect(ex * TILE_SIZE + 7, ey * TILE_SIZE + 7, TILE_SIZE - 14, TILE_SIZE - 14);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path preview (seen only)
|
||||||
|
if (playerPath.length >= 2) {
|
||||||
|
this.gfx.fillStyle(0x3355ff, 0.35);
|
||||||
|
for (const p of playerPath) {
|
||||||
|
// We can check isSeen via internal helper or just local array since we're inside
|
||||||
|
const i = idx(this.world, p.x, p.y);
|
||||||
|
if (this.seen[i] !== 1) continue;
|
||||||
|
this.gfx.fillRect(p.x * TILE_SIZE + 6, p.y * TILE_SIZE + 6, TILE_SIZE - 12, TILE_SIZE - 12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actors (enemies only if visible)
|
||||||
|
for (const a of this.world.actors.values()) {
|
||||||
|
const i = idx(this.world, a.pos.x, a.pos.y);
|
||||||
|
const isVis = this.visible[i] === 1;
|
||||||
|
if (!a.isPlayer && !isVis) continue;
|
||||||
|
|
||||||
|
const color = a.isPlayer ? 0x66ff66 : 0xff6666;
|
||||||
|
this.gfx.fillStyle(color, 1);
|
||||||
|
this.gfx.fillRect(a.pos.x * TILE_SIZE + 4, a.pos.y * TILE_SIZE + 4, TILE_SIZE - 8, TILE_SIZE - 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showDamage(x: number, y: number, amount: number) {
|
||||||
|
const screenX = x * TILE_SIZE + TILE_SIZE / 2;
|
||||||
|
const screenY = y * TILE_SIZE;
|
||||||
|
|
||||||
|
const text = this.scene.add.text(screenX, screenY, amount.toString(), {
|
||||||
|
fontSize: "16px",
|
||||||
|
color: "#ff3333",
|
||||||
|
stroke: "#000",
|
||||||
|
strokeThickness: 2,
|
||||||
|
fontStyle: "bold"
|
||||||
|
}).setOrigin(0.5, 1).setDepth(200);
|
||||||
|
|
||||||
|
this.scene.tweens.add({
|
||||||
|
targets: text,
|
||||||
|
y: screenY - 24,
|
||||||
|
alpha: 0,
|
||||||
|
duration: 800,
|
||||||
|
ease: "Power1",
|
||||||
|
onComplete: () => text.destroy()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
257
src/scenes/GameScene.ts
Normal file
257
src/scenes/GameScene.ts
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
import Phaser from "phaser";
|
||||||
|
import {
|
||||||
|
type EntityId,
|
||||||
|
type Vec2,
|
||||||
|
type Action,
|
||||||
|
type RunState,
|
||||||
|
type World,
|
||||||
|
TILE_SIZE
|
||||||
|
} from "../game/types";
|
||||||
|
import { inBounds, isBlocked, isPlayerOnExit } from "../game/world";
|
||||||
|
import { findPathAStar } from "../game/pathfinding";
|
||||||
|
import { applyAction, stepUntilPlayerTurn } from "../game/simulation";
|
||||||
|
import { makeTestWorld } from "../game/generator";
|
||||||
|
import { DungeonRenderer } from "./DungeonRenderer";
|
||||||
|
|
||||||
|
export class GameScene extends Phaser.Scene {
|
||||||
|
private world!: World;
|
||||||
|
private playerId!: EntityId;
|
||||||
|
|
||||||
|
private levelIndex = 1;
|
||||||
|
|
||||||
|
private runState: RunState = {
|
||||||
|
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2 },
|
||||||
|
inventory: { gold: 0, items: [] }
|
||||||
|
};
|
||||||
|
|
||||||
|
private cursors!: Phaser.Types.Input.Keyboard.CursorKeys;
|
||||||
|
|
||||||
|
private playerPath: Vec2[] = [];
|
||||||
|
private awaitingPlayer = false;
|
||||||
|
|
||||||
|
// Sub-systems
|
||||||
|
private dungeonRenderer!: DungeonRenderer;
|
||||||
|
private isMenuOpen = false;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super("GameScene");
|
||||||
|
}
|
||||||
|
|
||||||
|
create() {
|
||||||
|
this.cursors = this.input.keyboard!.createCursorKeys();
|
||||||
|
|
||||||
|
// Camera
|
||||||
|
this.cameras.main.setZoom(2);
|
||||||
|
|
||||||
|
// Initialize Sub-systems
|
||||||
|
this.dungeonRenderer = new DungeonRenderer(this);
|
||||||
|
|
||||||
|
// Launch UI Scene
|
||||||
|
this.scene.launch("GameUI");
|
||||||
|
|
||||||
|
// Listen for Menu State
|
||||||
|
this.events.on("menu-toggled", (isOpen: boolean) => {
|
||||||
|
this.isMenuOpen = isOpen;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load initial level
|
||||||
|
this.loadLevel(1);
|
||||||
|
|
||||||
|
// Menu Inputs
|
||||||
|
this.input.keyboard?.on("keydown-I", () => {
|
||||||
|
this.events.emit("toggle-menu");
|
||||||
|
// Force update UI in case it opened
|
||||||
|
this.emitUIUpdate();
|
||||||
|
});
|
||||||
|
this.input.keyboard?.on("keydown-ESC", () => {
|
||||||
|
this.events.emit("close-menu");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mouse click -> compute path (only during player turn, and not while menu is open)
|
||||||
|
this.input.on("pointerdown", (p: Phaser.Input.Pointer) => {
|
||||||
|
if (!this.awaitingPlayer) return;
|
||||||
|
if (this.isMenuOpen) return;
|
||||||
|
|
||||||
|
const tx = Math.floor(p.worldX / TILE_SIZE);
|
||||||
|
const ty = Math.floor(p.worldY / TILE_SIZE);
|
||||||
|
|
||||||
|
if (!inBounds(this.world, tx, ty)) return;
|
||||||
|
|
||||||
|
// Exploration rule: cannot click-to-move into unseen tiles
|
||||||
|
if (!this.dungeonRenderer.isSeen(tx, ty)) return;
|
||||||
|
|
||||||
|
// Check if clicking on an enemy
|
||||||
|
const isEnemy = [...this.world.actors.values()].some(a => a.pos.x === tx && a.pos.y === ty && !a.isPlayer);
|
||||||
|
|
||||||
|
const player = this.world.actors.get(this.playerId)!;
|
||||||
|
const path = findPathAStar(
|
||||||
|
this.world,
|
||||||
|
this.dungeonRenderer.seenArray,
|
||||||
|
{ ...player.pos },
|
||||||
|
{ x: tx, y: ty },
|
||||||
|
{ ignoreBlockedTarget: isEnemy }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (path.length >= 2) this.playerPath = path;
|
||||||
|
this.dungeonRenderer.render(this.playerPath);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
if (!this.awaitingPlayer) return;
|
||||||
|
if (this.isMenuOpen) return;
|
||||||
|
|
||||||
|
// Auto-walk one step per turn
|
||||||
|
if (this.playerPath.length >= 2) {
|
||||||
|
const player = this.world.actors.get(this.playerId)!;
|
||||||
|
const next = this.playerPath[1];
|
||||||
|
const dx = next.x - player.pos.x;
|
||||||
|
const dy = next.y - player.pos.y;
|
||||||
|
|
||||||
|
if (Math.abs(dx) + Math.abs(dy) !== 1) {
|
||||||
|
this.playerPath = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBlocked(this.world, next.x, next.y)) {
|
||||||
|
// Check if it's an enemy at 'next'
|
||||||
|
const targetId = [...this.world.actors.values()].find(
|
||||||
|
a => a.pos.x === next.x && a.pos.y === next.y && !a.isPlayer
|
||||||
|
)?.id;
|
||||||
|
|
||||||
|
if (targetId !== undefined) {
|
||||||
|
this.commitPlayerAction({ type: "attack", targetId });
|
||||||
|
this.playerPath = []; // Stop after attack
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// Blocked by something else (friendly?)
|
||||||
|
this.playerPath = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.commitPlayerAction({ type: "move", dx, dy });
|
||||||
|
this.playerPath.shift();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arrow keys
|
||||||
|
let action: Action | null = null;
|
||||||
|
let dx = 0;
|
||||||
|
let dy = 0;
|
||||||
|
|
||||||
|
if (Phaser.Input.Keyboard.JustDown(this.cursors.left!)) dx = -1;
|
||||||
|
else if (Phaser.Input.Keyboard.JustDown(this.cursors.right!)) dx = 1;
|
||||||
|
else if (Phaser.Input.Keyboard.JustDown(this.cursors.up!)) dy = -1;
|
||||||
|
else if (Phaser.Input.Keyboard.JustDown(this.cursors.down!)) dy = 1;
|
||||||
|
|
||||||
|
if (dx !== 0 || dy !== 0) {
|
||||||
|
console.log("Input: ", dx, dy);
|
||||||
|
const player = this.world.actors.get(this.playerId)!;
|
||||||
|
const targetX = player.pos.x + dx;
|
||||||
|
const targetY = player.pos.y + dy;
|
||||||
|
console.log("Target: ", targetX, targetY);
|
||||||
|
|
||||||
|
// Check for enemy at target position
|
||||||
|
const targetId = [...this.world.actors.values()].find(
|
||||||
|
a => a.pos.x === targetX && a.pos.y === targetY && !a.isPlayer
|
||||||
|
)?.id;
|
||||||
|
console.log("Found Target ID:", targetId);
|
||||||
|
|
||||||
|
if (targetId !== undefined) {
|
||||||
|
action = { type: "attack", targetId };
|
||||||
|
} else {
|
||||||
|
action = { type: "move", dx, dy };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action) {
|
||||||
|
this.playerPath = [];
|
||||||
|
this.commitPlayerAction(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitUIUpdate() {
|
||||||
|
this.events.emit("update-ui", {
|
||||||
|
world: this.world,
|
||||||
|
playerId: this.playerId,
|
||||||
|
levelIndex: this.levelIndex
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private commitPlayerAction(action: Action) {
|
||||||
|
this.awaitingPlayer = false;
|
||||||
|
|
||||||
|
const playerEvents = applyAction(this.world, this.playerId, action);
|
||||||
|
const enemyStep = stepUntilPlayerTurn(this.world, this.playerId);
|
||||||
|
this.awaitingPlayer = enemyStep.awaitingPlayerId === this.playerId;
|
||||||
|
|
||||||
|
// Process events for visual fx
|
||||||
|
const allEvents = [...playerEvents, ...enemyStep.events];
|
||||||
|
if (allEvents.length > 0) console.log("Events:", allEvents);
|
||||||
|
for (const ev of allEvents) {
|
||||||
|
if (ev.type === "damaged") {
|
||||||
|
console.log("Showing damage:", ev.amount, "at", ev.x, ev.y);
|
||||||
|
this.dungeonRenderer.showDamage(ev.x, ev.y, ev.amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Level transition
|
||||||
|
if (isPlayerOnExit(this.world, this.playerId)) {
|
||||||
|
this.syncRunStateFromPlayer();
|
||||||
|
this.loadLevel(this.levelIndex + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dungeonRenderer.computeFov(this.playerId);
|
||||||
|
this.centerCameraOnPlayer();
|
||||||
|
this.dungeonRenderer.render(this.playerPath);
|
||||||
|
this.emitUIUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadLevel(level: number) {
|
||||||
|
this.levelIndex = level;
|
||||||
|
|
||||||
|
const { world, playerId } = makeTestWorld(level, this.runState);
|
||||||
|
this.world = world;
|
||||||
|
this.playerId = playerId;
|
||||||
|
|
||||||
|
// Reset transient state
|
||||||
|
this.playerPath = [];
|
||||||
|
this.awaitingPlayer = false;
|
||||||
|
|
||||||
|
// Camera bounds for this level
|
||||||
|
this.cameras.main.setBounds(0, 0, this.world.width * TILE_SIZE, this.world.height * TILE_SIZE);
|
||||||
|
|
||||||
|
// Initialize Renderer for new level
|
||||||
|
this.dungeonRenderer.initializeLevel(this.world);
|
||||||
|
|
||||||
|
// Step until player turn
|
||||||
|
const enemyStep = stepUntilPlayerTurn(this.world, this.playerId);
|
||||||
|
this.awaitingPlayer = enemyStep.awaitingPlayerId === this.playerId;
|
||||||
|
|
||||||
|
this.dungeonRenderer.computeFov(this.playerId);
|
||||||
|
this.centerCameraOnPlayer();
|
||||||
|
this.dungeonRenderer.render(this.playerPath);
|
||||||
|
this.emitUIUpdate();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private syncRunStateFromPlayer() {
|
||||||
|
const p = this.world.actors.get(this.playerId);
|
||||||
|
if (!p?.stats || !p.inventory) return;
|
||||||
|
|
||||||
|
this.runState = {
|
||||||
|
stats: { ...p.stats },
|
||||||
|
inventory: { gold: p.inventory.gold, items: [...p.inventory.items] }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private centerCameraOnPlayer() {
|
||||||
|
const player = this.world.actors.get(this.playerId)!;
|
||||||
|
this.cameras.main.centerOn(
|
||||||
|
player.pos.x * TILE_SIZE + TILE_SIZE / 2,
|
||||||
|
player.pos.y * TILE_SIZE + TILE_SIZE / 2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
173
src/scenes/GameUI.ts
Normal file
173
src/scenes/GameUI.ts
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import Phaser from "phaser";
|
||||||
|
import { type World, type EntityId } from "../game/types";
|
||||||
|
|
||||||
|
export default class GameUI extends Phaser.Scene {
|
||||||
|
// HUD
|
||||||
|
private levelText!: Phaser.GameObjects.Text;
|
||||||
|
private healthBar!: Phaser.GameObjects.Graphics;
|
||||||
|
|
||||||
|
// Menu
|
||||||
|
private menuOpen = false;
|
||||||
|
private menuContainer!: Phaser.GameObjects.Container;
|
||||||
|
private menuText!: Phaser.GameObjects.Text;
|
||||||
|
private menuBg!: Phaser.GameObjects.Rectangle;
|
||||||
|
private menuButton!: Phaser.GameObjects.Container;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super({ key: "GameUI" });
|
||||||
|
}
|
||||||
|
|
||||||
|
create() {
|
||||||
|
this.createHud();
|
||||||
|
this.createMenu();
|
||||||
|
|
||||||
|
// Listen for updates from GameScene
|
||||||
|
const gameScene = this.scene.get("GameScene");
|
||||||
|
gameScene.events.on("update-ui", (data: { world: World; playerId: EntityId; levelIndex: number }) => {
|
||||||
|
this.updateUI(data.world, data.playerId, data.levelIndex);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also listen for toggle request if needed, or stick to inputs in GameScene?
|
||||||
|
// GameScene handles Input 'I' -> calls events.emit('toggle-menu')?
|
||||||
|
// Or GameUI handles input?
|
||||||
|
// Let's keep input in GameScene for now to avoid conflicts, or move 'I' here.
|
||||||
|
// If 'I' is pressed, GameScene might need to know if menu is open (to pause).
|
||||||
|
gameScene.events.on("toggle-menu", () => this.toggleMenu());
|
||||||
|
gameScene.events.on("close-menu", () => this.setMenuOpen(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
private createHud() {
|
||||||
|
this.levelText = this.add.text(10, 10, "Level 1", {
|
||||||
|
fontSize: "20px",
|
||||||
|
color: "#ffffff",
|
||||||
|
fontStyle: "bold"
|
||||||
|
}).setDepth(100);
|
||||||
|
|
||||||
|
this.healthBar = this.add.graphics().setDepth(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
private createMenu() {
|
||||||
|
const cam = this.cameras.main;
|
||||||
|
|
||||||
|
// Button (top-right)
|
||||||
|
const btnW = 90;
|
||||||
|
const btnH = 28;
|
||||||
|
|
||||||
|
const btnBg = this.add.rectangle(0, 0, btnW, btnH, 0x000000, 0.6).setStrokeStyle(1, 0xffffff, 0.8);
|
||||||
|
const btnLabel = this.add.text(0, 0, "Menu", { fontSize: "14px", color: "#ffffff" }).setOrigin(0.5);
|
||||||
|
|
||||||
|
this.menuButton = this.add.container(0, 0, [btnBg, btnLabel]);
|
||||||
|
this.menuButton.setDepth(1000);
|
||||||
|
|
||||||
|
const placeButton = () => {
|
||||||
|
this.menuButton.setPosition(cam.width - btnW / 2 - 10, btnH / 2 + 10);
|
||||||
|
};
|
||||||
|
placeButton();
|
||||||
|
this.scale.on("resize", placeButton); // Use scale manager resize
|
||||||
|
|
||||||
|
btnBg.setInteractive({ useHandCursor: true }).on("pointerdown", () => this.toggleMenu());
|
||||||
|
|
||||||
|
// Panel (center)
|
||||||
|
const panelW = 340;
|
||||||
|
const panelH = 220;
|
||||||
|
|
||||||
|
this.menuBg = this.add
|
||||||
|
.rectangle(0, 0, panelW, panelH, 0x000000, 0.8)
|
||||||
|
.setStrokeStyle(1, 0xffffff, 0.9)
|
||||||
|
.setInteractive(); // capture clicks
|
||||||
|
|
||||||
|
this.menuText = this.add
|
||||||
|
.text(-panelW / 2 + 14, -panelH / 2 + 12, "", {
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "#ffffff",
|
||||||
|
wordWrap: { width: panelW - 28 }
|
||||||
|
})
|
||||||
|
.setOrigin(0, 0);
|
||||||
|
|
||||||
|
this.menuContainer = this.add.container(0, 0, [this.menuBg, this.menuText]);
|
||||||
|
this.menuContainer.setDepth(1001);
|
||||||
|
|
||||||
|
const placePanel = () => {
|
||||||
|
this.menuContainer.setPosition(cam.width / 2, cam.height / 2);
|
||||||
|
};
|
||||||
|
placePanel();
|
||||||
|
this.scale.on("resize", placePanel);
|
||||||
|
|
||||||
|
this.setMenuOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private toggleMenu() {
|
||||||
|
this.setMenuOpen(!this.menuOpen);
|
||||||
|
}
|
||||||
|
|
||||||
|
private setMenuOpen(open: boolean) {
|
||||||
|
this.menuOpen = open;
|
||||||
|
this.menuContainer.setVisible(open);
|
||||||
|
|
||||||
|
// Notify GameScene back?
|
||||||
|
const gameScene = this.scene.get("GameScene");
|
||||||
|
gameScene.events.emit("menu-toggled", open);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateUI(world: World, playerId: EntityId, levelIndex: number) {
|
||||||
|
this.updateHud(world, playerId, levelIndex);
|
||||||
|
if (this.menuOpen) {
|
||||||
|
this.updateMenuText(world, playerId, levelIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateHud(world: World, playerId: EntityId, levelIndex: number) {
|
||||||
|
this.levelText.setText(`Level ${levelIndex}`);
|
||||||
|
|
||||||
|
const p = world.actors.get(playerId);
|
||||||
|
if (!p || !p.stats) return;
|
||||||
|
|
||||||
|
const barX = 10;
|
||||||
|
const barY = 40;
|
||||||
|
const barW = 200;
|
||||||
|
const barH = 16;
|
||||||
|
|
||||||
|
this.healthBar.clear();
|
||||||
|
this.healthBar.fillStyle(0x444444, 1);
|
||||||
|
this.healthBar.fillRect(barX, barY, barW, barH);
|
||||||
|
|
||||||
|
const hp = Math.max(0, p.stats.hp);
|
||||||
|
const maxHp = Math.max(1, p.stats.maxHp);
|
||||||
|
const pct = Phaser.Math.Clamp(hp / maxHp, 0, 1);
|
||||||
|
const fillW = Math.floor(barW * pct);
|
||||||
|
|
||||||
|
this.healthBar.fillStyle(0xff0000, 1);
|
||||||
|
this.healthBar.fillRect(barX, barY, fillW, barH);
|
||||||
|
|
||||||
|
this.healthBar.lineStyle(2, 0xffffff, 1);
|
||||||
|
this.healthBar.strokeRect(barX, barY, barW, barH);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateMenuText(world: World, playerId: EntityId, levelIndex: number) {
|
||||||
|
const p = world.actors.get(playerId);
|
||||||
|
const stats = p?.stats;
|
||||||
|
const inv = p?.inventory;
|
||||||
|
|
||||||
|
const lines: string[] = [];
|
||||||
|
lines.push(`Level ${levelIndex}`);
|
||||||
|
lines.push("");
|
||||||
|
lines.push("Stats");
|
||||||
|
lines.push(` HP: ${stats?.hp ?? 0}/${stats?.maxHp ?? 0}`);
|
||||||
|
lines.push(` Attack: ${stats?.attack ?? 0}`);
|
||||||
|
lines.push(` Defense: ${stats?.defense ?? 0}`);
|
||||||
|
lines.push(` Speed: ${p?.speed ?? 0}`);
|
||||||
|
lines.push("");
|
||||||
|
lines.push("Inventory");
|
||||||
|
lines.push(` Gold: ${inv?.gold ?? 0}`);
|
||||||
|
lines.push(` Items: ${(inv?.items?.length ?? 0) === 0 ? "(none)" : ""}`);
|
||||||
|
|
||||||
|
if (inv?.items?.length) {
|
||||||
|
for (const it of inv.items) lines.push(` - ${it}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push("");
|
||||||
|
lines.push("Hotkeys: I to toggle, Esc to close");
|
||||||
|
|
||||||
|
this.menuText.setText(lines.join("\n"));
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/scenes/SplashScene.ts
Normal file
41
src/scenes/SplashScene.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import Phaser from "phaser";
|
||||||
|
import { Scene } from 'phaser';
|
||||||
|
|
||||||
|
export class SplashScene extends Scene {
|
||||||
|
constructor() {
|
||||||
|
super("SplashScene");
|
||||||
|
}
|
||||||
|
|
||||||
|
preload() {
|
||||||
|
this.load.image('splash', 'splash_bg.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
create() {
|
||||||
|
const { width, height } = this.scale;
|
||||||
|
|
||||||
|
// Background (Placeholder for Image)
|
||||||
|
// If we successfully load the image 'splash', we use it.
|
||||||
|
if (this.textures.exists('splash')) {
|
||||||
|
this.add.image(width / 2, height / 2, 'splash').setDisplaySize(width, height);
|
||||||
|
} else {
|
||||||
|
this.add.rectangle(0, 0, width, height, 0x110022).setOrigin(0);
|
||||||
|
this.add.text(width/2, height/2, "ROGUE LEGACY", {
|
||||||
|
fontSize: "48px",
|
||||||
|
color: "#ffffff",
|
||||||
|
fontStyle: "bold"
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fade In
|
||||||
|
this.cameras.main.fadeIn(1000, 0, 0, 0);
|
||||||
|
|
||||||
|
// Fade Out after delay
|
||||||
|
this.time.delayedCall(2500, () => {
|
||||||
|
this.cameras.main.fadeOut(1000, 0, 0, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.cameras.main.once(Phaser.Cameras.Scene2D.Events.FADE_OUT_COMPLETE, () => {
|
||||||
|
this.scene.start("StartScene");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/scenes/StartScene.ts
Normal file
52
src/scenes/StartScene.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import Phaser from "phaser";
|
||||||
|
|
||||||
|
export class StartScene extends Phaser.Scene {
|
||||||
|
constructor() {
|
||||||
|
super("StartScene");
|
||||||
|
}
|
||||||
|
|
||||||
|
create() {
|
||||||
|
const { width, height } = this.scale;
|
||||||
|
|
||||||
|
this.cameras.main.fadeIn(500, 0, 0, 0);
|
||||||
|
|
||||||
|
// Title
|
||||||
|
this.add.text(width / 2, height * 0.3, "ROGUE", {
|
||||||
|
fontSize: "64px",
|
||||||
|
color: "#ff0044",
|
||||||
|
fontStyle: "bold",
|
||||||
|
stroke: "#ffffff",
|
||||||
|
strokeThickness: 4
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
const startBtn = this.createButton(width / 2, height * 0.55, "Start Game");
|
||||||
|
const optBtn = this.createButton(width / 2, height * 0.65, "Options");
|
||||||
|
|
||||||
|
startBtn.on("pointerdown", () => {
|
||||||
|
this.scene.start("GameScene");
|
||||||
|
});
|
||||||
|
|
||||||
|
optBtn.on("pointerdown", () => {
|
||||||
|
console.log("Options clicked");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private createButton(x: number, y: number, text: string) {
|
||||||
|
const bg = this.add.rectangle(0, 0, 200, 50, 0x333333).setStrokeStyle(2, 0xffffff);
|
||||||
|
const txt = this.add.text(0, 0, text, { fontSize: "24px", color: "#ffffff" }).setOrigin(0.5);
|
||||||
|
|
||||||
|
const container = this.add.container(x, y, [bg, txt]);
|
||||||
|
container.setSize(200, 50);
|
||||||
|
container.setInteractive({ useHandCursor: true });
|
||||||
|
|
||||||
|
container.on("pointerover", () => {
|
||||||
|
bg.setFillStyle(0x555555);
|
||||||
|
});
|
||||||
|
container.on("pointerout", () => {
|
||||||
|
bg.setFillStyle(0x333333);
|
||||||
|
});
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
tsconfig.json
Normal file
26
tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user