From eb924a01f971071f1fd012f1dd844a2146c8c371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20THOMAS?= <74055963+SebastienThomasDEV@users.noreply.github.com> Date: Tue, 7 Nov 2023 21:34:35 +0100 Subject: [PATCH 01/14] refactor in ts currently messy --- .gitignore | 23 + index.html | 162 ++-- package-lock.json | 813 ++++++++++++++++++ package.json | 15 + public/vite.svg | 1 + src/entities/Archer.ts | 5 + src/entities/Player.ts | 14 + src/entities/Warrior.ts | 18 + src/main.ts | 6 + src/model/Entity.ts | 36 + src/sprites/character/Character_RollDown.png | Bin 0 -> 1184 bytes .../character/Character_RollDownLeft.png | Bin 0 -> 1307 bytes .../character/Character_RollDownRight.png | Bin 0 -> 1272 bytes src/sprites/character/Character_RollLeft.png | Bin 0 -> 1114 bytes src/sprites/character/Character_RollRight.png | Bin 0 -> 1076 bytes src/sprites/character/Character_RollUp.png | Bin 0 -> 1135 bytes .../character/Character_RollUpLeft.png | Bin 0 -> 1152 bytes .../character/Character_RollUpRight.png | Bin 0 -> 1108 bytes .../character/Character_SlashDownLeft.png | Bin 0 -> 864 bytes .../character/Character_SlashDownRight.png | Bin 0 -> 881 bytes .../character/Character_SlashUpLeft.png | Bin 0 -> 934 bytes .../character/Character_SlashUpRight.png | Bin 0 -> 895 bytes .../character/Weapon/Sword_DownLeft.png | Bin 0 -> 591 bytes .../character/Weapon/Sword_DownRight.png | Bin 0 -> 477 bytes src/sprites/character/Weapon/Sword_UpLeft.png | Bin 0 -> 540 bytes .../character/Weapon/Sword_UpRight.png | Bin 0 -> 580 bytes src/sprites/character/down/Character_Down.png | Bin 0 -> 917 bytes .../character/down/Character_DownLeft.png | Bin 0 -> 797 bytes .../character/down/Character_DownRight.png | Bin 0 -> 842 bytes src/sprites/character/left/Character_Left.png | Bin 0 -> 762 bytes .../character/right/Character_Right.png | Bin 0 -> 787 bytes src/sprites/character/up/Character_Up.png | Bin 0 -> 905 bytes src/sprites/character/up/Character_UpLeft.png | Bin 0 -> 818 bytes .../character/up/Character_UpRight.png | Bin 0 -> 821 bytes src/vendor/Game.ts | 26 + src/vendor/Runtime.ts | 36 + src/vendor/State.ts | 82 ++ src/vendor/Ui.ts | 14 + src/vite-env.d.ts | 1 + tsconfig.json | 25 + 40 files changed, 1196 insertions(+), 81 deletions(-) create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/vite.svg create mode 100644 src/entities/Archer.ts create mode 100644 src/entities/Player.ts create mode 100644 src/entities/Warrior.ts create mode 100644 src/main.ts create mode 100644 src/model/Entity.ts create mode 100644 src/sprites/character/Character_RollDown.png create mode 100644 src/sprites/character/Character_RollDownLeft.png create mode 100644 src/sprites/character/Character_RollDownRight.png create mode 100644 src/sprites/character/Character_RollLeft.png create mode 100644 src/sprites/character/Character_RollRight.png create mode 100644 src/sprites/character/Character_RollUp.png create mode 100644 src/sprites/character/Character_RollUpLeft.png create mode 100644 src/sprites/character/Character_RollUpRight.png create mode 100644 src/sprites/character/Character_SlashDownLeft.png create mode 100644 src/sprites/character/Character_SlashDownRight.png create mode 100644 src/sprites/character/Character_SlashUpLeft.png create mode 100644 src/sprites/character/Character_SlashUpRight.png create mode 100644 src/sprites/character/Weapon/Sword_DownLeft.png create mode 100644 src/sprites/character/Weapon/Sword_DownRight.png create mode 100644 src/sprites/character/Weapon/Sword_UpLeft.png create mode 100644 src/sprites/character/Weapon/Sword_UpRight.png create mode 100644 src/sprites/character/down/Character_Down.png create mode 100644 src/sprites/character/down/Character_DownLeft.png create mode 100644 src/sprites/character/down/Character_DownRight.png create mode 100644 src/sprites/character/left/Character_Left.png create mode 100644 src/sprites/character/right/Character_Right.png create mode 100644 src/sprites/character/up/Character_Up.png create mode 100644 src/sprites/character/up/Character_UpLeft.png create mode 100644 src/sprites/character/up/Character_UpRight.png create mode 100644 src/vendor/Game.ts create mode 100644 src/vendor/Runtime.ts create mode 100644 src/vendor/State.ts create mode 100644 src/vendor/Ui.ts create mode 100644 src/vite-env.d.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index 485dee6..a547bf3 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +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? diff --git a/index.html b/index.html index 0d2ce3f..bec9790 100644 --- a/index.html +++ b/index.html @@ -1,84 +1,84 @@ - - - - - - - - - - - Document - - -
-
- -

- CastleStorm & © 2023,
- 411 Inc Ltd. All rights reserved. -

-
- -
- - + + + + + + + + + + + Document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..dc97453 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,813 @@ +{ + "name": "castlestorm", + "version": "0.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "castlestorm", + "version": "0.0.0", + "devDependencies": { + "typescript": "^5.0.2", + "vite": "^4.4.5" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", + "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==", + "dev": true, + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + } + }, + "dependencies": { + "@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "dev": true, + "optional": true + }, + "esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "requires": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, + "nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "requires": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true + }, + "typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true + }, + "vite": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", + "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==", + "dev": true, + "requires": { + "esbuild": "^0.18.10", + "fsevents": "~2.3.2", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..59551a0 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "castlestorm", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "devDependencies": { + "typescript": "^5.0.2", + "vite": "^4.4.5" + } +} diff --git a/public/vite.svg b/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/entities/Archer.ts b/src/entities/Archer.ts new file mode 100644 index 0000000..af04902 --- /dev/null +++ b/src/entities/Archer.ts @@ -0,0 +1,5 @@ +import Entity from "../model/Entity"; + +export default class Archer extends Entity { + +} \ No newline at end of file diff --git a/src/entities/Player.ts b/src/entities/Player.ts new file mode 100644 index 0000000..f29787d --- /dev/null +++ b/src/entities/Player.ts @@ -0,0 +1,14 @@ +import Entity from "../model/Entity"; +import State from "../vendor/State"; + +export default class Player extends Entity { + constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { + super(x, y, 10); + this.state = state; + this.draw = this.draw.bind(this); + } + public draw(): void { + this.context.fillStyle = 'rgb(0, 0, 0)'; + this.context.fillRect(this.x, this.y, 10, 10); + } +} \ No newline at end of file diff --git a/src/entities/Warrior.ts b/src/entities/Warrior.ts new file mode 100644 index 0000000..34789a5 --- /dev/null +++ b/src/entities/Warrior.ts @@ -0,0 +1,18 @@ +import Entity from "../model/Entity"; +import State from "../vendor/State"; + +export default class Warrior extends Entity { + context: CanvasRenderingContext2D; + canvas: HTMLCanvasElement; + state: State; + + constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { + super(x, y, 10); + this.x = x; + this.y = y; + this.context = context; + this.canvas = canvas; + this.state = state; + this.draw = this.draw.bind(this); + } +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..ab18d6b --- /dev/null +++ b/src/main.ts @@ -0,0 +1,6 @@ +import {Game} from "./vendor/Game"; + +window.onload = () => { + const game = new Game(); + game.start(); +} \ No newline at end of file diff --git a/src/model/Entity.ts b/src/model/Entity.ts new file mode 100644 index 0000000..15931cb --- /dev/null +++ b/src/model/Entity.ts @@ -0,0 +1,36 @@ +import State from "../vendor/State"; + +export default class Entity { + + public x: number; + public y: number; + public radius: number; + context: CanvasRenderingContext2D; + canvas: HTMLCanvasElement; + state: State; + + constructor(x: number, y: number, radius: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { + this.x = x; + this.y = y; + this.radius = radius; + this.context = context; + this.canvas = canvas; + this.state = state; + } + + public draw(): void { + throw new Error("Method not implemented."); + } + + public update(): void { + throw new Error("Method not implemented."); + } + + public move(): void { + throw new Error("Method not implemented."); + } + + public collision(): void { + throw new Error("Method not implemented."); + } +} \ No newline at end of file diff --git a/src/sprites/character/Character_RollDown.png b/src/sprites/character/Character_RollDown.png new file mode 100644 index 0000000000000000000000000000000000000000..2422b399d8cae8b225adb5a9cc4ec2ad9af28f47 GIT binary patch literal 1184 zcmV;R1Yi4!P)x$4{3$390vmscW~z``S&C#% z$iV}F4vqsuOVj`fQdo<8!8jpdNWzR?{1caZP^*xcP>09f0$Z!8bWp-vm0D9z|U4vn^> zZei|j0RTeR!+g0U5wwp+5}xw74*xm>bR7psJGyP#NJkz6z|wo#;yzit&j=3y z6EqRT(aHqry(f$JT5`F}fD<(75pF`+_*UIyx{iaB#e%hM+nO(zb(^UJ3tbNY5X2FH z)d&Rc#?{D>C~NV(B9nfR3wq$GNqn^yAf9SdELi_mo( z>twO0(p+5_24i441_tBoo#`0RU;w11t0+EH4UVb?4B`lPHa4ucA3l_}UA42g=rrnx zAdc2y0>01@&DZPo002pn&~4Qwkm`IwLlvlHKp8!1tpW1l)3uYuLhC#%#-`2)U5^V- zM$m;Y*On5*5!$wWZSl)?7h+CBoMDJFgs#W+C~IHZ07Z$A2%;FeOhEDJ3j)C0-Qu5< zBoQls7J@h`Gyz?kpqaU^qWsU7OTOmG$1eITR)MbLlp0Xl0AeD*Me%i-pzAo9`1w*O zx*jMWHFXH$2(yFX1ryMmn=ikr=sJ!?Z5A0Yh$FP)(@cD-J~Gp>REcWa_LTwD2`@#zBqK%+mx{oNg<=A)^kHRxpGQ|xbk z`IQ@io{^>00JQx8t`?u5XH+eM@>7otK^&pCxv6v~@aX86)r4P~`6u7toFGBSeNSuCT45;fM2I&;=W;#P_ z(7~JOEK_#rT5HfDp_Im+9Srf~?=JzYw!2huh(>?J4};qAsdRjq3V=Hs8@w?Y5oA}O zGSpc~tBgqp7wUWLUjD@)f>ij>_3-!I?{Ro| z2-V^Pz~krza4sg>+uK{kkE4|dU;oBUz$?^4xwh++s`ImW0l|ej&f+uY;g_+lLJB&>utrS(8w>`6!C){LD)SFBPn6!!IN8qt0000 literal 0 HcmV?d00001 diff --git a/src/sprites/character/Character_RollDownLeft.png b/src/sprites/character/Character_RollDownLeft.png new file mode 100644 index 0000000000000000000000000000000000000000..3d303be9327aa4e57d015dadc51d67a3a31e5917 GIT binary patch literal 1307 zcmV+$1?2jPP)kydAe+J(l1;_SAPde|M9}RX#KxP4c#8kTlu)wGKVeSh3VE!GdB8II$Xaaxz{6xhc|Ld(`sg1!&yJTnDA!AWg20Yn^@N09r}bORT|Ap=|ovR}Ly*-JPDKtB2F%? zr26JZ!2T|MWMOSEU|*0;CKYdtK!9ZcaJP!x!C;ZNW6Af6Z&9W?09BV!1`LWnwqNzM z4q|^7TJPzE6U4C`P$D~(GOAXK*FoY=Ktw3e=4M{!3A!_wDuC`_z^+?wu7f(Z$d)8% zY_d9WGPK@P*9m}FmV<3$1Mm)ca*ZZBl`=Y&GO|{i|0O6fRjY+;GP#f=3eLmpuUUi< z-c$jI#YYgLP`t=A=b>vg-GFV|{QIia!atiIMAZe(LF<%V9qsYN=Ne7q8ci_9kS^u; zeOaqL<_L?}zrT&^oaZLt6;(Bm@TLmDUwqsI*;AOnK?ou|as%78-K79Py7@- z!XBJM4sdm}2SstibSh;uv4*RoJpkd75R|vS{$|2?2ohe{t>9Dv(E5|L+BYUcRNcp^ z=!>r~*tU&CBB20K&d<*yX_JaD(=@TNwS!!v2>|?8uW1@qws!Ev&%dH-wXm|a1LK`_ z1wdqT@!nf^006&z^(E4!9MYxSnDb<o}SQ0Qm0K(Cu@Cs1EjUB>btIK8XB9Q09HLz@M(lyJ9CXD9m-g*PJa8G0NbJXi~JXUWjht6fB@riUPhYmcEHzu!Znuce#&9;pV z<)C#MxZN;narr?!zdVtV6L`A~ z0Q~%+P$)#U?D*L>HgM27<#h+!w z6=C8tci*%3r96V=#b9Zdj~@VonvFfSz-a!xJ|q$eU@{l+b-+LQh%xzu{~0{JqB?U3 zO!~p7AInWVF+TrpfADDi-l$)3=0Z%w(kEXsHvjTp&ms8;&ilO2^PTVeV+u$l z5{X12kw_#GiDU}9sT2S(I)1NtPD1OPUCmBa}-fUD<5M1UJ| z1S-C(=O~82ycTsZ|3xKI;hUbv%DTbNo3@3rZot)ZDC-8Ap0`jD79pdmc#??yCtN%u}zsv?MHp-Bb!xcvsJ%fHwcGtQ#nxjXKgWZ427^y0VOT05G@H z84+VyH*neO1$LuQ0pC{>c+q*q0KnCAJP|^$O4IYgH1ThMQK4r+eg9(fw(=L=z_V5> z@<2o}it$j}jLQ+Agd&>9y@s#}CR4TT9Pbz)^ZGOIhHqVEjzP}!2AR=YmfN5I{A%_`)ga-^JUsQaty#m_6 zPz3ZoObi)Sh1==O{hc`UWmNUXhCOnkAbpQ)6)+A7B#L#i=sI-gYpo~1;}QvNq+RiD z2ouBjL`#Gh+&+hCTevtX(BCZ7)+}blAJ&Ni34j8bclQ%z-GJNagdPvY<(GNu!Jzm< z$nvJe6efm@swz}yG0_?O92Z9g{(5m#K(m~sEkBcBCh4B1*8*;<_oq2H+B#5#?1 z61ba6O%-BTCkiA03WRKC7UEuL?WR$ zjuZBBff(ATV!XShtQ;J}v@Lx3(=S*#IEJQa00wRV0NGj{D+k9Y7K@QDgDDPRREXq6 z8~H3Y@>%@)^;ZCZ4{pEh>$-u9lT0Sj?OyHniAzBeglSv2v%0EW_Ig1t?*~SA+&mYc zVJM@jN>r0GvHv6h$8n%(8us`1QLR?_(ByYBCI}zCcMm`Q{2K!XLJ;}$@dY1_hK@A1 z?ZC^xZnw+Cj*E5#T)mH}_;|Q=SIO4uf$pp*XXf2*7Z@aVB!YdO- zV-F|RDbN4P!SS`2OubW(l8YF zrVIeAt*t4JT=4!q+`2nBGZf)X=8X0Wg)hxCMVrND~2ZFiuk~0>yC0OGDIRS2NaD1s$g; zu$96*M3YT->6MJpjwK;B`1Q&DuCk*FJG> z2jImIh=YVQMKB&bk3Wsy)(*Ne`_BP@CbJn0h=YJQTnLqSBdJ4*QFab?qft8#N9bd( zuofeoNTPVb>V`PTu#xsxIbWU_dX!bRxa3vGv@^FN1rksl2c|5 zvKb$$$2Q$K_rOhp^1uL;ECq5d?EC-#+qMx!(cK4048gx2lrKrAhX=U&{DsbkJ=EJ> zB&3=7ecN_iz(!`Y!pTOmv=K_NZTwY$H)oLkm|Cw_Fu)Fa6&QT_}MK)PJ zS20dgAp(FIjn$zHBNhsP-(c|o=ivyHQc>_X{wr~;D)-AdaO8=NJr`Cu0dQXs0JZDu z>#|b;0K6Z4qVr)705BcI;O|v4PE)+Lp4F^yg4lC2b=cL2g#zHJv%(3^!;!ey`LKsK z{wqueF^=B$#qr;nTgzsZ!}dEzh&?xxM-}jx(?ML=l2^6rZFrY_rLpG$ZGNWI;~nRB9~^|x9r zfVKYVAO-*=q?r-kV!_Mqil>JMH7+_{heabc8Vxm1Kc|NWfb^UjdoGPVmnNi16Vjx< z?`L+YN`wZ)5pm=Z5J%*qAnsBgzVB0XF2r^EzE5r2raVVESN^%|B>wpQDX_5Tz~f0Z zP!63BdvreRi6m^>7L#L@xD@a$&^!_FJOEH8P?g*jwR3SlMV09v%De%#vh#sB~S07*qoM6N<$g1XrMx&QzG literal 0 HcmV?d00001 diff --git a/src/sprites/character/Character_RollRight.png b/src/sprites/character/Character_RollRight.png new file mode 100644 index 0000000000000000000000000000000000000000..e45ea115ccf61a814fa75846297e27123aca517f GIT binary patch literal 1076 zcmV-41k3x0P))MHWhc)VZP%ca%%8!4*>iX05f$x@8uOKtSUOua~fGYf>t0F z<~yz^&IwQf0JKL#3R=6KObSRPF?Ai$bjspmEt*cHUHf-NEV@PC$#W0)2v8yKH?D84 z4)9*zOMOoaBywuJJ;~S?Q1(aDDf1l{zT;9NFjU_bzJ3;MaqPOoZL|3H>(`lzd+QEB zSr|FB%>T1ZMsCve9hY|&G&+wT=yw1>qwXNH>xkMS6vw%HqJ0^Dm7=wQ)UGQ+erNF@ z&w{MF_ecpQi*EbnKaXeTiw@hiF&>ZkX-X0qNwBW%nxk9H23_o*UEuWSP`mo}y^z*A zD6z8^Ue==9`W9g7x<;8Fpu*b^y^6;agsJONs`3B7Kg(Xr}|Ms*c{#R_p@|}tkmmu z?eg*x=ywn{J%mjU0Qln5qAxy;$hc3Z&4bQY8y1~n=k7^iy;ZC$Jo261-=_QJ+5{dO z1`h@RKDRz{Va6ERqahDa*r*la=H>>7+Zh=?9*=n<0Lu#{OM4x&K^J%ZkRN{;el10B zUEz`MVbg;-y2U5+P3Df9D*>)26YTBnX#imL0^IdOd~SWjY|sTTH=;cn@~q6#inhh- z1qhoSsts#ng%%~P?FOjet{?JP$|kMw$oFc)LfG{1U-Tn$66Q*PgM$M+JUlSE2V1=W z`)3ybfQPF~UJ;mAtkmxS&fOD@w5kn@>z0MsdI>QhtX=@ywgEt#H>=m{(qoBz)U1l3 ziG>F|`fl|CoE{xYFE|kffZ4XqN7(dO*z^+5$frk#WySyiT+?@47CE)0;X5wHQ&@{M z=$iCO9)RZF=&)9+B};c@EGu>6@$S!8CCP$ss2CcZg!pXGWwSxI%tHn?^Fy7cCg#;r?58yXMOun*z u8(8w+FvY*0o%afbLZMJ76bgkR55E9;hny{scUN%$00008?R=PNCQ0KoQLKtEH73xH4xr^(&)jmJ_7&oOupR^PR`HixJ;=-Y$5OFKe>94fbFV)d z-AarmaU%!;sCOa&QsYR>`8U6pW z=^v!x`=b%6GwPiv2JXZGU!VWUgTMa1QN|=7@<1xT%3%IYK`4H(8iW#jYFUd_sto7{ z>$F(S?0;>cstBog)fp{TfqYv*xcP+W=;O5`ws*(J_O5!57ae`HYFC1OSpZBbB9Cl1 zWn1i|^+>si|4kw_Zj2*5iMq5i)F3c?_EKJkPdSK=B@&gf~)lRyKT}PnVa;vs8cEu&*@+n54j|R$pZ>GYg1- zJo>_<)iz<$A+P*sQ)X6|nFK}INi_ppd^ZLJKf^24i z*IS#On(~F-PL->}J)~<`Yqog3wP`KzO2wjAHym6xf{^a{xvdP4*w<Dgy+ML{EFqa+(+pvE!940|n z??elOK>B|Z4O#&p0!$*%rV|)1v?Tq%WTPsBBnTuR3YD7GJ{YcB;WFm~$t=L;EJ%V- zGT$N{y`8F&2Hh@T>U^GnJg0(cMXOzp1c8*9_SZV7tq^X$zqCBLp4Wny%yd)@>7(4E zt3kU9n3o080`h19!n|paIxtnQ+Z=<4L)A1}>YOI$<#L&Fxopi|nsD=pSE$qy+v;FX z^?jjI*7J zbVF2w@SlG_Xur4dQm_ahm1KtR{BBh-x;os}@ibz|teY(~L&8~EMuOANCsTr$wg z%8GY!aiR50Be8i1&_#k^)=Ff0UA)ve&22wI-4iJliyn}4HFvZV2$NY6aJ?S_vL|9x zr&*)bngA2j08|4(9hgn-^WF`=(M=JM6fCR$5{4~npa5x+ehr{8>%6|jzey<8-p(FqR002ovPDHLkV1oYB BBAEaH literal 0 HcmV?d00001 diff --git a/src/sprites/character/Character_RollUpLeft.png b/src/sprites/character/Character_RollUpLeft.png new file mode 100644 index 0000000000000000000000000000000000000000..ff131d0e7e4f21ac3ed9e585626ecc232f85f737 GIT binary patch literal 1152 zcmV-`1b_R9P)Nkls)YLl-4QU34ti7x6@y-hY*YOSEy5wE>06H zH!d8h2u7VFbBMsn2FKX9jgS?RYEnqE+Fk3%O4>aIGaoFclSX=P-h1=ry%|s_6bgkx zp-?Ck3WY+USioI910VvAVhe2352A18g=<>d4*)#}(f~I;-Kr`V$yeFSkbocFi#xEO z>o7mkL@t|MNB-LE?(UBD@%n;d1&S4T@l;Iod?yL|;P=7bqHpGDaFueq1;8g)uB`l< z+6rTZYgz!2hY0Q5>Wqh>X=Ab4>0O!P|jO5fSk76o0`qQ(JUBk6bpZ zmD?@i`eGHi3yZ+6=gA=OGl3+HbRVTZ7|h&j`(~b8y@A{pVeA+H>hQhwnf6QTk3@AD z4u@h^?Qs#vg05Gxr}LZ>gt7*RTYtn_CDC?g)(S|v_zB9LCr`c?>4mkw-n`jZc7$oy zF$sb=SpXo{K80kmkq|o|Py=(o_gB%Ke$ zZe?T(G|?fI7SJ^<%6qrz0LEAj_L9nc?3B}uWq*17V;;YJg{EoK+NU0rF=ie6#|nVQ zFJFmX$>v7!ke%a`@m~DsXX>|p@T+ftTsEt9J&!vFH+gXOEVdHs5Hxzo3FBT^kU|US zzJ5q~?{-LTx!szZjwe02w&}S@)c!Eu2J5%Jo@xJ8h6L7L{AYUPXc&h7T23@= z=SPw4QLcT;)8~I`-|X+FcM^E7GCqAQRw%byp@X0XSN#N~XDK^yA>+SJ z>Rw`-_KzYQ#|cfAV#RkxS_%Xm5OsR=|1{)Bh|X6Ao&dz;LME+Csjb-G=jiANf|2;q zb$Ht#sWCYh5c8gkxCxAAgVY5E*tY#=5;VxWd=>kDMcH4WP$(1%g+ifFtj2#V1d$c` SA}D?U0000SFFHvbh01c8wL3U3-RdMJv> zOD1XP;4N?v2mvG;a0*0P1Y2#l>mVL;lqu^dk+Ol_2OuDcr+e?d@7>)y5)1}|!C){L z3=VPCHr1z+jqUGjR(H~;GF-#$;nAp_CiXw&FN@#E$IvS zyfqk)vox)urG3e^t<%v+tHZC8STE%B494SF2LLJzLq}xWG>8-5|NNHU|NTe(N#t@l z>+B5Qt2(*<{jgFljf!d#|7EKP)Vdue?FPMr3IG>Jz3|?o-C*~yMTH^vU%UcH#deZ01N06m z9QV&sKMf`EW40Dp)Ly#=7>vikGPrmJShgU!0So2S(L1QHd)R`*>320525vF553AK8 za?FcnUWFmJ-qa@`m&=i6Euy3S%vMAppV!hoQ4x3(s_4>@Gz+8Oyzj01FJ6U7pzg_q z!xjU#7`9bcprA<0^n{=i)FP?cEZMeI>vjZll2O)N@MUu7CcUnH50J;h z)790rqmbPD;`~|B51}U3JftnP5DiE(31an^wq@Xj5LlL_ZCkqrI30~-CWeO9OYYH< z2lu|w?OKbHZS(2sO8Blf_b3#{s!2Gzvc=sCgTMVNlS$<{k zDc3mxII2Sivp>!!Sg-yn)923Rx^NsPyindyk>PpRdH4{Nmm$9Eg`ef+yIyL~0fIil zKGo0RZ{>RAepMMt_6BU@#aA27|${ a8vg-|0)?5&tGM_80000001Be1^@s6m49>f0009gNklT8qvN`b z!z?Rt`SZO|J|F-yJFxzEAgM40_Sfq_4TOj&=>?SZ0>UIA3DfI9mCwNB8l+rrJZ}v$ z68_Y2RgVDduh*|bNiU%BpikpLpNNRYgFYp_fFj}3c+fYt7@7}8B|NkrL@$WKIqbhA zegMGM`YNQPxP@Uz0O-Zz2bFs@RFdl*SQdl)%wQp~zh3`o5VG?jPd?d_O^ToU50PJg zssro$@_Q47z?%V?a-1zv($O81N7yv+z0|0J4eW?~h^U0B^ zIM`p8U$25$o5ii&y{`VsQU ql6Cw?_ow1uKf^E#!!QiPoaG-2DU`)Ecz2ut0000001Be1^@s6m49>f0009xNklVQx1fUi7|YV<(Do9f{e0KEGB0HVgwhQ6zoKM#rkd#Q^}I zg$h=;w?OCfxDnI{-F{y+0_DM|2{6M(V1G9Ll8%syFD3#oEIL92Pk4a;^IKv6v`0vdbzDf z*{|)O%M9wG4x#Z9D1&lQs(iii1aO6d40=PuceeK)2rHIq5h|f^Xxx^)SHk| z6_)^D&~BoI3PR6C)iEKZMAb1-bxizv`)0b2q(Q?lO4Mlp@b3K+P%Z$dIwnHT#m7!} zCXJBR-|p_KhGl;;+S8}`fHH(FR(UMhXw{jGaxZOn>+@F0@Z2$MM` z$b3m~mVeN0#U5-3GP9=IoVx_T}Y3W<)uRW4{lt~(#w7d06@Z78`}j_D9pG3pWic&GsJSh zSvx*v|Ap~iohF2D**LoO2kXGtVt;QloaS;`eEJru@1R!a9|N0f!%X{MaP;UQ*4cll z{lLj7ZtPsf)7{yAqo)2GJCTX~m($NM48t%C!!QiPFc*}+4aqYY;?}fy00000NkvXX Hu0mjfwPc001Be1^@s6m49>f000ARNklFq zpy~MxHa81N#I@m2H9gvv7*PK!0iltU3;FhFeNkyn_hXh*!ctrgrh7wb1JlQ8Hg#{~{)x zYr|pYdm{1Y??0I~Krg$|SQZavF9J4LI7%cFR2_%aZ{HGC_aJZ9h`$(rY6Wy{+tLS) z$?Q-0+|i?AH}R+W(t1pmhamA6<4;Y%^QTV%JEz`h6ETZrxav5j{HHl1N|`SG;xEoW z%^^hf8v*5ZgnoscB-?9tWVtEUC+~q*!R@UZFOb+616dz_#QzugHLBkT6rhw+D*j$K zqX%9^H9cQ7JwNldz+?Bai9!3dD;@_qHD<-1uRnDOzIEw>rK{ejXHOZew760)oBI(Z zGZXYI$g&;qYVbw%8-a0-R$2f&59iDnU2!v$ioY;_>M8i$*Y5zk%Wr${ae9B4VF@-f zs@#s~6L5u>#`Uvz2hLvwz{#fv2Eg@mwpDINO8m#;2jJedSpYR!X_@7gWQRPDB|!BX zff}u}RKF3ZTCHY0VHlb?!Z0*HpZ9NM>fi4Ye|G+<%jUd)gXQiA(~xf8eMG0zG51$j zR}cNn6cVNX^HQHD??0NGb8~Yv-oD1|t)s`Cx&I;;|L3JXfcSIpr!K*oR;%I!$)x8wtKn4wmyQEo?!>aEP&lf?fBv56-Bg8YVY zqft>`+`Y!-H_ww;jHGuWhKD!HDIcF6z3uqz5`Q87{YbQ10L8gWCeXb8D!IUNe%H3aaf^;g{KxAT1VIo4K@bE%5QKl2U$Ytv>}Es#2><{907*qo IM6N<$f-|GREC2ui literal 0 HcmV?d00001 diff --git a/src/sprites/character/Character_SlashUpRight.png b/src/sprites/character/Character_SlashUpRight.png new file mode 100644 index 0000000000000000000000000000000000000000..835cbbb839b17c20482995b2a3b248da13c1bc12 GIT binary patch literal 895 zcmV-_1AzRAP)001Be1^@s6m49>f0009^Es2e;QojE9aAt6NLR8$bT>A5(5N55ClOG1VIqQYW7tb0C@6B z{s-po0(@p{a{&Cc@`U_r?0;0Z0I~hgQ7|*M@g@Ar0dP%G$iIgEx&ee^n3x&cIA&4n zqp+{4KqmA6FvfEL@kMY96SFYn0C;jN`PalRRW0Vl#s+?0UUDD~p@lg#?<`xaG-a&n z$KD1PE+9U6+jEiKm)+k&4didhUp0Wa;AIgbqKyk6LSN)vq>VoU`P=iCz6FbW{IVh} z%wb^;$1pKvtg+s(ud3;c3tR_%6`}_6ci;cGY$3BSEHwgvxG&JWGv34~oHE88!vug* zZVU7DB58_(Ss0SP!~RN(kTTP+D4?lP*jH7__T2RIZ$liDX^hF=VSlLrR#{>%_JXC~ zxP&QV&=f_oJr}m;uDl4M{~-VR_)7&)+>v4@~*u{5u1Zv&RO&-jv@bg_)AaGeN`=NNJaR9p@rVj7@TuRl2qxl zOOo~z%VuLS{R4n3%Tg3YkG0B32JBFEVNP-p&t=!L7Cx=Z25J>(BesFM*4L1kCbzSGW zuIIew?pA|p^0(oydz-uwDqYtB0KV_1`>sw7@$~J7yLfW!t1^yRJhFr@i|_jghF02# zH0dUPJO0uSq8BMD;N#v7u(y*dz}(%E+^^?#H93x11jHw_bXM&hjS;|V#7F-2{q<+S z!3g@#Pw9@u^#7U!=pBvk;#i6-%ZQ?gFAf6V_wnkPB7OgpdjLxQ4*P2dFl|N$qwBqs zQSP8OTQ8;0=RayHZm%KdzVFS;XOO|@8^mWA0LNzU7uchK0TBBcs^&% z-i`87mR8=6|JKFte_&sK{rJa#GgUuh`IL%mY_GeoyxrN?E;Bo~Z~60%>(f6KZ7BF- zbwJzq3TxeqwZ=M+w!S=_?rHhr#MwPNF39Le?dL0)ap~Ta;>qXbLPOWqa;=_a{crtd zyWh-Snx?Vd->nbusQ2fuTIHp=^iB_3kk{My^X&I5eXOsxF*rLnd*+`wvOBV z{rk6NyC>Pby?-<+gYVp%vfUv?dS3nVhzopr03zT6 Ai2wiq literal 0 HcmV?d00001 diff --git a/src/sprites/character/Weapon/Sword_DownRight.png b/src/sprites/character/Weapon/Sword_DownRight.png new file mode 100644 index 0000000000000000000000000000000000000000..e3bb6ee2f9c2b68d92ef5e1c28ab6ef951a403e6 GIT binary patch literal 477 zcmeAS@N?(olHy`uVBq!ia0y~yU~~Yo9XQy4WcKrQazKG4o-U3d6?5L+_VqjDAkpw} z{jul@rjqwfymJmN`To!)PEcGSQ~N^v9HY-RoShT(ekp#wrK!jS)K3IxNZ)Y#t&&pT zTz5mS`SM*wxd+W%pD}9G)@hu-U45jN^}eVj+k!7Y760WGud{o#%df17zociq^ZiBU z_4_`H=WXuSux2RCXH2PQ-EedJv;K4U|LoB{6aMnE`1{Rz)(nEF=kNbykGRvMU;FP7 zXM)T5z4OmyGc@m6V#oOF>h13x?+-6XE{^@*yK~h%=il?%xfyGizJ61_y`eY2)jIEL z@BPCG>sR_W{BC)_WR+j9^?tdtKeqDyJ|3*Ze9u7pTF3nvTw(h+GQ8W&8n?3ZBeUBE z*0`DHriK4`(8yx2(E9uLWo_s0iMt6L*dn(7Q1O5MwqkyRwW`@mYYZkaEZgp~V--hu zy}>kwW#;A5I&}sU8SKnIHcPS?>|GiERLFkD$>j%L8@!q=H(mY+f12QdFKx&Ce$3Thne`j&5hnHKr>LmSKqDd z7gkTX)INLNozkw38EWlJa&N2OGJZ)D-Iska`o8<$1x$>0PPTp6D`>yMg`s>yN4(z; zr6c^SeuY&toPXE%W3O%6>BkMS91mpQ^~eXzWXRi+{3oY*p%CMpgy&P|UsVoKYPkJY zQctjjtA=aN%R}OGif?{$Qf4Wbtt(o?KYz8|0$!#c5_;v$Y?b^5hmXBo|HE-Hi$VOi z)VxRRxk3jH9pS&F+E7^&%W9?6@HFC=<71W$3b*4kW-~Ay{$%Trw`~{OuScJ*t(u;m z#2RtJ=KR+7fK$Jk7i{@6H|W;mBF732MxBIN%Vxy?yCUb7dAk1DtaW$ze1#A2nCHLm zdp;vR?lj-BzV(NuDdjX*$}ycdTja;E*iwd1@PGJz&ANZDWyy=E>!)CpTn@Ln$K;%eb2kkQ{R7L z`S3XW=|VBXUw;xauE&26e0hJ{hV;ef<=f1Dao2tM$&4BmNW7SD;zyMtnY+?WL9x!@ M>FVdQ&MBb@0N}OedH?_b literal 0 HcmV?d00001 diff --git a/src/sprites/character/Weapon/Sword_UpRight.png b/src/sprites/character/Weapon/Sword_UpRight.png new file mode 100644 index 0000000000000000000000000000000000000000..aa7791d67df8bd73dee10c71647349f529485b22 GIT binary patch literal 580 zcmeAS@N?(olHy`uVBq!ia0y~yU~~Yo9XQy4WcKrQatsVi+@3CuAr*7p-a6QOIY5Bz zL1l-wyI;r#lZS5^mnyvS6kf@s_29@vrZb8+6I?lO9x1d*uAiT>cInyPaJlV;8Y zGzl3rl-|rS6BMj<`SM=i@akRJH`C?vjSk;w{~fw=(*Coy2fXh5UL$Wm|Jtd#km;MY zcU;=J&hnXEN=(iLZm%c*4Zbt{39sKF_R*{&sjzW{JL9ztM}&NSx*1Ge_xhP!Vp!zo z`Z?7M_ty4zlohR6k->TV*XmvCve#~X?6-aEwc}l0`@Q~^Fza4f{Qd9LZ|ToBZCBdd zADYB(w(!eO*~aXsM-wIH`7vG#-TF7`h}CK9*K%(njh=01)_J!fer9z~^d8grl?&eo zrt+Ij<@y=SyE!5F-}(G+wYP4{Cb-O$eHQWZdxEg{-1~w*Ja27(|9kb;8*j^Wzs0v~ z&z|z;_{qd?Z_9Qs`_$c0|MR+5@xQM%+|R#!lKS}a^S^)R-*?R4{dm3Ksn_?^PX8AB z@ihC$dimNtd)~3jq{OrS41Vta>&lw*CzN9DMvK}ssjJO&6x4E@F+(=NsF`W^){{m0 zOp1!EGbU`esNiFrFrymrB_ yZr<+jwgP3=8FyOk+hVk=RXLEO2s``0*?&wq-`^Zhc)e*ENQI}XpUXO@geCxp=>!D; literal 0 HcmV?d00001 diff --git a/src/sprites/character/down/Character_Down.png b/src/sprites/character/down/Character_Down.png new file mode 100644 index 0000000000000000000000000000000000000000..79754937ae33e7e78bd7f2a90739414a5fe16441 GIT binary patch literal 917 zcmV;G18V$1q=p*!C){L3IED#01(a1#wyeTQr=v|ftNvpJShg&jbULJ(9EF&WxtW>5S-&4= z_*jS-CP)CFDFIpbA9sRcn+G7owXMQ?kbunkANv)sP%5F_?OrP=f|9}64ghE?gOivS zob~&pBLP|WKlTM^ce_*%45?SbD%9d3SC9USUmrU0@7Yp8L zsQlGD7Yn75mhu-(fXY?(Y8++Dy7lVIyMF+~w-5lesuj7-soM^THnm;(tc74ZCH3mI zBlq5-O|+}`LCjjnyMF-Yi(o4gyf$bjFyxpsb2$KV9Nt1x9t%i!nx= z^Q6jPs{X~eqRe0ApK~7i!_=m;5^LaCZ|J78f_?Y+!qC>v)g8ZHv~M~qg?;xpRuD0u zR<(ksTT7T;-v(n$KJa}XqAEmU)~27f|ET^b|D1Dt`t?)J6Eq$Ls}ScLj}E`dBhR|+ z;Lp3a*xlW|>6l|RMac_{4T&aVn1CeoL$RdbKcqjxHvk|qPl?15+G~m}igIZuC})?- z9)^=a*xHfhKMDN^ckVx`&p99Z9yKQud00000NkvXXu0mjfo8r2c literal 0 HcmV?d00001 diff --git a/src/sprites/character/down/Character_DownLeft.png b/src/sprites/character/down/Character_DownLeft.png new file mode 100644 index 0000000000000000000000000000000000000000..a76cf65949fa49842bc8c5c5d82fa3f4904fb30d GIT binary patch literal 797 zcmV+&1LFLNP)Udv z0f4s5AvBbP78Kxo{XgyZfh++c2@}hzJB-E(&^7-L%3S2yHUJO+`gwax5%cBmBIaXz zb3*_qyG|=v0RRA7=HOfMTYCYz?7vh4Y?6c`=3~noM9jx*y}|-R06>6W0MbNkl0-`q znl8Y(`-e9g0RTnJr!8|RVm?9H2E{|XuOX1Xw8hV2E7YjnY5%=r7r3@9SAgjDLo6`F zY`rS=x69&ymyYBz!2q>BtTVGwcww*H%q}y)Mym()1 z#Z~Ed-T%=BaBaIMX5IqB+$uPfjH(W1Ehs>z{g>GRFtM!OY`w}Ieh#4=O#Yh|6rj`o z|7A{590qv(bpar~1(~dW$k%E>RnOsxWyypVS3_|wt1+O$ISk_;L_`#a0mWfJNE0GY z!^mz^pzn4>>J>HBQ@GBP_?i1p1K2I+v|G-Jh-kN*Qyc~qF`stJxd=!Xtth}L_}B8m z*k|VzP)LGengGz3w=b&~{=>Zm06?5`0zlLLLoMQ`;vWFLd-7Oc08k7IVf6!vPySSk z_=gwIWemjv6l2g5K!g6Xg349D&%fA(lo+c;6aWBuo+qC<=fpXefoT!XP!pj(;xN$O z01f-6I1HpN?rA`+4WI^t%PYd=6?vW~gDPx^S{RSVfKmWeR~Ub9J<=sFXu^MP+^7w> zuPL%-)rLH2a?WKkgt)E|Ahe5`g_WnZ^F<5|_)iP&UZk&;21LI2`cc1`f--+<3Lt;0 zb1}`uQiFcu{>8`ljA6vzKT31Sv!jsno?{sl5XmS=h`JzC=-ltnewi@toHqQxova?fY3_1%?7XNp${z1pKXQGn{1+FK~2d;;o z*Vh~f#otp#!?N%-`b8tREzT8DfU^0|)BqjZ#zhoS;CdLjZTPqX01QyY@!2W(eBgSx zh$1B;QfC3m=)aOhn9Z|sW`HUHz{izHhO@UEkd7-KRW%hqJ&4rUU9Jd7!uRUm3R&LW@!?G}%Ojc^j-OowLa|$lA z|5P0+uRkmV0FATn0DzI(7VpEJkJiZ`^|e#7pv}k$NETpK(z3Gc{sG)Pe(=GY4`I(2 zuVK$8BFcSJhP4CNqrmm%HeWEm5hCKrQ(ulDsktrwWoSC=Q{Z|O_I#QS`$R-E9rkHD z?3c0t3AFgtuj;?dQ4Q{KRWn z1pubQJ_e{F?D?p5TOcCTx-HbYEnI$hpSScJy8J)9e^0#E=Krm?4*=vijyNE7B?_2= z4iB1OEgp5r(f8`vQzhD({fij~fAJ51UOs-LAcR;n?D-2xkdvXR035@r zG=Qv+gn!3zlnz4CZIBDm{WLp@`9)47LLLpAjC#GUR9lz-xTzM3y{rMHyyOc{fdGW* zqoX52e21z;bP-`vbb2BdRl;lH|0V`5ywreBrzwA~E`Y%GFs#sAGQxhG7_nVHk#C7=}5_SGx`XwEe9dRo4#y z6p1DQptf!2Zu|-YP<8#A4hV%$0LZnhLhPfU0H8$rUu3DYH9}ee00dYe6h5N$x{zRm z!pA(0jR&C2`ZEoHc^p$Hd_BQ&^ka>6?(hpNjRSqQMp(?s~$8n6<-co1)s;(cv zQzz&}eiH5X`zB}KvjG60Y#R>1Rq9{%0%iOC%NtYX|AGLJ=Xn%~CPkvDk3wU-du=J( z$TMHuZ;OJjAc> zOWgeY0YII9FS-Li}TJc}R`MQ%2qyq-MIF0;C^? zH-U=6^>-egisnbEPjWylKY19G7b0@!i=CxV`jdw*Tdq$3PBBhCJ5LS$R&3ALE&@QU sRx1}@aHaZlafD$QhG7_nVVM7zKTV!EaAoG8V*mgE07*qoM6N<$g4={&ng9R* literal 0 HcmV?d00001 diff --git a/src/sprites/character/right/Character_Right.png b/src/sprites/character/right/Character_Right.png new file mode 100644 index 0000000000000000000000000000000000000000..2ca39842a71eb49d0bdc3b6f4a309c3b2afa10ba GIT binary patch literal 787 zcmV+u1MK{XP))+M90i6gp`0uW$%K_?LL-*e(VCfOs@?X^Jr& zOrgaP4GqNe+ac0bGHz?7`f%)fK=@16`#$%cPUn-sU@#aA27|$1Fc=Jm<#=Q707&$> zacsIjfK1$d0p!58OAo#R1=w8w-G>h$YJkA?1d#HxsICC(;{PD)7otG&k*>)o9d)(`m-T|D6>mz}op|z6317=SnB!b4B2K45!oF zhrulJs?o^(?E4e<*?;K{;8>Op@-N~>DC@dhMgi8lArMEqIY3lD5N zx3jS70<5e5(g`}2r5hm`D3*?{IZWJroWZDwN!X%UcZweM)G(N54(sZlWdzXA&juHq zH1|^SBEq=s;|xZeIJ>#ez#Yq~#f{vfo;url)NuwQU4*=g55^z({TmpNaod+!zbk?3 z$*kWM5s_KHE916bq7mXPPT=+4(?a5| zKmmXa_mAX6DJ4oNz48@rMaj>RYJ{>10M+|%ZEZo0{&$(B%DC+V@b=Re0EWZi{~RwH zwgcfrlv2sKnrDx!+Si}g3%-~emG~cfhkUsBEC5O={r-F#Wc9yO2blG{0Bj$f>SUrs z3he-T8MJkd`t}ikcduUXr1hNbqtp4_i^`=cf0^~We1AXI9WTj(lcAVgj=e)j4tuS0 z0c2;Vlv|Rwlb+VOjvEhufOI;Y!tcyg=$~>;wn6;74F6O@f$K@9(<$>-Xfj@EOU{T( zx;0$3|3Yu7@=wl4DX9ZAo6TfQO|(i@>i+_&@Q+vte}lnbFc=I5gTZiL`2`?d4qV~I RDp&vj002ovPDHLkV1l%;fNlT) literal 0 HcmV?d00001 diff --git a/src/sprites/character/up/Character_Up.png b/src/sprites/character/up/Character_Up.png new file mode 100644 index 0000000000000000000000000000000000000000..d1902e938e3543aa182f2dd5f1beb68b8e2816f9 GIT binary patch literal 905 zcmV;419tq0P)efOU4&UOHaL?V$$Boc{4B9TbK`Ko0A zfc4*}j%oJ~0L;9|Qm_tEv;%M?wieiu}K#G%9Es&^OwD z;Q;dh07uWGrWW&*QA~5PJgB?4MKsP1j{L-GHO#h2g4Yia{vjP8@H# zZsdcIbOO@sU!gQwnw!IQug7Y-fimtw+a6#ZRm(&{)0C#`z9^UweA-+i7*9;vsrEmz z01BFhrt2~QsObg(gbo6)%&KLQ`lpa&0@Ch(D%~(2?G$* z5Gmpi3YwYP z;iP@Z96j&52K7?4OcaYn*tX68M>%OYG}rtW_!F_3s7%VE^+s)a!L$gJQ8L{{FT85i9!#FX?|I zo9YbCIuDHBg&UTDnDmqOFZ3w|n8{?6ZnsP7m|Ot&$9n*P*{vM_e#0Ewwh?F-$pCB{ zt2i<*n4{+b3{QY<+h|qFST3zZs*i;IbNwII9{|AV>8Y4$INo?{+a}LIqyTvW#DUW8 z^9>K5j&B>Q%8_|N>7D|+KiQX4_Z|YKT`& zH&{(K7)rab=?9p&{{j8d?vw8!bI-}A1mSs-KR}U4 fBoc{4k{>4Q| zL*1bhEGS~>ayx``F>jmtC3An$6@KZS^FQa@2L?bQkw_#Gi9{liNOn?HvjD(np9jaG z>jwbt7)6elf{=g&9J+q+mSKt@Xo?cK{WueVL)BkWRRG}B9HYovhAGY?3`8smNTYt{ z^}&*=;%+?VEyDx=SQstb?;?lm!L9Fp{Xjm@xnmR;>}ra_oXI2%)2Y?($ntCac5^1Mrau(oXLdmeAow7H7mqh7_ChL*#>|h zr{c8h-&p`;xQiStjL=1H>+{=2$UelKfVAuPG6OK0T6T$d6Vxb7Kmt;&-^&aDO;OnO z;1*s35#%ULKmt;&-_K@=59<~Roi2bUk?X3BHX|ifWq0GTkM1|4DT)Z_rcKZ~9}w(j zFqQg!6#(ZPj4@Ck+(Dq~3|hR~eq0G4`)Sngdj>eI6#xL4N<&m55!P2AvY&(mBtbtB zg#Mj-0HSrdOZ#Vb&1!@5PsLBof2i%)y)1^X?-6wU3& zW&SL12V&MwgJ8w}8vUUm65tp`Zre83bzL-s7FMJ^TSkjI? zHG5tHh@5=|ZE%l%zb~%+eEpFQ*^ghp_wi#Zm-#bR=yZ{(G@uRc(fRNh{eB<1u8Rf$ z-`<}uEDvE2hkiGI-U%OMc)# zN5eLfpr4k3X|Fw>>1xg8Wgv9cuDi3JN~1&7fOrBBxBj`!Hy7*tiOzwj3~uD~9Ed&u w$t<807*qoM6N<$f)){a*8l(j literal 0 HcmV?d00001 diff --git a/src/sprites/character/up/Character_UpRight.png b/src/sprites/character/up/Character_UpRight.png new file mode 100644 index 0000000000000000000000000000000000000000..a406a08e289758257df384914ba87de8c5fb78fa GIT binary patch literal 821 zcmV-51Iqk~P)Iuq z(y=7LQ}>T(R%_z2x*-7Z)>w*NpJWU0bp7YESuX&866C6wW*lK8R)Dnm&x92QXjW_3 z3<4VtIRFkj=TuBH({tSi2Q`a)Hp`#e+dv$}&QEWDk=mrtzj6Z<4TH<;Ycb8dABP-= zopbihmCUJoqa27Li4`EN{-3ETtC;t`TixK)y@_-j77YWKZXUp9fX#qrwMNf%BflHd z>R-75q{u8Ba@Y)T$a#T;Ie!*O7XZ@fU#S6P_o?vjC{q4I1(GU2n*Bdl5tiM@oVyS1 zi~|%6BeHvVbpI-ScM;D5Y4)$wp%v4l=eqZfAo<_}&~Mn3j~(T+6;XQlf?xqulQxeu z`&SOqp9C0>BWYe57ebK9WI~-CMmwPbq}G3^ApJju{2X@98Ma#h{QlZcT>&I%^dIV1 zz{{l8MF%ypoAGg8|j=Z!spLcPqe-IC_nSzC=dgv9+94P&nV zU!OlkEgeAg8+PE;r(Bk03CpqsAZl+)kqhsZ*755#t2J-=KOeSR{urYsG5`J;nBgA~ zVVXZwr|y>4IlH(L0G4G}87-3mSaAI#_#=uZX|a{$4F7vdXx z_r*hu{{77t)BhekE}nb>G#ZUYqtR$In*Zf55VE<&mw%9100000NkvXXu0mjf this.fps) { + this.then = this.now - (this.elapsed % this.fps); + Ui.draw(this.canvas, this.context); + } + } + +} \ No newline at end of file diff --git a/src/vendor/State.ts b/src/vendor/State.ts new file mode 100644 index 0000000..879701e --- /dev/null +++ b/src/vendor/State.ts @@ -0,0 +1,82 @@ +export default class State { + enemies: any; + player: any; + projectiles: any; + loots: any; + score: number; + level: number; + + constructor() { + this.enemies = []; + this.player = null; + this.projectiles = []; + this.loots = []; + this.score = 0; + this.level = 1; + } + + public addEnemy(enemy: any): void { + this.enemies.push(enemy); + } + + public addPlayer(player: any): void { + this.player = player; + } + + public addProjectile(projectile: any): void { + this.projectiles.push(projectile); + } + + public addLoot(loot: any): void { + this.loots.push(loot); + } + + public removeEnemy(enemy: any): void { + this.enemies = this.enemies.filter((e: any) => e !== enemy); + } + + public removePlayer(): void { + this.player = null; + } + + public removeProjectile(projectile: any): void { + this.projectiles = this.projectiles.filter((p: any) => p !== projectile); + } + + public removeLoot(loot: any): void { + this.loots = this.loots.filter((l: any) => l !== loot); + } + + levelUp(): void { + this.level++; + } + + addScore(score: number): void { + this.score += score; + } + + getScore(): number { + return this.score; + } + + getLevel(): number { + return this.level; + } + + getEnemies(): any { + return this.enemies; + } + + getPlayer(): any { + return this.player; + } + + getProjectiles(): any { + return this.projectiles; + } + + getLoots(): any { + return this.loots; + } + +} \ No newline at end of file diff --git a/src/vendor/Ui.ts b/src/vendor/Ui.ts new file mode 100644 index 0000000..c9b7c6f --- /dev/null +++ b/src/vendor/Ui.ts @@ -0,0 +1,14 @@ +export default class Ui { + + + public static draw(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D): void { + context.fillStyle = 'rgb(0, 0, 0)'; + context.fillRect(0, 0, canvas.width, canvas.height); + } + + public static clear(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D): void { + context.clearRect(0, 0, canvas.width, canvas.height); + } + + +} \ No newline at end of file diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1a2f4df --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": [ + "./src" + ] +} From ce96f45674d6fc193eae79b7be97d86fdf3c4cd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20THOMAS?= <74055963+SebastienThomasDEV@users.noreply.github.com> Date: Tue, 7 Nov 2023 21:39:46 +0100 Subject: [PATCH 02/14] entities --- src/entities/Archer.ts | 8 ++++++++ src/entities/Player.ts | 9 ++++----- src/entities/Warrior.ts | 17 ++++++----------- src/model/Entity.ts | 4 ++++ 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/entities/Archer.ts b/src/entities/Archer.ts index af04902..b08d851 100644 --- a/src/entities/Archer.ts +++ b/src/entities/Archer.ts @@ -1,5 +1,13 @@ import Entity from "../model/Entity"; +import State from "../vendor/State"; export default class Archer extends Entity { + constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { + super(x, y, 10, context, canvas, state); + } + public draw(): void { + this.context.fillStyle = 'rgb(0, 0, 255)'; + this.context.fillRect(this.x, this.y, this.radius, this.radius); + } } \ No newline at end of file diff --git a/src/entities/Player.ts b/src/entities/Player.ts index f29787d..a97d7aa 100644 --- a/src/entities/Player.ts +++ b/src/entities/Player.ts @@ -3,12 +3,11 @@ import State from "../vendor/State"; export default class Player extends Entity { constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { - super(x, y, 10); - this.state = state; - this.draw = this.draw.bind(this); + super(x, y, 10, context, canvas, state); } + public draw(): void { - this.context.fillStyle = 'rgb(0, 0, 0)'; - this.context.fillRect(this.x, this.y, 10, 10); + this.context.fillStyle = 'rgb(0, 255, 0)'; + this.context.fillRect(this.x, this.y, this.radius, this.radius); } } \ No newline at end of file diff --git a/src/entities/Warrior.ts b/src/entities/Warrior.ts index 34789a5..23ff8b9 100644 --- a/src/entities/Warrior.ts +++ b/src/entities/Warrior.ts @@ -2,17 +2,12 @@ import Entity from "../model/Entity"; import State from "../vendor/State"; export default class Warrior extends Entity { - context: CanvasRenderingContext2D; - canvas: HTMLCanvasElement; - state: State; - constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { - super(x, y, 10); - this.x = x; - this.y = y; - this.context = context; - this.canvas = canvas; - this.state = state; - this.draw = this.draw.bind(this); + super(x, y, 10, context, canvas, state); + } + + public draw(): void { + this.context.fillStyle = 'rgb(255, 0, 0)'; + this.context.fillRect(this.x, this.y, this.radius, this.radius); } } \ No newline at end of file diff --git a/src/model/Entity.ts b/src/model/Entity.ts index 15931cb..1542b82 100644 --- a/src/model/Entity.ts +++ b/src/model/Entity.ts @@ -4,6 +4,8 @@ export default class Entity { public x: number; public y: number; + public dx: number; + public dy: number; public radius: number; context: CanvasRenderingContext2D; canvas: HTMLCanvasElement; @@ -12,6 +14,8 @@ export default class Entity { constructor(x: number, y: number, radius: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { this.x = x; this.y = y; + this.dx = 0; + this.dy = 0; this.radius = radius; this.context = context; this.canvas = canvas; From 3593db61639a5068ae0f19662801ffefbed1d3f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20THOMAS?= <74055963+SebastienThomasDEV@users.noreply.github.com> Date: Mon, 13 Nov 2023 23:59:56 +0100 Subject: [PATCH 03/14] refacto --- index.html | 6 ++-- public/vite.svg | 1 - src/entities/Archer.ts | 6 ++-- src/entities/Player.ts | 6 ++-- src/entities/Warrior.ts | 6 ++-- src/main.ts | 6 ++-- src/{model => models}/Entity.ts | 6 ++-- src/models/Prop.ts | 0 src/vendor/Game.ts | 46 +++++++++++++++++++-------- src/vendor/{State.ts => GameState.ts} | 28 ++++++++++------ src/vendor/Renderer.ts | 24 ++++++++++++++ src/vendor/Runtime.ts | 36 --------------------- src/vendor/Ui.ts | 1 + 13 files changed, 94 insertions(+), 78 deletions(-) delete mode 100644 public/vite.svg rename src/{model => models}/Entity.ts (85%) create mode 100644 src/models/Prop.ts rename src/vendor/{State.ts => GameState.ts} (71%) create mode 100644 src/vendor/Renderer.ts delete mode 100644 src/vendor/Runtime.ts diff --git a/index.html b/index.html index bec9790..ec43d89 100644 --- a/index.html +++ b/index.html @@ -4,8 +4,7 @@ - - + @@ -13,7 +12,7 @@ Document - + @@ -79,6 +78,5 @@ - diff --git a/public/vite.svg b/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/entities/Archer.ts b/src/entities/Archer.ts index b08d851..823fa36 100644 --- a/src/entities/Archer.ts +++ b/src/entities/Archer.ts @@ -1,8 +1,8 @@ -import Entity from "../model/Entity"; -import State from "../vendor/State"; +import Entity from "../models/Entity"; +import GameState from "../vendor/GameState"; export default class Archer extends Entity { - constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { + constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: GameState) { super(x, y, 10, context, canvas, state); } diff --git a/src/entities/Player.ts b/src/entities/Player.ts index a97d7aa..d74d755 100644 --- a/src/entities/Player.ts +++ b/src/entities/Player.ts @@ -1,8 +1,8 @@ -import Entity from "../model/Entity"; -import State from "../vendor/State"; +import Entity from "../models/Entity"; +import GameState from "../vendor/GameState"; export default class Player extends Entity { - constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { + constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: GameState) { super(x, y, 10, context, canvas, state); } diff --git a/src/entities/Warrior.ts b/src/entities/Warrior.ts index 23ff8b9..18f14a0 100644 --- a/src/entities/Warrior.ts +++ b/src/entities/Warrior.ts @@ -1,8 +1,8 @@ -import Entity from "../model/Entity"; -import State from "../vendor/State"; +import Entity from "../models/Entity"; +import GameState from "../vendor/GameState"; export default class Warrior extends Entity { - constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { + constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: GameState) { super(x, y, 10, context, canvas, state); } diff --git a/src/main.ts b/src/main.ts index ab18d6b..33ced38 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,8 @@ import {Game} from "./vendor/Game"; window.onload = () => { - const game = new Game(); - game.start(); + const canvas = document.createElement('canvas'); + document.body.appendChild(canvas) + const game = new Game(canvas); + game.loop() } \ No newline at end of file diff --git a/src/model/Entity.ts b/src/models/Entity.ts similarity index 85% rename from src/model/Entity.ts rename to src/models/Entity.ts index 1542b82..168e53a 100644 --- a/src/model/Entity.ts +++ b/src/models/Entity.ts @@ -1,4 +1,4 @@ -import State from "../vendor/State"; +import GameState from "../vendor/GameState"; export default class Entity { @@ -9,9 +9,9 @@ export default class Entity { public radius: number; context: CanvasRenderingContext2D; canvas: HTMLCanvasElement; - state: State; + state: GameState; - constructor(x: number, y: number, radius: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { + constructor(x: number, y: number, radius: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: GameState) { this.x = x; this.y = y; this.dx = 0; diff --git a/src/models/Prop.ts b/src/models/Prop.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/vendor/Game.ts b/src/vendor/Game.ts index cb63cdf..5204dbe 100644 --- a/src/vendor/Game.ts +++ b/src/vendor/Game.ts @@ -1,26 +1,46 @@ -import Runtime from "./Runtime"; -import State from "./State"; +import Renderer from "./Renderer"; +// import State from "./State"; export class Game { - canvas: HTMLCanvasElement; - context: CanvasRenderingContext2D; - state: State; + requestId: number | null; + isRunning : boolean; + fps: number; + then: number; + elapsed: number; + renderer: Renderer; + + constructor(canvas: HTMLCanvasElement) { + this.renderer = new Renderer(canvas) + this.then = 0; + this.elapsed = 0; + this.fps = 1 + this.requestId = 0 + this.isRunning = false; + } - constructor() { - this.canvas = document.getElementById('game_window') as HTMLCanvasElement; - this.context = this.canvas.getContext('2d')!; - this.state = new State(); - this.start = this.start.bind(this); + public loop(): void { + this.requestId = requestAnimationFrame(this.loop.bind(this)) + const now = performance.now(); + const delta = now - this.then + const frameInterval = 1000 / this.fps; + if (delta > frameInterval) { + this.then = now - (delta / frameInterval); + this.elapsed++ + console.log(this.elapsed) + this.renderer.render() + } } - public start(): void { - const runtime = new Runtime(60, this.context, this.canvas, this.state); - runtime.animate(); + public setFps(fps: number) { + this.fps = fps; } + public getFps() { + return this.fps; + } } diff --git a/src/vendor/State.ts b/src/vendor/GameState.ts similarity index 71% rename from src/vendor/State.ts rename to src/vendor/GameState.ts index 879701e..d5b8380 100644 --- a/src/vendor/State.ts +++ b/src/vendor/GameState.ts @@ -1,18 +1,26 @@ -export default class State { - enemies: any; - player: any; - projectiles: any; - loots: any; +import Entity from "../model/Entity"; +import Player from "../entities/Player"; + + +export default class GameState { + enemies: Entity[]; + player: Player | null; + projectiles: Entity[]; + loots: Entity[]; score: number; level: number; + context: CanvasRenderingContext2D; + canvas: HTMLCanvasElement; - constructor() { + constructor(context: CanvasRenderingContext2D, canvas: HTMLCanvasElement) { this.enemies = []; this.player = null; this.projectiles = []; this.loots = []; this.score = 0; this.level = 1; + this.context = context; + this.canvas = canvas; } public addEnemy(enemy: any): void { @@ -63,19 +71,19 @@ export default class State { return this.level; } - getEnemies(): any { + getEnemies(): Entity[] { return this.enemies; } - getPlayer(): any { + getPlayer(): Player | null { return this.player; } - getProjectiles(): any { + getProjectiles(): Entity[] { return this.projectiles; } - getLoots(): any { + getLoots(): Entity[] { return this.loots; } diff --git a/src/vendor/Renderer.ts b/src/vendor/Renderer.ts new file mode 100644 index 0000000..ef6dfa9 --- /dev/null +++ b/src/vendor/Renderer.ts @@ -0,0 +1,24 @@ +import Ui from "./Ui"; + + +export default class Renderer { + context: CanvasRenderingContext2D; + canvas: HTMLCanvasElement; + constructor(canvas: HTMLCanvasElement) { + const context = canvas.getContext('2d'); + if (context === null) { + throw new Error("Canvas not supported") + } + this.context = context!; + this.canvas = canvas; + } + + + public render(entities: Entity[]): void { + Ui.draw(this.canvas, this.context); + } + + + + +} \ No newline at end of file diff --git a/src/vendor/Runtime.ts b/src/vendor/Runtime.ts deleted file mode 100644 index c842371..0000000 --- a/src/vendor/Runtime.ts +++ /dev/null @@ -1,36 +0,0 @@ -import Ui from "./Ui"; -import State from "./State"; - -export default class Runtime { - fps: number; - then: number; - now: number; - elapsed: number; - context: CanvasRenderingContext2D; - canvas: HTMLCanvasElement; - state: State; - - constructor(fps: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { - this.fps = fps; - this.then = Date.now(); - this.now = Date.now(); - this.elapsed = 0; - this.context = context; - this.canvas = canvas; - this.state = state; - this.animate = this.animate.bind(this); - } - - - public animate(): void { - requestAnimationFrame(this.animate); - this.then = this.then || Date.now(); - this.now = Date.now(); - this.elapsed = this.now - this.then; - if (this.elapsed > this.fps) { - this.then = this.now - (this.elapsed % this.fps); - Ui.draw(this.canvas, this.context); - } - } - -} \ No newline at end of file diff --git a/src/vendor/Ui.ts b/src/vendor/Ui.ts index c9b7c6f..8cefd11 100644 --- a/src/vendor/Ui.ts +++ b/src/vendor/Ui.ts @@ -2,6 +2,7 @@ export default class Ui { public static draw(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D): void { + this.clear(canvas, context); context.fillStyle = 'rgb(0, 0, 0)'; context.fillRect(0, 0, canvas.width, canvas.height); } From 70ce981e80d45b3d31c224b8f5f239135f84306d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20THOMAS?= <74055963+SebastienThomasDEV@users.noreply.github.com> Date: Wed, 20 Dec 2023 02:32:39 +0100 Subject: [PATCH 04/14] player move & shoot --- assets/css/main.css | 6 ++ src/entities/Archer.ts | 4 +- src/entities/Player.ts | 129 +++++++++++++++++++++++++++++++++++-- src/entities/Projectile.ts | 27 ++++++++ src/entities/Warrior.ts | 4 +- src/models/Entity.ts | 35 +++++----- src/models/Prop.ts | 20 ++++++ src/vendor/Game.ts | 24 ++++--- src/vendor/GameState.ts | 90 -------------------------- src/vendor/Renderer.ts | 27 ++++++-- src/vendor/State.ts | 45 +++++++++++++ src/vendor/Ui.ts | 5 +- 12 files changed, 289 insertions(+), 127 deletions(-) create mode 100644 src/entities/Projectile.ts delete mode 100644 src/vendor/GameState.ts create mode 100644 src/vendor/State.ts diff --git a/assets/css/main.css b/assets/css/main.css index 38d83ce..7aaa082 100644 --- a/assets/css/main.css +++ b/assets/css/main.css @@ -19,4 +19,10 @@ body { background-image: url("../img/bricks.png"); background-repeat: repeat; box-shadow: inset 0px 0px 6px 6px rgba(0, 0, 0, 0.5); +} + +canvas { + height: 100vh; + width: 100vw; + display: block; } \ No newline at end of file diff --git a/src/entities/Archer.ts b/src/entities/Archer.ts index 823fa36..4593b41 100644 --- a/src/entities/Archer.ts +++ b/src/entities/Archer.ts @@ -1,8 +1,8 @@ import Entity from "../models/Entity"; -import GameState from "../vendor/GameState"; +import State from "../vendor/State"; export default class Archer extends Entity { - constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: GameState) { + constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { super(x, y, 10, context, canvas, state); } diff --git a/src/entities/Player.ts b/src/entities/Player.ts index d74d755..8a481c1 100644 --- a/src/entities/Player.ts +++ b/src/entities/Player.ts @@ -1,13 +1,134 @@ import Entity from "../models/Entity"; -import GameState from "../vendor/GameState"; +import State from "../vendor/State"; +import {Projectile} from "./Projectile"; + export default class Player extends Entity { - constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: GameState) { - super(x, y, 10, context, canvas, state); + public isMoving: boolean; + private inputs: any = { + 'z': false, + 'q': false, + 's': false, + 'd': false, + 'click': false, + }; + private mouse: any = { + x: 0, + y: 0 + } + private angle: number; + private speed: number = 3; + + + constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, private state?: State) { + super(x, y, 10, context, canvas); + this.isMoving = false; + this.angle = 0; + if (this.state === undefined) { + throw new Error("State is undefined"); + } + this.initialize(); } public draw(): void { this.context.fillStyle = 'rgb(0, 255, 0)'; - this.context.fillRect(this.x, this.y, this.radius, this.radius); + this.context.beginPath(); + this.context.arc(this.x, this.y, this.radius, 0, 2 * Math.PI); + this.context.fill(); + this.context.closePath(); + } + + private initialize(): void { + this.keyEvent(); + this.clickEvent(); + this.mouseEvent(); + } + + private keyEvent(): void { + document.body.addEventListener('keydown', (e: KeyboardEvent) => { + this.inputs[e.key] = true; + }); + document.body.addEventListener('keyup', (e: KeyboardEvent) => { + for (let i = 0; i < Object.keys(this.inputs).length; i++) { + if (Object.keys(this.inputs)[i] === e.key) { + this.inputs[e.key] = false; + } + } + }); + } + + public update(): void { + this.draw(); + const keys = Object.keys(this.inputs); + const keyDown: string[] = []; + for (let i = 0; i < keys.length; i++) { + if (this.inputs[keys[i]]) { + keyDown.push(keys[i]); + } + } + if (keyDown.length !== 0) { + for (let i = 0; i < keys.length; i++) { + if (this.inputs[keys[i]]) { + this.isMoving = true; + switch (keys[i]) { + case 'z': + this.t.y -= this.speed; + break; + case 'q': + this.t.x -= this.speed; + break; + case 's': + this.t.y += this.speed; + break; + case 'd': + this.t.x += this.speed; + break; + } + } + } + const dx = this.t.x - this.x; + const dy = this.t.y - this.y; + if (dx !== 0 || dy !== 0) { + const angle = Math.atan2(dy, dx); + const velocity = { + x: Math.cos(angle) * this.speed, + y: Math.sin(angle) * this.speed + } + if (Math.abs(dx) < Math.abs(velocity.x)) { + this.x = this.t.x; + } else { + this.x += velocity.x; + } + if (Math.abs(dy) < Math.abs(velocity.y)) { + this.y = this.t.y; + } else { + this.y += velocity.y; + } + } + } + if (this.inputs['click']) { + this.angle = Math.atan2(this.mouse.y - this.y, this.mouse.x - this.x); + this.state?.addEntity(new Projectile(this.x, this.y, this.context, this.canvas, { x: Math.cos(this.angle) * 20, y: Math.sin(this.angle) * 20 })); + } + } + + clickEvent(): void { + this.canvas.addEventListener('mousedown', () => { + this.inputs['click'] = true; + console.log(this.inputs['click']); + }); + this.canvas.addEventListener('mouseup', () => { + this.inputs['click'] = false; + console.log(this.inputs['click']); + }); + } + + mouseEvent(): void { + this.canvas.addEventListener('mousemove', (e) => { + this.mouse = { + x: e.clientX, + y: e.clientY + } + }); } } \ No newline at end of file diff --git a/src/entities/Projectile.ts b/src/entities/Projectile.ts new file mode 100644 index 0000000..07ea729 --- /dev/null +++ b/src/entities/Projectile.ts @@ -0,0 +1,27 @@ +import Entity from "../models/Entity"; + +export class Projectile extends Entity { + private speed: number = 3; + + constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, private vlc: { x: number, y: number }) { + super(x, y, 10, context, canvas); + this.v = { + x: this.vlc.x, + y: this.vlc.y + } + } + + public draw(): void { + this.context.fillStyle = 'rgb(0, 0, 255)'; + this.context.beginPath(); + this.context.arc(this.x, this.y, this.radius, 0, 2 * Math.PI); + this.context.fill(); + this.context.closePath(); + } + + public update(): void { + this.draw(); + this.x += this.v.x + this.speed; + this.y += this.v.y + this.speed; + } +} \ No newline at end of file diff --git a/src/entities/Warrior.ts b/src/entities/Warrior.ts index 18f14a0..ae09dd5 100644 --- a/src/entities/Warrior.ts +++ b/src/entities/Warrior.ts @@ -1,8 +1,8 @@ import Entity from "../models/Entity"; -import GameState from "../vendor/GameState"; +import State from "../vendor/State"; export default class Warrior extends Entity { - constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: GameState) { + constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { super(x, y, 10, context, canvas, state); } diff --git a/src/models/Entity.ts b/src/models/Entity.ts index 168e53a..77ecb58 100644 --- a/src/models/Entity.ts +++ b/src/models/Entity.ts @@ -1,25 +1,37 @@ -import GameState from "../vendor/GameState"; export default class Entity { public x: number; public y: number; - public dx: number; - public dy: number; + // v for velocity + public v: { + x: number, + y: number + } + // t for target + public t: { + x: number, + y: number + } public radius: number; context: CanvasRenderingContext2D; canvas: HTMLCanvasElement; - state: GameState; - constructor(x: number, y: number, radius: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: GameState) { + constructor(x: number, y: number, radius: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement) { this.x = x; this.y = y; - this.dx = 0; - this.dy = 0; + this.v = { + x: 0, + y: 0 + } + + this.t = { + x: 0, + y: 0 + } this.radius = radius; this.context = context; this.canvas = canvas; - this.state = state; } public draw(): void { @@ -30,11 +42,4 @@ export default class Entity { throw new Error("Method not implemented."); } - public move(): void { - throw new Error("Method not implemented."); - } - - public collision(): void { - throw new Error("Method not implemented."); - } } \ No newline at end of file diff --git a/src/models/Prop.ts b/src/models/Prop.ts index e69de29..e54e6fb 100644 --- a/src/models/Prop.ts +++ b/src/models/Prop.ts @@ -0,0 +1,20 @@ +import State from "../vendor/State"; +import Entity from "./Entity"; + +export default class Prop extends Entity { + + constructor( + x: number, + y: number, + radius: number, + context: CanvasRenderingContext2D, + canvas: HTMLCanvasElement, + state: State) { + super(x, y, radius, context, canvas, state); + } + + draw() { + this.context.fillStyle = 'rgb(0, 0, 0)'; + this.context.fillRect(this.x, this.y, this.radius, this.radius); + } +} \ No newline at end of file diff --git a/src/vendor/Game.ts b/src/vendor/Game.ts index 5204dbe..a8dec88 100644 --- a/src/vendor/Game.ts +++ b/src/vendor/Game.ts @@ -1,7 +1,5 @@ - - import Renderer from "./Renderer"; -// import State from "./State"; +import State from "./State"; export class Game { @@ -11,14 +9,17 @@ export class Game { then: number; elapsed: number; renderer: Renderer; + canvas: HTMLCanvasElement; constructor(canvas: HTMLCanvasElement) { - this.renderer = new Renderer(canvas) + this.renderer = new Renderer(canvas, new State()) this.then = 0; this.elapsed = 0; - this.fps = 1 - this.requestId = 0 + this.fps = 60; + this.requestId = 0; this.isRunning = false; + this.canvas = canvas; + this.autoResize(); } public loop(): void { @@ -29,7 +30,6 @@ export class Game { if (delta > frameInterval) { this.then = now - (delta / frameInterval); this.elapsed++ - console.log(this.elapsed) this.renderer.render() } } @@ -42,5 +42,13 @@ export class Game { return this.fps; } - + private autoResize(): void { + const resizeObserver = new ResizeObserver((entries) => { + const { width, height } = entries[0].contentRect; + this.canvas.width = width; + this.canvas.height = height; + this.renderer.render(); + }) + resizeObserver.observe(this.canvas); + } } diff --git a/src/vendor/GameState.ts b/src/vendor/GameState.ts deleted file mode 100644 index d5b8380..0000000 --- a/src/vendor/GameState.ts +++ /dev/null @@ -1,90 +0,0 @@ -import Entity from "../model/Entity"; -import Player from "../entities/Player"; - - -export default class GameState { - enemies: Entity[]; - player: Player | null; - projectiles: Entity[]; - loots: Entity[]; - score: number; - level: number; - context: CanvasRenderingContext2D; - canvas: HTMLCanvasElement; - - constructor(context: CanvasRenderingContext2D, canvas: HTMLCanvasElement) { - this.enemies = []; - this.player = null; - this.projectiles = []; - this.loots = []; - this.score = 0; - this.level = 1; - this.context = context; - this.canvas = canvas; - } - - public addEnemy(enemy: any): void { - this.enemies.push(enemy); - } - - public addPlayer(player: any): void { - this.player = player; - } - - public addProjectile(projectile: any): void { - this.projectiles.push(projectile); - } - - public addLoot(loot: any): void { - this.loots.push(loot); - } - - public removeEnemy(enemy: any): void { - this.enemies = this.enemies.filter((e: any) => e !== enemy); - } - - public removePlayer(): void { - this.player = null; - } - - public removeProjectile(projectile: any): void { - this.projectiles = this.projectiles.filter((p: any) => p !== projectile); - } - - public removeLoot(loot: any): void { - this.loots = this.loots.filter((l: any) => l !== loot); - } - - levelUp(): void { - this.level++; - } - - addScore(score: number): void { - this.score += score; - } - - getScore(): number { - return this.score; - } - - getLevel(): number { - return this.level; - } - - getEnemies(): Entity[] { - return this.enemies; - } - - getPlayer(): Player | null { - return this.player; - } - - getProjectiles(): Entity[] { - return this.projectiles; - } - - getLoots(): Entity[] { - return this.loots; - } - -} \ No newline at end of file diff --git a/src/vendor/Renderer.ts b/src/vendor/Renderer.ts index ef6dfa9..9802d58 100644 --- a/src/vendor/Renderer.ts +++ b/src/vendor/Renderer.ts @@ -1,24 +1,43 @@ import Ui from "./Ui"; +import Entity from "../models/Entity"; +import State from "./State"; +import Player from "../entities/Player"; export default class Renderer { context: CanvasRenderingContext2D; canvas: HTMLCanvasElement; - constructor(canvas: HTMLCanvasElement) { + state: State; + entities: Entity[] = []; + + constructor(canvas: HTMLCanvasElement, state: State) { const context = canvas.getContext('2d'); if (context === null) { throw new Error("Canvas not supported") } - this.context = context!; + context.imageSmoothingEnabled = false; + this.context = context; this.canvas = canvas; + this.state = state; } - public render(entities: Entity[]): void { + public render(): void { Ui.draw(this.canvas, this.context); + let playerInstance = false; + for (let i = 0; i < this.state.entities.length; i++) { + if (this.state.entities[i] instanceof Player) { + playerInstance = true; + } + this.state.entities[i].update(); + } + + if (!playerInstance) { + this.state.addEntity(new Player(0, 0, this.context, this.canvas, this.state)); + } } +} -} \ No newline at end of file diff --git a/src/vendor/State.ts b/src/vendor/State.ts new file mode 100644 index 0000000..489feb6 --- /dev/null +++ b/src/vendor/State.ts @@ -0,0 +1,45 @@ +import Entity from "../models/Entity"; + + +export default class State { + entities: Entity[] = []; + score: number = 0; + level: number = 0; + + constructor() {} + + public addEntity(entity: Entity): void { + this.entities.push(entity); + } + + public removeEntity(entity: Entity): void { + for (let i = 0; i < this.entities.length; i++) { + if (this.entities[i] === entity) { + this.entities.splice(i, 1); + } + } + } + + public clear(): void { + this.entities = []; + } + + levelUp(): void { + this.level++; + } + + addScore(score: number): void { + this.score += score; + } + + getScore(): number { + return this.score; + } + + getLevel(): number { + return this.level; + } + + + +} \ No newline at end of file diff --git a/src/vendor/Ui.ts b/src/vendor/Ui.ts index 8cefd11..5a3c3e7 100644 --- a/src/vendor/Ui.ts +++ b/src/vendor/Ui.ts @@ -1,9 +1,10 @@ -export default class Ui { +export default class Ui { + static bg: string = '#709775'; public static draw(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D): void { this.clear(canvas, context); - context.fillStyle = 'rgb(0, 0, 0)'; + context.fillStyle = this.bg; context.fillRect(0, 0, canvas.width, canvas.height); } From 76e38ab648bc657bf4ea49a84aaed6d6dd741313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20THOMAS?= <74055963+SebastienThomasDEV@users.noreply.github.com> Date: Wed, 20 Dec 2023 17:25:27 +0100 Subject: [PATCH 05/14] stop player from guiting canva --- src/entities/Player.ts | 38 ++++++++++++++---- src/models/Prop.ts | 6 +-- src/sprites/character/Character_RollDown.png | Bin 1184 -> 0 bytes .../character/Character_RollDownLeft.png | Bin 1307 -> 0 bytes .../character/Character_RollDownRight.png | Bin 1272 -> 0 bytes src/sprites/character/Character_RollLeft.png | Bin 1114 -> 0 bytes src/sprites/character/Character_RollRight.png | Bin 1076 -> 0 bytes src/sprites/character/Character_RollUp.png | Bin 1135 -> 0 bytes .../character/Character_RollUpLeft.png | Bin 1152 -> 0 bytes .../character/Character_RollUpRight.png | Bin 1108 -> 0 bytes .../character/Character_SlashDownLeft.png | Bin 864 -> 0 bytes .../character/Character_SlashDownRight.png | Bin 881 -> 0 bytes .../character/Character_SlashUpLeft.png | Bin 934 -> 0 bytes .../character/Character_SlashUpRight.png | Bin 895 -> 0 bytes .../character/Weapon/Sword_DownLeft.png | Bin 591 -> 0 bytes .../character/Weapon/Sword_DownRight.png | Bin 477 -> 0 bytes src/sprites/character/Weapon/Sword_UpLeft.png | Bin 540 -> 0 bytes .../character/Weapon/Sword_UpRight.png | Bin 580 -> 0 bytes src/sprites/character/down/Character_Down.png | Bin 917 -> 0 bytes .../character/down/Character_DownLeft.png | Bin 797 -> 0 bytes .../character/down/Character_DownRight.png | Bin 842 -> 0 bytes src/sprites/character/left/Character_Left.png | Bin 762 -> 0 bytes .../character/right/Character_Right.png | Bin 787 -> 0 bytes src/sprites/character/up/Character_Up.png | Bin 905 -> 0 bytes src/sprites/character/up/Character_UpLeft.png | Bin 818 -> 0 bytes .../character/up/Character_UpRight.png | Bin 821 -> 0 bytes 26 files changed, 32 insertions(+), 12 deletions(-) delete mode 100644 src/sprites/character/Character_RollDown.png delete mode 100644 src/sprites/character/Character_RollDownLeft.png delete mode 100644 src/sprites/character/Character_RollDownRight.png delete mode 100644 src/sprites/character/Character_RollLeft.png delete mode 100644 src/sprites/character/Character_RollRight.png delete mode 100644 src/sprites/character/Character_RollUp.png delete mode 100644 src/sprites/character/Character_RollUpLeft.png delete mode 100644 src/sprites/character/Character_RollUpRight.png delete mode 100644 src/sprites/character/Character_SlashDownLeft.png delete mode 100644 src/sprites/character/Character_SlashDownRight.png delete mode 100644 src/sprites/character/Character_SlashUpLeft.png delete mode 100644 src/sprites/character/Character_SlashUpRight.png delete mode 100644 src/sprites/character/Weapon/Sword_DownLeft.png delete mode 100644 src/sprites/character/Weapon/Sword_DownRight.png delete mode 100644 src/sprites/character/Weapon/Sword_UpLeft.png delete mode 100644 src/sprites/character/Weapon/Sword_UpRight.png delete mode 100644 src/sprites/character/down/Character_Down.png delete mode 100644 src/sprites/character/down/Character_DownLeft.png delete mode 100644 src/sprites/character/down/Character_DownRight.png delete mode 100644 src/sprites/character/left/Character_Left.png delete mode 100644 src/sprites/character/right/Character_Right.png delete mode 100644 src/sprites/character/up/Character_Up.png delete mode 100644 src/sprites/character/up/Character_UpLeft.png delete mode 100644 src/sprites/character/up/Character_UpRight.png diff --git a/src/entities/Player.ts b/src/entities/Player.ts index 8a481c1..51b77e2 100644 --- a/src/entities/Player.ts +++ b/src/entities/Player.ts @@ -17,7 +17,7 @@ export default class Player extends Entity { y: 0 } private angle: number; - private speed: number = 3; + private speed: number = 10; constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, private state?: State) { @@ -72,17 +72,38 @@ export default class Player extends Entity { this.isMoving = true; switch (keys[i]) { case 'z': - this.t.y -= this.speed; + if (this.t.y > 0) { + this.t.y -= this.speed; + } else { + this.t.y = 0; + } break; case 'q': - this.t.x -= this.speed; + if (this.t.x > 0) { + this.t.x -= this.speed; + } else { + this.t.x = 0; + } break; case 's': - this.t.y += this.speed; + if (this.t.y < this.canvas.height) { + this.t.y += this.speed; + } else { + this.t.y = this.canvas.height; + } break; case 'd': - this.t.x += this.speed; + if (this.t.x < this.canvas.width) { + this.t.x += this.speed; + } else { + this.t.x = this.canvas.width; + } break; + case ' ': + // dash mechanic + + break; + } } } @@ -108,18 +129,19 @@ export default class Player extends Entity { } if (this.inputs['click']) { this.angle = Math.atan2(this.mouse.y - this.y, this.mouse.x - this.x); - this.state?.addEntity(new Projectile(this.x, this.y, this.context, this.canvas, { x: Math.cos(this.angle) * 20, y: Math.sin(this.angle) * 20 })); + this.state?.addEntity(new Projectile(this.x, this.y, this.context, this.canvas, { + x: Math.cos(this.angle) * 20, + y: Math.sin(this.angle) * 20 + })); } } clickEvent(): void { this.canvas.addEventListener('mousedown', () => { this.inputs['click'] = true; - console.log(this.inputs['click']); }); this.canvas.addEventListener('mouseup', () => { this.inputs['click'] = false; - console.log(this.inputs['click']); }); } diff --git a/src/models/Prop.ts b/src/models/Prop.ts index e54e6fb..b3d038b 100644 --- a/src/models/Prop.ts +++ b/src/models/Prop.ts @@ -1,4 +1,3 @@ -import State from "../vendor/State"; import Entity from "./Entity"; export default class Prop extends Entity { @@ -8,9 +7,8 @@ export default class Prop extends Entity { y: number, radius: number, context: CanvasRenderingContext2D, - canvas: HTMLCanvasElement, - state: State) { - super(x, y, radius, context, canvas, state); + canvas: HTMLCanvasElement) { + super(x, y, radius, context, canvas); } draw() { diff --git a/src/sprites/character/Character_RollDown.png b/src/sprites/character/Character_RollDown.png deleted file mode 100644 index 2422b399d8cae8b225adb5a9cc4ec2ad9af28f47..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1184 zcmV;R1Yi4!P)x$4{3$390vmscW~z``S&C#% z$iV}F4vqsuOVj`fQdo<8!8jpdNWzR?{1caZP^*xcP>09f0$Z!8bWp-vm0D9z|U4vn^> zZei|j0RTeR!+g0U5wwp+5}xw74*xm>bR7psJGyP#NJkz6z|wo#;yzit&j=3y z6EqRT(aHqry(f$JT5`F}fD<(75pF`+_*UIyx{iaB#e%hM+nO(zb(^UJ3tbNY5X2FH z)d&Rc#?{D>C~NV(B9nfR3wq$GNqn^yAf9SdELi_mo( z>twO0(p+5_24i441_tBoo#`0RU;w11t0+EH4UVb?4B`lPHa4ucA3l_}UA42g=rrnx zAdc2y0>01@&DZPo002pn&~4Qwkm`IwLlvlHKp8!1tpW1l)3uYuLhC#%#-`2)U5^V- zM$m;Y*On5*5!$wWZSl)?7h+CBoMDJFgs#W+C~IHZ07Z$A2%;FeOhEDJ3j)C0-Qu5< zBoQls7J@h`Gyz?kpqaU^qWsU7OTOmG$1eITR)MbLlp0Xl0AeD*Me%i-pzAo9`1w*O zx*jMWHFXH$2(yFX1ryMmn=ikr=sJ!?Z5A0Yh$FP)(@cD-J~Gp>REcWa_LTwD2`@#zBqK%+mx{oNg<=A)^kHRxpGQ|xbk z`IQ@io{^>00JQx8t`?u5XH+eM@>7otK^&pCxv6v~@aX86)r4P~`6u7toFGBSeNSuCT45;fM2I&;=W;#P_ z(7~JOEK_#rT5HfDp_Im+9Srf~?=JzYw!2huh(>?J4};qAsdRjq3V=Hs8@w?Y5oA}O zGSpc~tBgqp7wUWLUjD@)f>ij>_3-!I?{Ro| z2-V^Pz~krza4sg>+uK{kkE4|dU;oBUz$?^4xwh++s`ImW0l|ej&f+uY;g_+lLJB&>utrS(8w>`6!C){LD)SFBPn6!!IN8qt0000 diff --git a/src/sprites/character/Character_RollDownLeft.png b/src/sprites/character/Character_RollDownLeft.png deleted file mode 100644 index 3d303be9327aa4e57d015dadc51d67a3a31e5917..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1307 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 diff --git a/src/sprites/character/Character_RollRight.png b/src/sprites/character/Character_RollRight.png deleted file mode 100644 index e45ea115ccf61a814fa75846297e27123aca517f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1076 zcmV-41k3x0P))MHWhc)VZP%ca%%8!4*>iX05f$x@8uOKtSUOua~fGYf>t0F z<~yz^&IwQf0JKL#3R=6KObSRPF?Ai$bjspmEt*cHUHf-NEV@PC$#W0)2v8yKH?D84 z4)9*zOMOoaBywuJJ;~S?Q1(aDDf1l{zT;9NFjU_bzJ3;MaqPOoZL|3H>(`lzd+QEB zSr|FB%>T1ZMsCve9hY|&G&+wT=yw1>qwXNH>xkMS6vw%HqJ0^Dm7=wQ)UGQ+erNF@ z&w{MF_ecpQi*EbnKaXeTiw@hiF&>ZkX-X0qNwBW%nxk9H23_o*UEuWSP`mo}y^z*A zD6z8^Ue==9`W9g7x<;8Fpu*b^y^6;agsJONs`3B7Kg(Xr}|Ms*c{#R_p@|}tkmmu z?eg*x=ywn{J%mjU0Qln5qAxy;$hc3Z&4bQY8y1~n=k7^iy;ZC$Jo261-=_QJ+5{dO z1`h@RKDRz{Va6ERqahDa*r*la=H>>7+Zh=?9*=n<0Lu#{OM4x&K^J%ZkRN{;el10B zUEz`MVbg;-y2U5+P3Df9D*>)26YTBnX#imL0^IdOd~SWjY|sTTH=;cn@~q6#inhh- z1qhoSsts#ng%%~P?FOjet{?JP$|kMw$oFc)LfG{1U-Tn$66Q*PgM$M+JUlSE2V1=W z`)3ybfQPF~UJ;mAtkmxS&fOD@w5kn@>z0MsdI>QhtX=@ywgEt#H>=m{(qoBz)U1l3 ziG>F|`fl|CoE{xYFE|kffZ4XqN7(dO*z^+5$frk#WySyiT+?@47CE)0;X5wHQ&@{M z=$iCO9)RZF=&)9+B};c@EGu>6@$S!8CCP$ss2CcZg!pXGWwSxI%tHn?^Fy7cCg#;r?58yXMOun*z u8(8w+FvY*0o%afbLZMJ76bgkR55E9;hny{scUN%$00008?R=PNCQ0KoQLKtEH73xH4xr^(&)jmJ_7&oOupR^PR`HixJ;=-Y$5OFKe>94fbFV)d z-AarmaU%!;sCOa&QsYR>`8U6pW z=^v!x`=b%6GwPiv2JXZGU!VWUgTMa1QN|=7@<1xT%3%IYK`4H(8iW#jYFUd_sto7{ z>$F(S?0;>cstBog)fp{TfqYv*xcP+W=;O5`ws*(J_O5!57ae`HYFC1OSpZBbB9Cl1 zWn1i|^+>si|4kw_Zj2*5iMq5i)F3c?_EKJkPdSK=B@&gf~)lRyKT}PnVa;vs8cEu&*@+n54j|R$pZ>GYg1- zJo>_<)iz<$A+P*sQ)X6|nFK}INi_ppd^ZLJKf^24i z*IS#On(~F-PL->}J)~<`Yqog3wP`KzO2wjAHym6xf{^a{xvdP4*w<Dgy+ML{EFqa+(+pvE!940|n z??elOK>B|Z4O#&p0!$*%rV|)1v?Tq%WTPsBBnTuR3YD7GJ{YcB;WFm~$t=L;EJ%V- zGT$N{y`8F&2Hh@T>U^GnJg0(cMXOzp1c8*9_SZV7tq^X$zqCBLp4Wny%yd)@>7(4E zt3kU9n3o080`h19!n|paIxtnQ+Z=<4L)A1}>YOI$<#L&Fxopi|nsD=pSE$qy+v;FX z^?jjI*7J zbVF2w@SlG_Xur4dQm_ahm1KtR{BBh-x;os}@ibz|teY(~L&8~EMuOANCsTr$wg z%8GY!aiR50Be8i1&_#k^)=Ff0UA)ve&22wI-4iJliyn}4HFvZV2$NY6aJ?S_vL|9x zr&*)bngA2j08|4(9hgn-^WF`=(M=JM6fCR$5{4~npa5x+ehr{8>%6|jzey<8-p(FqR002ovPDHLkV1oYB BBAEaH diff --git a/src/sprites/character/Character_RollUpLeft.png b/src/sprites/character/Character_RollUpLeft.png deleted file mode 100644 index ff131d0e7e4f21ac3ed9e585626ecc232f85f737..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1152 zcmV-`1b_R9P)Nkls)YLl-4QU34ti7x6@y-hY*YOSEy5wE>06H zH!d8h2u7VFbBMsn2FKX9jgS?RYEnqE+Fk3%O4>aIGaoFclSX=P-h1=ry%|s_6bgkx zp-?Ck3WY+USioI910VvAVhe2352A18g=<>d4*)#}(f~I;-Kr`V$yeFSkbocFi#xEO z>o7mkL@t|MNB-LE?(UBD@%n;d1&S4T@l;Iod?yL|;P=7bqHpGDaFueq1;8g)uB`l< z+6rTZYgz!2hY0Q5>Wqh>X=Ab4>0O!P|jO5fSk76o0`qQ(JUBk6bpZ zmD?@i`eGHi3yZ+6=gA=OGl3+HbRVTZ7|h&j`(~b8y@A{pVeA+H>hQhwnf6QTk3@AD z4u@h^?Qs#vg05Gxr}LZ>gt7*RTYtn_CDC?g)(S|v_zB9LCr`c?>4mkw-n`jZc7$oy zF$sb=SpXo{K80kmkq|o|Py=(o_gB%Ke$ zZe?T(G|?fI7SJ^<%6qrz0LEAj_L9nc?3B}uWq*17V;;YJg{EoK+NU0rF=ie6#|nVQ zFJFmX$>v7!ke%a`@m~DsXX>|p@T+ftTsEt9J&!vFH+gXOEVdHs5Hxzo3FBT^kU|US zzJ5q~?{-LTx!szZjwe02w&}S@)c!Eu2J5%Jo@xJ8h6L7L{AYUPXc&h7T23@= z=SPw4QLcT;)8~I`-|X+FcM^E7GCqAQRw%byp@X0XSN#N~XDK^yA>+SJ z>Rw`-_KzYQ#|cfAV#RkxS_%Xm5OsR=|1{)Bh|X6Ao&dz;LME+Csjb-G=jiANf|2;q zb$Ht#sWCYh5c8gkxCxAAgVY5E*tY#=5;VxWd=>kDMcH4WP$(1%g+ifFtj2#V1d$c` SA}D?U0000SFFHvbh01c8wL3U3-RdMJv> zOD1XP;4N?v2mvG;a0*0P1Y2#l>mVL;lqu^dk+Ol_2OuDcr+e?d@7>)y5)1}|!C){L z3=VPCHr1z+jqUGjR(H~;GF-#$;nAp_CiXw&FN@#E$IvS zyfqk)vox)urG3e^t<%v+tHZC8STE%B494SF2LLJzLq}xWG>8-5|NNHU|NTe(N#t@l z>+B5Qt2(*<{jgFljf!d#|7EKP)Vdue?FPMr3IG>Jz3|?o-C*~yMTH^vU%UcH#deZ01N06m z9QV&sKMf`EW40Dp)Ly#=7>vikGPrmJShgU!0So2S(L1QHd)R`*>320525vF553AK8 za?FcnUWFmJ-qa@`m&=i6Euy3S%vMAppV!hoQ4x3(s_4>@Gz+8Oyzj01FJ6U7pzg_q z!xjU#7`9bcprA<0^n{=i)FP?cEZMeI>vjZll2O)N@MUu7CcUnH50J;h z)790rqmbPD;`~|B51}U3JftnP5DiE(31an^wq@Xj5LlL_ZCkqrI30~-CWeO9OYYH< z2lu|w?OKbHZS(2sO8Blf_b3#{s!2Gzvc=sCgTMVNlS$<{k zDc3mxII2Sivp>!!Sg-yn)923Rx^NsPyindyk>PpRdH4{Nmm$9Eg`ef+yIyL~0fIil zKGo0RZ{>RAepMMt_6BU@#aA27|${ a8vg-|0)?5&tGM_80000001Be1^@s6m49>f0009gNklT8qvN`b z!z?Rt`SZO|J|F-yJFxzEAgM40_Sfq_4TOj&=>?SZ0>UIA3DfI9mCwNB8l+rrJZ}v$ z68_Y2RgVDduh*|bNiU%BpikpLpNNRYgFYp_fFj}3c+fYt7@7}8B|NkrL@$WKIqbhA zegMGM`YNQPxP@Uz0O-Zz2bFs@RFdl*SQdl)%wQp~zh3`o5VG?jPd?d_O^ToU50PJg zssro$@_Q47z?%V?a-1zv($O81N7yv+z0|0J4eW?~h^U0B^ zIM`p8U$25$o5ii&y{`VsQU ql6Cw?_ow1uKf^E#!!QiPoaG-2DU`)Ecz2ut0000001Be1^@s6m49>f0009xNklVQx1fUi7|YV<(Do9f{e0KEGB0HVgwhQ6zoKM#rkd#Q^}I zg$h=;w?OCfxDnI{-F{y+0_DM|2{6M(V1G9Ll8%syFD3#oEIL92Pk4a;^IKv6v`0vdbzDf z*{|)O%M9wG4x#Z9D1&lQs(iii1aO6d40=PuceeK)2rHIq5h|f^Xxx^)SHk| z6_)^D&~BoI3PR6C)iEKZMAb1-bxizv`)0b2q(Q?lO4Mlp@b3K+P%Z$dIwnHT#m7!} zCXJBR-|p_KhGl;;+S8}`fHH(FR(UMhXw{jGaxZOn>+@F0@Z2$MM` z$b3m~mVeN0#U5-3GP9=IoVx_T}Y3W<)uRW4{lt~(#w7d06@Z78`}j_D9pG3pWic&GsJSh zSvx*v|Ap~iohF2D**LoO2kXGtVt;QloaS;`eEJru@1R!a9|N0f!%X{MaP;UQ*4cll z{lLj7ZtPsf)7{yAqo)2GJCTX~m($NM48t%C!!QiPFc*}+4aqYY;?}fy00000NkvXX Hu0mjfwPc001Be1^@s6m49>f000ARNklFq zpy~MxHa81N#I@m2H9gvv7*PK!0iltU3;FhFeNkyn_hXh*!ctrgrh7wb1JlQ8Hg#{~{)x zYr|pYdm{1Y??0I~Krg$|SQZavF9J4LI7%cFR2_%aZ{HGC_aJZ9h`$(rY6Wy{+tLS) z$?Q-0+|i?AH}R+W(t1pmhamA6<4;Y%^QTV%JEz`h6ETZrxav5j{HHl1N|`SG;xEoW z%^^hf8v*5ZgnoscB-?9tWVtEUC+~q*!R@UZFOb+616dz_#QzugHLBkT6rhw+D*j$K zqX%9^H9cQ7JwNldz+?Bai9!3dD;@_qHD<-1uRnDOzIEw>rK{ejXHOZew760)oBI(Z zGZXYI$g&;qYVbw%8-a0-R$2f&59iDnU2!v$ioY;_>M8i$*Y5zk%Wr${ae9B4VF@-f zs@#s~6L5u>#`Uvz2hLvwz{#fv2Eg@mwpDINO8m#;2jJedSpYR!X_@7gWQRPDB|!BX zff}u}RKF3ZTCHY0VHlb?!Z0*HpZ9NM>fi4Ye|G+<%jUd)gXQiA(~xf8eMG0zG51$j zR}cNn6cVNX^HQHD??0NGb8~Yv-oD1|t)s`Cx&I;;|L3JXfcSIpr!K*oR;%I!$)x8wtKn4wmyQEo?!>aEP&lf?fBv56-Bg8YVY zqft>`+`Y!-H_ww;jHGuWhKD!HDIcF6z3uqz5`Q87{YbQ10L8gWCeXb8D!IUNe%H3aaf^;g{KxAT1VIo4K@bE%5QKl2U$Ytv>}Es#2><{907*qo IM6N<$f-|GREC2ui diff --git a/src/sprites/character/Character_SlashUpRight.png b/src/sprites/character/Character_SlashUpRight.png deleted file mode 100644 index 835cbbb839b17c20482995b2a3b248da13c1bc12..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 895 zcmV-_1AzRAP)001Be1^@s6m49>f0009^Es2e;QojE9aAt6NLR8$bT>A5(5N55ClOG1VIqQYW7tb0C@6B z{s-po0(@p{a{&Cc@`U_r?0;0Z0I~hgQ7|*M@g@Ar0dP%G$iIgEx&ee^n3x&cIA&4n zqp+{4KqmA6FvfEL@kMY96SFYn0C;jN`PalRRW0Vl#s+?0UUDD~p@lg#?<`xaG-a&n z$KD1PE+9U6+jEiKm)+k&4didhUp0Wa;AIgbqKyk6LSN)vq>VoU`P=iCz6FbW{IVh} z%wb^;$1pKvtg+s(ud3;c3tR_%6`}_6ci;cGY$3BSEHwgvxG&JWGv34~oHE88!vug* zZVU7DB58_(Ss0SP!~RN(kTTP+D4?lP*jH7__T2RIZ$liDX^hF=VSlLrR#{>%_JXC~ zxP&QV&=f_oJr}m;uDl4M{~-VR_)7&)+>v4@~*u{5u1Zv&RO&-jv@bg_)AaGeN`=NNJaR9p@rVj7@TuRl2qxl zOOo~z%VuLS{R4n3%Tg3YkG0B32JBFEVNP-p&t=!L7Cx=Z25J>(BesFM*4L1kCbzSGW zuIIew?pA|p^0(oydz-uwDqYtB0KV_1`>sw7@$~J7yLfW!t1^yRJhFr@i|_jghF02# zH0dUPJO0uSq8BMD;N#v7u(y*dz}(%E+^^?#H93x11jHw_bXM&hjS;|V#7F-2{q<+S z!3g@#Pw9@u^#7U!=pBvk;#i6-%ZQ?gFAf6V_wnkPB7OgpdjLxQ4*P2dFl|N$qwBqs zQSP8OTQ8;0=RayHZm%KdzVFS;XOO|@8^mWA0LNzU7uchK0TBBcs^&% z-i`87mR8=6|JKFte_&sK{rJa#GgUuh`IL%mY_GeoyxrN?E;Bo~Z~60%>(f6KZ7BF- zbwJzq3TxeqwZ=M+w!S=_?rHhr#MwPNF39Le?dL0)ap~Ta;>qXbLPOWqa;=_a{crtd zyWh-Snx?Vd->nbusQ2fuTIHp=^iB_3kk{My^X&I5eXOsxF*rLnd*+`wvOBV z{rk6NyC>Pby?-<+gYVp%vfUv?dS3nVhzopr03zT6 Ai2wiq diff --git a/src/sprites/character/Weapon/Sword_DownRight.png b/src/sprites/character/Weapon/Sword_DownRight.png deleted file mode 100644 index e3bb6ee2f9c2b68d92ef5e1c28ab6ef951a403e6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 477 zcmeAS@N?(olHy`uVBq!ia0y~yU~~Yo9XQy4WcKrQazKG4o-U3d6?5L+_VqjDAkpw} z{jul@rjqwfymJmN`To!)PEcGSQ~N^v9HY-RoShT(ekp#wrK!jS)K3IxNZ)Y#t&&pT zTz5mS`SM*wxd+W%pD}9G)@hu-U45jN^}eVj+k!7Y760WGud{o#%df17zociq^ZiBU z_4_`H=WXuSux2RCXH2PQ-EedJv;K4U|LoB{6aMnE`1{Rz)(nEF=kNbykGRvMU;FP7 zXM)T5z4OmyGc@m6V#oOF>h13x?+-6XE{^@*yK~h%=il?%xfyGizJ61_y`eY2)jIEL z@BPCG>sR_W{BC)_WR+j9^?tdtKeqDyJ|3*Ze9u7pTF3nvTw(h+GQ8W&8n?3ZBeUBE z*0`DHriK4`(8yx2(E9uLWo_s0iMt6L*dn(7Q1O5MwqkyRwW`@mYYZkaEZgp~V--hu zy}>kwW#;A5I&}sU8SKnIHcPS?>|GiERLFkD$>j%L8@!q=H(mY+f12QdFKx&Ce$3Thne`j&5hnHKr>LmSKqDd z7gkTX)INLNozkw38EWlJa&N2OGJZ)D-Iska`o8<$1x$>0PPTp6D`>yMg`s>yN4(z; zr6c^SeuY&toPXE%W3O%6>BkMS91mpQ^~eXzWXRi+{3oY*p%CMpgy&P|UsVoKYPkJY zQctjjtA=aN%R}OGif?{$Qf4Wbtt(o?KYz8|0$!#c5_;v$Y?b^5hmXBo|HE-Hi$VOi z)VxRRxk3jH9pS&F+E7^&%W9?6@HFC=<71W$3b*4kW-~Ay{$%Trw`~{OuScJ*t(u;m z#2RtJ=KR+7fK$Jk7i{@6H|W;mBF732MxBIN%Vxy?yCUb7dAk1DtaW$ze1#A2nCHLm zdp;vR?lj-BzV(NuDdjX*$}ycdTja;E*iwd1@PGJz&ANZDWyy=E>!)CpTn@Ln$K;%eb2kkQ{R7L z`S3XW=|VBXUw;xauE&26e0hJ{hV;ef<=f1Dao2tM$&4BmNW7SD;zyMtnY+?WL9x!@ M>FVdQ&MBb@0N}OedH?_b diff --git a/src/sprites/character/Weapon/Sword_UpRight.png b/src/sprites/character/Weapon/Sword_UpRight.png deleted file mode 100644 index aa7791d67df8bd73dee10c71647349f529485b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 580 zcmeAS@N?(olHy`uVBq!ia0y~yU~~Yo9XQy4WcKrQatsVi+@3CuAr*7p-a6QOIY5Bz zL1l-wyI;r#lZS5^mnyvS6kf@s_29@vrZb8+6I?lO9x1d*uAiT>cInyPaJlV;8Y zGzl3rl-|rS6BMj<`SM=i@akRJH`C?vjSk;w{~fw=(*Coy2fXh5UL$Wm|Jtd#km;MY zcU;=J&hnXEN=(iLZm%c*4Zbt{39sKF_R*{&sjzW{JL9ztM}&NSx*1Ge_xhP!Vp!zo z`Z?7M_ty4zlohR6k->TV*XmvCve#~X?6-aEwc}l0`@Q~^Fza4f{Qd9LZ|ToBZCBdd zADYB(w(!eO*~aXsM-wIH`7vG#-TF7`h}CK9*K%(njh=01)_J!fer9z~^d8grl?&eo zrt+Ij<@y=SyE!5F-}(G+wYP4{Cb-O$eHQWZdxEg{-1~w*Ja27(|9kb;8*j^Wzs0v~ z&z|z;_{qd?Z_9Qs`_$c0|MR+5@xQM%+|R#!lKS}a^S^)R-*?R4{dm3Ksn_?^PX8AB z@ihC$dimNtd)~3jq{OrS41Vta>&lw*CzN9DMvK}ssjJO&6x4E@F+(=NsF`W^){{m0 zOp1!EGbU`esNiFrFrymrB_ yZr<+jwgP3=8FyOk+hVk=RXLEO2s``0*?&wq-`^Zhc)e*ENQI}XpUXO@geCxp=>!D; diff --git a/src/sprites/character/down/Character_Down.png b/src/sprites/character/down/Character_Down.png deleted file mode 100644 index 79754937ae33e7e78bd7f2a90739414a5fe16441..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 917 zcmV;G18V$1q=p*!C){L3IED#01(a1#wyeTQr=v|ftNvpJShg&jbULJ(9EF&WxtW>5S-&4= z_*jS-CP)CFDFIpbA9sRcn+G7owXMQ?kbunkANv)sP%5F_?OrP=f|9}64ghE?gOivS zob~&pBLP|WKlTM^ce_*%45?SbD%9d3SC9USUmrU0@7Yp8L zsQlGD7Yn75mhu-(fXY?(Y8++Dy7lVIyMF+~w-5lesuj7-soM^THnm;(tc74ZCH3mI zBlq5-O|+}`LCjjnyMF-Yi(o4gyf$bjFyxpsb2$KV9Nt1x9t%i!nx= z^Q6jPs{X~eqRe0ApK~7i!_=m;5^LaCZ|J78f_?Y+!qC>v)g8ZHv~M~qg?;xpRuD0u zR<(ksTT7T;-v(n$KJa}XqAEmU)~27f|ET^b|D1Dt`t?)J6Eq$Ls}ScLj}E`dBhR|+ z;Lp3a*xlW|>6l|RMac_{4T&aVn1CeoL$RdbKcqjxHvk|qPl?15+G~m}igIZuC})?- z9)^=a*xHfhKMDN^ckVx`&p99Z9yKQud00000NkvXXu0mjfo8r2c diff --git a/src/sprites/character/down/Character_DownLeft.png b/src/sprites/character/down/Character_DownLeft.png deleted file mode 100644 index a76cf65949fa49842bc8c5c5d82fa3f4904fb30d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 797 zcmV+&1LFLNP)Udv z0f4s5AvBbP78Kxo{XgyZfh++c2@}hzJB-E(&^7-L%3S2yHUJO+`gwax5%cBmBIaXz zb3*_qyG|=v0RRA7=HOfMTYCYz?7vh4Y?6c`=3~noM9jx*y}|-R06>6W0MbNkl0-`q znl8Y(`-e9g0RTnJr!8|RVm?9H2E{|XuOX1Xw8hV2E7YjnY5%=r7r3@9SAgjDLo6`F zY`rS=x69&ymyYBz!2q>BtTVGwcww*H%q}y)Mym()1 z#Z~Ed-T%=BaBaIMX5IqB+$uPfjH(W1Ehs>z{g>GRFtM!OY`w}Ieh#4=O#Yh|6rj`o z|7A{590qv(bpar~1(~dW$k%E>RnOsxWyypVS3_|wt1+O$ISk_;L_`#a0mWfJNE0GY z!^mz^pzn4>>J>HBQ@GBP_?i1p1K2I+v|G-Jh-kN*Qyc~qF`stJxd=!Xtth}L_}B8m z*k|VzP)LGengGz3w=b&~{=>Zm06?5`0zlLLLoMQ`;vWFLd-7Oc08k7IVf6!vPySSk z_=gwIWemjv6l2g5K!g6Xg349D&%fA(lo+c;6aWBuo+qC<=fpXefoT!XP!pj(;xN$O z01f-6I1HpN?rA`+4WI^t%PYd=6?vW~gDPx^S{RSVfKmWeR~Ub9J<=sFXu^MP+^7w> zuPL%-)rLH2a?WKkgt)E|Ahe5`g_WnZ^F<5|_)iP&UZk&;21LI2`cc1`f--+<3Lt;0 zb1}`uQiFcu{>8`ljA6vzKT31Sv!jsno?{sl5XmS=h`JzC=-ltnewi@toHqQxova?fY3_1%?7XNp${z1pKXQGn{1+FK~2d;;o z*Vh~f#otp#!?N%-`b8tREzT8DfU^0|)BqjZ#zhoS;CdLjZTPqX01QyY@!2W(eBgSx zh$1B;QfC3m=)aOhn9Z|sW`HUHz{izHhO@UEkd7-KRW%hqJ&4rUU9Jd7!uRUm3R&LW@!?G}%Ojc^j-OowLa|$lA z|5P0+uRkmV0FATn0DzI(7VpEJkJiZ`^|e#7pv}k$NETpK(z3Gc{sG)Pe(=GY4`I(2 zuVK$8BFcSJhP4CNqrmm%HeWEm5hCKrQ(ulDsktrwWoSC=Q{Z|O_I#QS`$R-E9rkHD z?3c0t3AFgtuj;?dQ4Q{KRWn z1pubQJ_e{F?D?p5TOcCTx-HbYEnI$hpSScJy8J)9e^0#E=Krm?4*=vijyNE7B?_2= z4iB1OEgp5r(f8`vQzhD({fij~fAJ51UOs-LAcR;n?D-2xkdvXR035@r zG=Qv+gn!3zlnz4CZIBDm{WLp@`9)47LLLpAjC#GUR9lz-xTzM3y{rMHyyOc{fdGW* zqoX52e21z;bP-`vbb2BdRl;lH|0V`5ywreBrzwA~E`Y%GFs#sAGQxhG7_nVHk#C7=}5_SGx`XwEe9dRo4#y z6p1DQptf!2Zu|-YP<8#A4hV%$0LZnhLhPfU0H8$rUu3DYH9}ee00dYe6h5N$x{zRm z!pA(0jR&C2`ZEoHc^p$Hd_BQ&^ka>6?(hpNjRSqQMp(?s~$8n6<-co1)s;(cv zQzz&}eiH5X`zB}KvjG60Y#R>1Rq9{%0%iOC%NtYX|AGLJ=Xn%~CPkvDk3wU-du=J( z$TMHuZ;OJjAc> zOWgeY0YII9FS-Li}TJc}R`MQ%2qyq-MIF0;C^? zH-U=6^>-egisnbEPjWylKY19G7b0@!i=CxV`jdw*Tdq$3PBBhCJ5LS$R&3ALE&@QU sRx1}@aHaZlafD$QhG7_nVVM7zKTV!EaAoG8V*mgE07*qoM6N<$g4={&ng9R* diff --git a/src/sprites/character/right/Character_Right.png b/src/sprites/character/right/Character_Right.png deleted file mode 100644 index 2ca39842a71eb49d0bdc3b6f4a309c3b2afa10ba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 787 zcmV+u1MK{XP))+M90i6gp`0uW$%K_?LL-*e(VCfOs@?X^Jr& zOrgaP4GqNe+ac0bGHz?7`f%)fK=@16`#$%cPUn-sU@#aA27|$1Fc=Jm<#=Q707&$> zacsIjfK1$d0p!58OAo#R1=w8w-G>h$YJkA?1d#HxsICC(;{PD)7otG&k*>)o9d)(`m-T|D6>mz}op|z6317=SnB!b4B2K45!oF zhrulJs?o^(?E4e<*?;K{;8>Op@-N~>DC@dhMgi8lArMEqIY3lD5N zx3jS70<5e5(g`}2r5hm`D3*?{IZWJroWZDwN!X%UcZweM)G(N54(sZlWdzXA&juHq zH1|^SBEq=s;|xZeIJ>#ez#Yq~#f{vfo;url)NuwQU4*=g55^z({TmpNaod+!zbk?3 z$*kWM5s_KHE916bq7mXPPT=+4(?a5| zKmmXa_mAX6DJ4oNz48@rMaj>RYJ{>10M+|%ZEZo0{&$(B%DC+V@b=Re0EWZi{~RwH zwgcfrlv2sKnrDx!+Si}g3%-~emG~cfhkUsBEC5O={r-F#Wc9yO2blG{0Bj$f>SUrs z3he-T8MJkd`t}ikcduUXr1hNbqtp4_i^`=cf0^~We1AXI9WTj(lcAVgj=e)j4tuS0 z0c2;Vlv|Rwlb+VOjvEhufOI;Y!tcyg=$~>;wn6;74F6O@f$K@9(<$>-Xfj@EOU{T( zx;0$3|3Yu7@=wl4DX9ZAo6TfQO|(i@>i+_&@Q+vte}lnbFc=I5gTZiL`2`?d4qV~I RDp&vj002ovPDHLkV1l%;fNlT) diff --git a/src/sprites/character/up/Character_Up.png b/src/sprites/character/up/Character_Up.png deleted file mode 100644 index d1902e938e3543aa182f2dd5f1beb68b8e2816f9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 905 zcmV;419tq0P)efOU4&UOHaL?V$$Boc{4B9TbK`Ko0A zfc4*}j%oJ~0L;9|Qm_tEv;%M?wieiu}K#G%9Es&^OwD z;Q;dh07uWGrWW&*QA~5PJgB?4MKsP1j{L-GHO#h2g4Yia{vjP8@H# zZsdcIbOO@sU!gQwnw!IQug7Y-fimtw+a6#ZRm(&{)0C#`z9^UweA-+i7*9;vsrEmz z01BFhrt2~QsObg(gbo6)%&KLQ`lpa&0@Ch(D%~(2?G$* z5Gmpi3YwYP z;iP@Z96j&52K7?4OcaYn*tX68M>%OYG}rtW_!F_3s7%VE^+s)a!L$gJQ8L{{FT85i9!#FX?|I zo9YbCIuDHBg&UTDnDmqOFZ3w|n8{?6ZnsP7m|Ot&$9n*P*{vM_e#0Ewwh?F-$pCB{ zt2i<*n4{+b3{QY<+h|qFST3zZs*i;IbNwII9{|AV>8Y4$INo?{+a}LIqyTvW#DUW8 z^9>K5j&B>Q%8_|N>7D|+KiQX4_Z|YKT`& zH&{(K7)rab=?9p&{{j8d?vw8!bI-}A1mSs-KR}U4 fBoc{4k{>4Q| zL*1bhEGS~>ayx``F>jmtC3An$6@KZS^FQa@2L?bQkw_#Gi9{liNOn?HvjD(np9jaG z>jwbt7)6elf{=g&9J+q+mSKt@Xo?cK{WueVL)BkWRRG}B9HYovhAGY?3`8smNTYt{ z^}&*=;%+?VEyDx=SQstb?;?lm!L9Fp{Xjm@xnmR;>}ra_oXI2%)2Y?($ntCac5^1Mrau(oXLdmeAow7H7mqh7_ChL*#>|h zr{c8h-&p`;xQiStjL=1H>+{=2$UelKfVAuPG6OK0T6T$d6Vxb7Kmt;&-^&aDO;OnO z;1*s35#%ULKmt;&-_K@=59<~Roi2bUk?X3BHX|ifWq0GTkM1|4DT)Z_rcKZ~9}w(j zFqQg!6#(ZPj4@Ck+(Dq~3|hR~eq0G4`)Sngdj>eI6#xL4N<&m55!P2AvY&(mBtbtB zg#Mj-0HSrdOZ#Vb&1!@5PsLBof2i%)y)1^X?-6wU3& zW&SL12V&MwgJ8w}8vUUm65tp`Zre83bzL-s7FMJ^TSkjI? zHG5tHh@5=|ZE%l%zb~%+eEpFQ*^ghp_wi#Zm-#bR=yZ{(G@uRc(fRNh{eB<1u8Rf$ z-`<}uEDvE2hkiGI-U%OMc)# zN5eLfpr4k3X|Fw>>1xg8Wgv9cuDi3JN~1&7fOrBBxBj`!Hy7*tiOzwj3~uD~9Ed&u w$t<807*qoM6N<$f)){a*8l(j diff --git a/src/sprites/character/up/Character_UpRight.png b/src/sprites/character/up/Character_UpRight.png deleted file mode 100644 index a406a08e289758257df384914ba87de8c5fb78fa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 821 zcmV-51Iqk~P)Iuq z(y=7LQ}>T(R%_z2x*-7Z)>w*NpJWU0bp7YESuX&866C6wW*lK8R)Dnm&x92QXjW_3 z3<4VtIRFkj=TuBH({tSi2Q`a)Hp`#e+dv$}&QEWDk=mrtzj6Z<4TH<;Ycb8dABP-= zopbihmCUJoqa27Li4`EN{-3ETtC;t`TixK)y@_-j77YWKZXUp9fX#qrwMNf%BflHd z>R-75q{u8Ba@Y)T$a#T;Ie!*O7XZ@fU#S6P_o?vjC{q4I1(GU2n*Bdl5tiM@oVyS1 zi~|%6BeHvVbpI-ScM;D5Y4)$wp%v4l=eqZfAo<_}&~Mn3j~(T+6;XQlf?xqulQxeu z`&SOqp9C0>BWYe57ebK9WI~-CMmwPbq}G3^ApJju{2X@98Ma#h{QlZcT>&I%^dIV1 zz{{l8MF%ypoAGg8|j=Z!spLcPqe-IC_nSzC=dgv9+94P&nV zU!OlkEgeAg8+PE;r(Bk03CpqsAZl+)kqhsZ*755#t2J-=KOeSR{urYsG5`J;nBgA~ zVVXZwr|y>4IlH(L0G4G}87-3mSaAI#_#=uZX|a{$4F7vdXx z_r*hu{{77t)BhekE}nb>G#ZUYqtR$In*Zf55VE<&mw%9100000NkvXXu0mjf Date: Thu, 21 Dec 2023 17:28:40 +0100 Subject: [PATCH 06/14] bow assets --- src/entities/Archer.ts | 5 ++--- src/entities/Player.ts | 29 ++++++++++++++++++++++------- src/entities/Projectile.ts | 6 +----- src/models/Entity.ts | 1 + src/sprites/bow.png | Bin 0 -> 717 bytes src/sprites/player.png | Bin 0 -> 16491 bytes src/vendor/Renderer.ts | 3 ++- 7 files changed, 28 insertions(+), 16 deletions(-) create mode 100644 src/sprites/bow.png create mode 100644 src/sprites/player.png diff --git a/src/entities/Archer.ts b/src/entities/Archer.ts index 4593b41..b15b9ea 100644 --- a/src/entities/Archer.ts +++ b/src/entities/Archer.ts @@ -1,9 +1,8 @@ import Entity from "../models/Entity"; -import State from "../vendor/State"; export default class Archer extends Entity { - constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { - super(x, y, 10, context, canvas, state); + constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement) { + super(x, y, 10, context, canvas); } public draw(): void { diff --git a/src/entities/Player.ts b/src/entities/Player.ts index 51b77e2..0eacd30 100644 --- a/src/entities/Player.ts +++ b/src/entities/Player.ts @@ -5,6 +5,7 @@ import {Projectile} from "./Projectile"; export default class Player extends Entity { public isMoving: boolean; + private weapon: HTMLImageElement = new Image(); private inputs: any = { 'z': false, 'q': false, @@ -22,26 +23,40 @@ export default class Player extends Entity { constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, private state?: State) { super(x, y, 10, context, canvas); + this.initialize(); this.isMoving = false; this.angle = 0; if (this.state === undefined) { throw new Error("State is undefined"); } - this.initialize(); + } public draw(): void { - this.context.fillStyle = 'rgb(0, 255, 0)'; - this.context.beginPath(); - this.context.arc(this.x, this.y, this.radius, 0, 2 * Math.PI); - this.context.fill(); - this.context.closePath(); + this.context.save(); + this.context.translate(this.x, this.y); + this.context.rotate(this.angle); + this.context.drawImage(this.weapon, 0, 0, 16, 16, -this.radius, -this.radius, 24, 24); + this.context.restore(); + // let SCALE = 1; + // const WIDTH = 16; + // const HEIGHT = 32; + // const SCALED_WIDTH = SCALE * WIDTH; + // const SCALED_HEIGHT = SCALE * HEIGHT; + // console.log(this.sprite) + // this.context.drawImage(this.sprite, 0, 0, WIDTH, HEIGHT, this.x, this.y, SCALED_WIDTH, SCALED_HEIGHT); } private initialize(): void { this.keyEvent(); this.clickEvent(); this.mouseEvent(); + this.loadSprite(); + } + + private loadSprite(): void { + this.sprite.src = './src/sprites/player.png'; + this.weapon.src = './src/sprites/bow.png'; } private keyEvent(): void { @@ -128,7 +143,6 @@ export default class Player extends Entity { } } if (this.inputs['click']) { - this.angle = Math.atan2(this.mouse.y - this.y, this.mouse.x - this.x); this.state?.addEntity(new Projectile(this.x, this.y, this.context, this.canvas, { x: Math.cos(this.angle) * 20, y: Math.sin(this.angle) * 20 @@ -147,6 +161,7 @@ export default class Player extends Entity { mouseEvent(): void { this.canvas.addEventListener('mousemove', (e) => { + this.angle = Math.atan2(this.mouse.y - this.y, this.mouse.x - this.x); this.mouse = { x: e.clientX, y: e.clientY diff --git a/src/entities/Projectile.ts b/src/entities/Projectile.ts index 07ea729..a80ce8f 100644 --- a/src/entities/Projectile.ts +++ b/src/entities/Projectile.ts @@ -12,11 +12,7 @@ export class Projectile extends Entity { } public draw(): void { - this.context.fillStyle = 'rgb(0, 0, 255)'; - this.context.beginPath(); - this.context.arc(this.x, this.y, this.radius, 0, 2 * Math.PI); - this.context.fill(); - this.context.closePath(); + } public update(): void { diff --git a/src/models/Entity.ts b/src/models/Entity.ts index 77ecb58..42fa47b 100644 --- a/src/models/Entity.ts +++ b/src/models/Entity.ts @@ -3,6 +3,7 @@ export default class Entity { public x: number; public y: number; + public sprite: HTMLImageElement = new Image(); // v for velocity public v: { x: number, diff --git a/src/sprites/bow.png b/src/sprites/bow.png new file mode 100644 index 0000000000000000000000000000000000000000..5d5438eed68ac9f46134ba0244bf1b7d1311b1c6 GIT binary patch literal 717 zcmV;;0y6!HP)Px%hecn=7(z`-zTRENhvX(&rKU@>Z(%} z)7RzG<2E?x;`GF0-p7ZdcK~+O&6>Wr>V1P%+7JLX<@4!KYJRHIp z+2|c)k*ja>M2IpbrAYH*R!#3 zl&gh2!7E2sEn``A=j7aeV6|G=`vD;YZ8jS$77GxOO*w4+fZhl6I9*9-JV@;a-hMm- z0Ib*R-5+9hz*fr#9-A1R>>Wr>V1P%cY(KzIp{SR*bU)AoLxq%`OGYF8i-6q^=p0R5 zjoJ&qoSaiiXsbR*@I&?kii_dP!srFq0sj-T>12O*y9+)iiKOpBLqZRJHl2`}ls@Xe zcRg{Z6uFAQ^~dAWv(S@xe0o+quKQ|oW-UTr{ydOACi+Jq^mrvzgSPLcmL!~4LZZRo z1zyV2SS|p-a=8Qm)dOt*S&7EYr<7y*9)fYCZDTp~BY00000NkvXXu0mjfJ32t0 literal 0 HcmV?d00001 diff --git a/src/sprites/player.png b/src/sprites/player.png new file mode 100644 index 0000000000000000000000000000000000000000..02d6fc0e0d77b80c0f9dd958f92e94e322338a89 GIT binary patch literal 16491 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 literal 0 HcmV?d00001 diff --git a/src/vendor/Renderer.ts b/src/vendor/Renderer.ts index 9802d58..c515e18 100644 --- a/src/vendor/Renderer.ts +++ b/src/vendor/Renderer.ts @@ -33,7 +33,8 @@ export default class Renderer { } if (!playerInstance) { - this.state.addEntity(new Player(0, 0, this.context, this.canvas, this.state)); + // spawn player in middle of screen + this.state.addEntity(new Player(100, 100, this.context, this.canvas, this.state)); } } From 9e0802dc64256a5563e4cd96d9fb225f7b9c3635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20THOMAS?= <74055963+SebastienThomasDEV@users.noreply.github.com> Date: Thu, 21 Dec 2023 23:26:38 +0100 Subject: [PATCH 07/14] bow n arrow --- src/entities/Player.ts | 189 ++++++++++++++++++-------------- src/entities/Projectile.ts | 52 +++++++-- src/models/Entity.ts | 28 +++-- src/sprites/PngItem_2102882.png | Bin 0 -> 47521 bytes src/vendor/Renderer.ts | 2 +- 5 files changed, 167 insertions(+), 104 deletions(-) create mode 100644 src/sprites/PngItem_2102882.png diff --git a/src/entities/Player.ts b/src/entities/Player.ts index 0eacd30..44d2510 100644 --- a/src/entities/Player.ts +++ b/src/entities/Player.ts @@ -16,35 +16,50 @@ export default class Player extends Entity { private mouse: any = { x: 0, y: 0 - } - private angle: number; + }; private speed: number = 10; - constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, private state?: State) { - super(x, y, 10, context, canvas); + constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { + super(x, y, 10, context, canvas, state); this.initialize(); this.isMoving = false; this.angle = 0; - if (this.state === undefined) { - throw new Error("State is undefined"); - } - } public draw(): void { + let SCALE = 1; + const WIDTH = 16; + const HEIGHT = 16; + const SCALED_WIDTH = SCALE * WIDTH; + const SCALED_HEIGHT = SCALE * HEIGHT; this.context.save(); this.context.translate(this.x, this.y); this.context.rotate(this.angle); - this.context.drawImage(this.weapon, 0, 0, 16, 16, -this.radius, -this.radius, 24, 24); + this.context.drawImage(this.weapon, 0, 0, SCALED_WIDTH, SCALED_HEIGHT, -SCALED_WIDTH / 2, -SCALED_HEIGHT / 2, SCALED_WIDTH, SCALED_HEIGHT); + this.context.beginPath(); + + + + + // draw a crosshair + this.context.moveTo(-this.radius / 2, 0); + this.context.lineTo(this.radius / 2, 0); + this.context.moveTo(0, -this.radius / 2); + this.context.lineTo(0, this.radius / 2); + + this.context.strokeStyle = 'red'; + this.context.stroke(); + this.context.closePath(); this.context.restore(); - // let SCALE = 1; - // const WIDTH = 16; - // const HEIGHT = 32; - // const SCALED_WIDTH = SCALE * WIDTH; - // const SCALED_HEIGHT = SCALE * HEIGHT; - // console.log(this.sprite) - // this.context.drawImage(this.sprite, 0, 0, WIDTH, HEIGHT, this.x, this.y, SCALED_WIDTH, SCALED_HEIGHT); + + // // dessin de la ligne de tir du personnage en fonction du curseur (pour le debug) et un arc de cercle pour le curseur + this.context.beginPath(); + this.context.moveTo(this.x, this.y); + this.context.lineTo(this.mouse.x, this.mouse.y); + this.context.strokeStyle = 'red'; + this.context.stroke(); + this.context.closePath(); } private initialize(): void { @@ -69,85 +84,76 @@ export default class Player extends Entity { this.inputs[e.key] = false; } } + this.target.x = this.x; + this.target.y = this.y; }); } public update(): void { this.draw(); - const keys = Object.keys(this.inputs); - const keyDown: string[] = []; - for (let i = 0; i < keys.length; i++) { - if (this.inputs[keys[i]]) { - keyDown.push(keys[i]); + this.angle = Math.atan2(this.mouse.y - this.y, this.mouse.x - this.x); + if (this.angle < 0) { + this.angle += Math.PI * 2; + } + const dx = this.target.x - this.x; + const dy = this.target.y - this.y; + if (dx !== 0 || dy !== 0) { + const angle = Math.atan2(dy, dx); + this.velocity.x = Math.cos(angle) * this.speed; + this.velocity.y = Math.sin(angle) * this.speed; + if (Math.abs(dx) < Math.abs(this.velocity.x)) { + this.x = this.target.x; + } else { + this.x += this.velocity.x; + } + if (Math.abs(dy) < Math.abs(this.velocity.y)) { + this.y = this.target.y; + } else { + this.y += this.velocity.y; } } - if (keyDown.length !== 0) { - for (let i = 0; i < keys.length; i++) { - if (this.inputs[keys[i]]) { - this.isMoving = true; - switch (keys[i]) { - case 'z': - if (this.t.y > 0) { - this.t.y -= this.speed; - } else { - this.t.y = 0; - } - break; - case 'q': - if (this.t.x > 0) { - this.t.x -= this.speed; - } else { - this.t.x = 0; - } - break; - case 's': - if (this.t.y < this.canvas.height) { - this.t.y += this.speed; - } else { - this.t.y = this.canvas.height; - } - break; - case 'd': - if (this.t.x < this.canvas.width) { - this.t.x += this.speed; - } else { - this.t.x = this.canvas.width; - } - break; - case ' ': - // dash mechanic - - break; - + for (const key in this.inputs) { + if (this.inputs[key]) { + if (key === 'click') { + this.state?.addEntity(new Projectile(this.x, this.y, this.context, this.canvas, this.state, + { + x: Math.cos(this.angle) * 20, + y: Math.sin(this.angle) * 20 + })); + } + if (key === 'z') { + if (this.target.y > 0) { + this.target.y -= this.speed; + } else { + this.target.y = 0; } } - } - const dx = this.t.x - this.x; - const dy = this.t.y - this.y; - if (dx !== 0 || dy !== 0) { - const angle = Math.atan2(dy, dx); - const velocity = { - x: Math.cos(angle) * this.speed, - y: Math.sin(angle) * this.speed + if (key === 'q') { + if (this.target.x > 0) { + this.target.x -= this.speed; + } else { + this.target.x = 0; + } } - if (Math.abs(dx) < Math.abs(velocity.x)) { - this.x = this.t.x; - } else { - this.x += velocity.x; + if (key === 's') { + if (this.target.y < this.canvas.height) { + this.target.y += this.speed; + } else { + this.target.y = this.canvas.height; + } } - if (Math.abs(dy) < Math.abs(velocity.y)) { - this.y = this.t.y; - } else { - this.y += velocity.y; + if (key === 'd') { + if (this.target.x < this.canvas.width) { + this.target.x += this.speed; + } else { + this.target.x = this.canvas.width; + } } + + } else { + this.isMoving = false; } } - if (this.inputs['click']) { - this.state?.addEntity(new Projectile(this.x, this.y, this.context, this.canvas, { - x: Math.cos(this.angle) * 20, - y: Math.sin(this.angle) * 20 - })); - } } clickEvent(): void { @@ -161,11 +167,26 @@ export default class Player extends Entity { mouseEvent(): void { this.canvas.addEventListener('mousemove', (e) => { - this.angle = Math.atan2(this.mouse.y - this.y, this.mouse.x - this.x); this.mouse = { - x: e.clientX, - y: e.clientY + x: e.pageX, + y: e.pageY } }); } + + debounce(func: any, wait: number, immediate: boolean) { + let timeout: any; + return function () { + // @ts-ignore + const context = this as Function, args = arguments; + const later = function () { + timeout = null; + if (!immediate) func.apply(context, args); + }; + const callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) func.apply(context, args); + }; + }; } \ No newline at end of file diff --git a/src/entities/Projectile.ts b/src/entities/Projectile.ts index a80ce8f..1696cfa 100644 --- a/src/entities/Projectile.ts +++ b/src/entities/Projectile.ts @@ -1,23 +1,59 @@ import Entity from "../models/Entity"; +import State from "../vendor/State"; export class Projectile extends Entity { - private speed: number = 3; - constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, private vlc: { x: number, y: number }) { - super(x, y, 10, context, canvas); - this.v = { - x: this.vlc.x, - y: this.vlc.y + constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement , state: State, private speed: { x: number, y: number }) { + super(x, y, 10, context, canvas, state); + this.velocity = { + x: this.speed.x, + y: this.speed.y } + + this.angle = Math.atan2(this.velocity.y, this.velocity.x); + this.initialize(); + } + + public initialize(): void { + this.loadSprite(); } public draw(): void { + const sx = 32; + const sy = 16 + const sw = 16; + const sh = 16; + const dx = -8; + const dy = -8; + const dh = 16; + const dw = 16; + // image de flèche + this.context.save(); + this.context.translate(this.x, this.y); + this.context.rotate(this.angle); + this.context.drawImage(this.sprite, sx, sy, sw, sh, dx, dy, dw, dh); + // rotation de la flèche + this.context.beginPath(); + this.context.restore(); } public update(): void { + this.angle = Math.atan2(this.velocity.y, this.velocity.x); this.draw(); - this.x += this.v.x + this.speed; - this.y += this.v.y + this.speed; + this.x += this.velocity.x; + this.y += this.velocity.y; + if (this.isOutOfBounds()) { + this.state.removeEntity(this); + } + + } + + private isOutOfBounds(): boolean { + return this.x < 0 || this.x > this.canvas.width || this.y < 0 || this.y > this.canvas.height; + } + + private loadSprite(): void { + this.sprite.src = "./src/sprites/bow.png"; } } \ No newline at end of file diff --git a/src/models/Entity.ts b/src/models/Entity.ts index 42fa47b..15a3187 100644 --- a/src/models/Entity.ts +++ b/src/models/Entity.ts @@ -1,16 +1,20 @@ +import State from "../vendor/State"; export default class Entity { public x: number; public y: number; public sprite: HTMLImageElement = new Image(); + public state: State; + public angle: number; + public scale: number; // v for velocity - public v: { + public velocity: { x: number, y: number } // t for target - public t: { + public target: { x: number, y: number } @@ -18,21 +22,23 @@ export default class Entity { context: CanvasRenderingContext2D; canvas: HTMLCanvasElement; - constructor(x: number, y: number, radius: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement) { + constructor(x: number, y: number, radius: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { this.x = x; this.y = y; - this.v = { + this.angle = 0; + this.radius = radius; + this.context = context; + this.canvas = canvas; + this.state = state; + this.scale = 1; + this.velocity = { x: 0, y: 0 } - - this.t = { - x: 0, - y: 0 + this.target = { + x: this.x, + y: this.y } - this.radius = radius; - this.context = context; - this.canvas = canvas; } public draw(): void { diff --git a/src/sprites/PngItem_2102882.png b/src/sprites/PngItem_2102882.png new file mode 100644 index 0000000000000000000000000000000000000000..376cc2ecd95d9d9fb46d9ee67ae8a77a4ae5c7f5 GIT binary patch literal 47521 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 Date: Fri, 22 Dec 2023 02:02:19 +0100 Subject: [PATCH 08/14] matrix --- src/entities/Player.ts | 32 ++++++++++++++++++++++------ src/models/Entity.ts | 2 ++ src/utils/SpriteSheet.ts | 45 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 src/utils/SpriteSheet.ts diff --git a/src/entities/Player.ts b/src/entities/Player.ts index 44d2510..9141221 100644 --- a/src/entities/Player.ts +++ b/src/entities/Player.ts @@ -1,6 +1,7 @@ import Entity from "../models/Entity"; import State from "../vendor/State"; import {Projectile} from "./Projectile"; +import {SpriteSheet} from "../utils/SpriteSheet"; export default class Player extends Entity { @@ -17,6 +18,8 @@ export default class Player extends Entity { x: 0, y: 0 }; + delay: number = 0; + limit: number = 40; private speed: number = 10; @@ -28,15 +31,32 @@ export default class Player extends Entity { } public draw(): void { - let SCALE = 1; - const WIDTH = 16; - const HEIGHT = 16; - const SCALED_WIDTH = SCALE * WIDTH; - const SCALED_HEIGHT = SCALE * HEIGHT; + const frames = [0, 1, 2, 3]; + if (this.delay > this.limit) { + if (this.frame >= frames.length - 1) { + this.frame = 0; + } else { + this.frame++; + } + this.delay = 0; + } else { + this.delay++; + } + let spriteSheet = new SpriteSheet(this.sprite, 16, 16); + console.log(spriteSheet); + alert('test'); + let sy = 0; + let sx = 0; + const sw = 16; + const sh = 16; + const dx = -8; + const dy = -8; + const dh = 16; + const dw = 16; this.context.save(); this.context.translate(this.x, this.y); this.context.rotate(this.angle); - this.context.drawImage(this.weapon, 0, 0, SCALED_WIDTH, SCALED_HEIGHT, -SCALED_WIDTH / 2, -SCALED_HEIGHT / 2, SCALED_WIDTH, SCALED_HEIGHT); + this.context.drawImage(this.weapon, sx * this.frame, sy, sw, sh, dx, dy, dw, dh); this.context.beginPath(); diff --git a/src/models/Entity.ts b/src/models/Entity.ts index 15a3187..90a94b3 100644 --- a/src/models/Entity.ts +++ b/src/models/Entity.ts @@ -4,6 +4,7 @@ export default class Entity { public x: number; public y: number; + public frame: number; public sprite: HTMLImageElement = new Image(); public state: State; public angle: number; @@ -26,6 +27,7 @@ export default class Entity { this.x = x; this.y = y; this.angle = 0; + this.frame = 0; this.radius = radius; this.context = context; this.canvas = canvas; diff --git a/src/utils/SpriteSheet.ts b/src/utils/SpriteSheet.ts new file mode 100644 index 0000000..1b2ec71 --- /dev/null +++ b/src/utils/SpriteSheet.ts @@ -0,0 +1,45 @@ +export class SpriteSheet { + private matrix: number[][] = []; + private rows: number = 0; + private cols: number = 0; + private frames: number[] = []; + constructor( + public sprite: HTMLImageElement, + public width: number, + public height: number, + ) {} + + public create(rows: number, cols:number ): number[][] { + const matrix: number[][] = []; + for (let i = 0; i < rows; i++) { + matrix[i] = []; + for (let j = 0; j < cols; j++) { + matrix[i][j] = 0; + } + } + this.matrix = matrix; + this.rows = rows; + this.cols = cols; + return matrix; + } + + public setFrame(frameId: number, row: number, col: number): void { + this.matrix[row][col] = frameId; + this.frames.push(frameId); + console.log(this.frames); + } + + public getFrameById(frameId: number): number[] { + const frame: number[] = []; + for (let i = 0; i < this.rows; i++) { + for (let j = 0; j < this.cols; j++) { + if (this.matrix[i][j] === frameId) { + frame[0] = i; + frame[1] = j; + } + } + } + return frame; + } + +} \ No newline at end of file From ba62cc16d6670bcd82483ac6e0cd627379e8c6a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20THOMAS?= <74055963+SebastienThomasDEV@users.noreply.github.com> Date: Fri, 22 Dec 2023 02:29:59 +0100 Subject: [PATCH 09/14] spritesheet --- src/entities/Player.ts | 53 ++++++++++++++++++++++++++-------------- src/utils/SpriteSheet.ts | 36 ++++++++++++++++++++------- 2 files changed, 61 insertions(+), 28 deletions(-) diff --git a/src/entities/Player.ts b/src/entities/Player.ts index 9141221..dd681ac 100644 --- a/src/entities/Player.ts +++ b/src/entities/Player.ts @@ -31,33 +31,48 @@ export default class Player extends Entity { } public draw(): void { - const frames = [0, 1, 2, 3]; + + let spriteSheet = new SpriteSheet(16, 16, 3, 3); + spriteSheet + .setFrame({x: 0, y: 0}, 0, 0) + .setFrame({x: 0, y: spriteSheet.getFrameHeight()}, 1, 0) + .setFrame({x: spriteSheet.getFrameWidth(), y: 0}, 0, 1) + .setFrame({x: spriteSheet.getFrameWidth(), y: spriteSheet.getFrameHeight()}, 1, 1) + if (this.delay > this.limit) { - if (this.frame >= frames.length - 1) { + if (this.frame >= spriteSheet.getFramesCount() - 1) { this.frame = 0; } else { - this.frame++; } this.delay = 0; } else { this.delay++; } - let spriteSheet = new SpriteSheet(this.sprite, 16, 16); - console.log(spriteSheet); - alert('test'); - let sy = 0; - let sx = 0; - const sw = 16; - const sh = 16; - const dx = -8; - const dy = -8; - const dh = 16; - const dw = 16; - this.context.save(); - this.context.translate(this.x, this.y); - this.context.rotate(this.angle); - this.context.drawImage(this.weapon, sx * this.frame, sy, sw, sh, dx, dy, dw, dh); - this.context.beginPath(); + for (let i = 0; i < spriteSheet.getFramesCount(); i++) { + if (this.frame === i) { + console.log(spriteSheet.getFrameById(i)); + this.context.save(); + this.context.translate(this.x, this.y); + this.context.rotate(this.angle); + this.context.drawImage(this.sprite, spriteSheet.getFrameById(i).x, spriteSheet.getFrameById(i).y, spriteSheet.getFrameWidth(), spriteSheet.getFrameHeight(), this.x, this.y, 16, 16); + this.context.restore(); + } + } + // let sy = 0; + // let sx = 0; + // const sw = 16; + // const sh = 16; + // const dx = -8; + // const dy = -8; + // const dh = 16; + // const dw = 16; + // this.context.save(); + // this.context.translate(this.x, this.y); + // this.context.rotate(this.angle); + // this.context.drawImage(this.weapon, sx * this.frame, sy, sw, sh, dx, dy, dw, dh); + // console.log(spriteSheet); + // alert('test'); + // this.context.beginPath(); diff --git a/src/utils/SpriteSheet.ts b/src/utils/SpriteSheet.ts index 1b2ec71..581c5bd 100644 --- a/src/utils/SpriteSheet.ts +++ b/src/utils/SpriteSheet.ts @@ -3,11 +3,17 @@ export class SpriteSheet { private rows: number = 0; private cols: number = 0; private frames: number[] = []; - constructor( - public sprite: HTMLImageElement, - public width: number, - public height: number, - ) {} + // public sprite: HTMLImageElement; + public frameWidth: number; + public frameHeight: number; + + constructor(frameWidth: number, frameHeight: number, rows: number, cols: number) { + this.frameWidth = frameWidth; + this.frameHeight = frameHeight; + this.rows = rows; + this.cols = cols; + this.create(rows, cols); + } public create(rows: number, cols:number ): number[][] { const matrix: number[][] = []; @@ -23,10 +29,10 @@ export class SpriteSheet { return matrix; } - public setFrame(frameId: number, row: number, col: number): void { - this.matrix[row][col] = frameId; - this.frames.push(frameId); - console.log(this.frames); + public setFrame(frame: any, row: number, col: number): SpriteSheet { + this.matrix[row][col] = frame; + this.frames.push(this.frames.length + 1); + return this; } public getFrameById(frameId: number): number[] { @@ -42,4 +48,16 @@ export class SpriteSheet { return frame; } + public getFramesCount(): number { + return this.frames.length; + } + + getFrameHeight(): number { + return this.frameHeight; + } + + getFrameWidth(): number { + return this.frameWidth; + } + } \ No newline at end of file From f98856652d074335564f718eb47e2820e44939d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20THOMAS?= <74055963+SebastienThomasDEV@users.noreply.github.com> Date: Tue, 26 Dec 2023 17:39:44 +0100 Subject: [PATCH 10/14] bow animation --- src/entities/Archer.ts | 73 +++++++++++++++++++-- src/entities/Player.ts | 127 ++++++++++--------------------------- src/entities/Projectile.ts | 46 +++++++++++--- src/utils/SpriteSheet.ts | 43 ++++++------- src/vendor/Renderer.ts | 3 +- src/vendor/State.ts | 45 ++++++++----- 6 files changed, 186 insertions(+), 151 deletions(-) diff --git a/src/entities/Archer.ts b/src/entities/Archer.ts index b15b9ea..489f11e 100644 --- a/src/entities/Archer.ts +++ b/src/entities/Archer.ts @@ -1,12 +1,73 @@ -import Entity from "../models/Entity"; +import Player from "./Player"; +import {Projectile} from "./Projectile"; +import {SpriteSheet} from "../utils/SpriteSheet"; -export default class Archer extends Entity { - constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement) { - super(x, y, 10, context, canvas); +export default class Archer extends Player { + private loaded: boolean = false; + private ammunitions: number = 10; + private bullet: Projectile|null = null; + constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: any) { + super(x, y, context, canvas, state); } public draw(): void { - this.context.fillStyle = 'rgb(0, 0, 255)'; - this.context.fillRect(this.x, this.y, this.radius, this.radius); + super.draw(); + + let spriteSheet = new SpriteSheet(16, 16, 3, 3, this.weapon); + spriteSheet + .setFrame(1, 1, 1) + .setFrame(2, 2, 1) + .setFrame(3, 1, 2) + .setFrame(4, 2, 2) + ; + if (this.delay > this.limit) { + if (this.isFiring) { + this.loaded = false; + if (this.frame < spriteSheet.getFramesCount() - 1) { + this.frame++; + } + if (this.frame === spriteSheet.getFramesCount() - 1) { + this.loaded = true; + this.frame = spriteSheet.getFramesCount() - 1; + } + } else { + this.frame = 0; + } + this.delay = 0; + } else { + this.delay++; + } + this.context.save(); + this.context.translate(this.x, this.y); + this.context.rotate(this.angle); + spriteSheet.drawFrame(this.frame, this.context); + this.context.restore(); + } + + hookMouseUp() { + if (this.isFiring) { + if (this.loaded) { + this.ammunitions--; + this.bullet?.launch(); + this.bullet = null; + this.loaded = false; + this.isFiring = false; + this.frame = 0; + } + } + super.hookMouseUp(); } + + hookMouseDown() { + if (!this.isFiring) { + this.isFiring = true; + if (!this.bullet) { + this.bullet = new Projectile(this.x, this.y, this.context, this.canvas, this.state); + this.state.addEntity(this.bullet); + } + } + super.hookMouseDown(); + } + + } \ No newline at end of file diff --git a/src/entities/Player.ts b/src/entities/Player.ts index dd681ac..29595b8 100644 --- a/src/entities/Player.ts +++ b/src/entities/Player.ts @@ -1,107 +1,48 @@ import Entity from "../models/Entity"; import State from "../vendor/State"; -import {Projectile} from "./Projectile"; -import {SpriteSheet} from "../utils/SpriteSheet"; - export default class Player extends Entity { - public isMoving: boolean; - private weapon: HTMLImageElement = new Image(); + public isMoving: boolean = false; + public isFiring: boolean = false; + public weapon: HTMLImageElement = new Image(); private inputs: any = { 'z': false, 'q': false, 's': false, 'd': false, - 'click': false, - }; - private mouse: any = { - x: 0, - y: 0 }; delay: number = 0; - limit: number = 40; + limit: number = 20; private speed: number = 10; constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { super(x, y, 10, context, canvas, state); this.initialize(); - this.isMoving = false; this.angle = 0; } public draw(): void { - - let spriteSheet = new SpriteSheet(16, 16, 3, 3); - spriteSheet - .setFrame({x: 0, y: 0}, 0, 0) - .setFrame({x: 0, y: spriteSheet.getFrameHeight()}, 1, 0) - .setFrame({x: spriteSheet.getFrameWidth(), y: 0}, 0, 1) - .setFrame({x: spriteSheet.getFrameWidth(), y: spriteSheet.getFrameHeight()}, 1, 1) - - if (this.delay > this.limit) { - if (this.frame >= spriteSheet.getFramesCount() - 1) { - this.frame = 0; - } else { - } - this.delay = 0; - } else { - this.delay++; - } - for (let i = 0; i < spriteSheet.getFramesCount(); i++) { - if (this.frame === i) { - console.log(spriteSheet.getFrameById(i)); - this.context.save(); - this.context.translate(this.x, this.y); - this.context.rotate(this.angle); - this.context.drawImage(this.sprite, spriteSheet.getFrameById(i).x, spriteSheet.getFrameById(i).y, spriteSheet.getFrameWidth(), spriteSheet.getFrameHeight(), this.x, this.y, 16, 16); - this.context.restore(); - } - } - // let sy = 0; - // let sx = 0; - // const sw = 16; - // const sh = 16; - // const dx = -8; - // const dy = -8; - // const dh = 16; - // const dw = 16; - // this.context.save(); - // this.context.translate(this.x, this.y); - // this.context.rotate(this.angle); - // this.context.drawImage(this.weapon, sx * this.frame, sy, sw, sh, dx, dy, dw, dh); - // console.log(spriteSheet); - // alert('test'); - // this.context.beginPath(); - - - - - // draw a crosshair - this.context.moveTo(-this.radius / 2, 0); - this.context.lineTo(this.radius / 2, 0); - this.context.moveTo(0, -this.radius / 2); - this.context.lineTo(0, this.radius / 2); - - this.context.strokeStyle = 'red'; - this.context.stroke(); - this.context.closePath(); - this.context.restore(); - - // // dessin de la ligne de tir du personnage en fonction du curseur (pour le debug) et un arc de cercle pour le curseur + // draw a outline of the player this.context.beginPath(); - this.context.moveTo(this.x, this.y); - this.context.lineTo(this.mouse.x, this.mouse.y); - this.context.strokeStyle = 'red'; - this.context.stroke(); + this.context.arc(this.x, this.y, this.radius, 0, Math.PI * 2); + this.context.fillStyle = 'rgba(0, 0, 0, 0.5)'; + this.context.fill(); this.context.closePath(); + + // // // dessin de la ligne de tir du personnage en fonction du curseur (pour le debug) et un arc de cercle pour le curseur + // this.context.beginPath(); + // this.context.moveTo(this.x, this.y); + // this.context.lineTo(this.mouse.x, this.mouse.y); + // this.context.strokeStyle = 'red'; + // this.context.stroke(); + // this.context.closePath(); } private initialize(): void { + this.loadSprite(); this.keyEvent(); this.clickEvent(); - this.mouseEvent(); - this.loadSprite(); } private loadSprite(): void { @@ -126,7 +67,9 @@ export default class Player extends Entity { public update(): void { this.draw(); - this.angle = Math.atan2(this.mouse.y - this.y, this.mouse.x - this.x); + this.state.playerPos.x = this.x; + this.state.playerPos.y = this.y; + this.angle = Math.atan2(this.state.mouse.y - this.y, this.state.mouse.x - this.x); if (this.angle < 0) { this.angle += Math.PI * 2; } @@ -149,13 +92,13 @@ export default class Player extends Entity { } for (const key in this.inputs) { if (this.inputs[key]) { - if (key === 'click') { - this.state?.addEntity(new Projectile(this.x, this.y, this.context, this.canvas, this.state, - { - x: Math.cos(this.angle) * 20, - y: Math.sin(this.angle) * 20 - })); - } + // if (key === 'click') { + // this.state?.addEntity(new Projectile(this.x, this.y, this.context, this.canvas, this.state, + // { + // x: Math.cos(this.angle) * 20, + // y: Math.sin(this.angle) * 20 + // })); + // } if (key === 'z') { if (this.target.y > 0) { this.target.y -= this.speed; @@ -193,21 +136,17 @@ export default class Player extends Entity { clickEvent(): void { this.canvas.addEventListener('mousedown', () => { - this.inputs['click'] = true; + this.hookMouseDown(); + this.isFiring = true; }); this.canvas.addEventListener('mouseup', () => { - this.inputs['click'] = false; + this.hookMouseUp(); + this.isFiring = false; }); } - mouseEvent(): void { - this.canvas.addEventListener('mousemove', (e) => { - this.mouse = { - x: e.pageX, - y: e.pageY - } - }); - } + hookMouseDown(): void {} + hookMouseUp(): void {} debounce(func: any, wait: number, immediate: boolean) { let timeout: any; diff --git a/src/entities/Projectile.ts b/src/entities/Projectile.ts index 1696cfa..36e0e56 100644 --- a/src/entities/Projectile.ts +++ b/src/entities/Projectile.ts @@ -2,15 +2,16 @@ import Entity from "../models/Entity"; import State from "../vendor/State"; export class Projectile extends Entity { + isShot: boolean = false; + aimAngle: number = 0; + launchAngle: number = 0; - constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement , state: State, private speed: { x: number, y: number }) { + constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement , state: State) { super(x, y, 10, context, canvas, state); this.velocity = { - x: this.speed.x, - y: this.speed.y + x: 0, + y: 0 } - - this.angle = Math.atan2(this.velocity.y, this.velocity.x); this.initialize(); } @@ -30,8 +31,16 @@ export class Projectile extends Entity { const dw = 16; // image de flèche this.context.save(); - this.context.translate(this.x, this.y); - this.context.rotate(this.angle); + if (this.isShot) { + this.context.translate(this.x, this.y); + } else { + this.context.translate(this.state.playerPos.x, this.state.playerPos.y); + } + if (!this.isShot) { + this.context.rotate(this.aimAngle); + } else { + this.context.rotate(this.angle); + } this.context.drawImage(this.sprite, sx, sy, sw, sh, dx, dy, dw, dh); // rotation de la flèche this.context.beginPath(); @@ -39,11 +48,24 @@ export class Projectile extends Entity { } public update(): void { + this.aimAngle = Math.atan2(this.state.mouse.y - this.y, this.state.mouse.x - this.x); this.angle = Math.atan2(this.velocity.y, this.velocity.x); this.draw(); - this.x += this.velocity.x; - this.y += this.velocity.y; + if (this.isShot) { + if (this.launchAngle === 0) { + this.launchAngle = this.aimAngle; + this.angle = Math.atan2(this.velocity.y, this.velocity.x); + } + this.velocity.x = Math.cos(this.launchAngle) * 20; + this.velocity.y = Math.sin(this.launchAngle) * 20; + this.x += this.velocity.x; + this.y += this.velocity.y; + } else { + this.x = this.state.playerPos.x; + this.y = this.state.playerPos.y; + } if (this.isOutOfBounds()) { + console.log("isOutOfBounds"); this.state.removeEntity(this); } @@ -56,4 +78,10 @@ export class Projectile extends Entity { private loadSprite(): void { this.sprite.src = "./src/sprites/bow.png"; } + + launch(): void { + this.isShot = true; + } + + } \ No newline at end of file diff --git a/src/utils/SpriteSheet.ts b/src/utils/SpriteSheet.ts index 581c5bd..28a46b4 100644 --- a/src/utils/SpriteSheet.ts +++ b/src/utils/SpriteSheet.ts @@ -1,15 +1,16 @@ export class SpriteSheet { private matrix: number[][] = []; - private rows: number = 0; - private cols: number = 0; + rows: number = 0; + cols: number = 0; private frames: number[] = []; - // public sprite: HTMLImageElement; + public sprite: HTMLImageElement; public frameWidth: number; public frameHeight: number; - constructor(frameWidth: number, frameHeight: number, rows: number, cols: number) { + constructor(frameWidth: number, frameHeight: number, rows: number, cols: number, sprite: HTMLImageElement) { this.frameWidth = frameWidth; this.frameHeight = frameHeight; + this.sprite = sprite; this.rows = rows; this.cols = cols; this.create(rows, cols); @@ -29,35 +30,29 @@ export class SpriteSheet { return matrix; } - public setFrame(frame: any, row: number, col: number): SpriteSheet { - this.matrix[row][col] = frame; + public setFrame(id: any, row: number, col: number): SpriteSheet { + this.matrix[row - 1][col - 1] = id; this.frames.push(this.frames.length + 1); return this; } - public getFrameById(frameId: number): number[] { - const frame: number[] = []; - for (let i = 0; i < this.rows; i++) { - for (let j = 0; j < this.cols; j++) { - if (this.matrix[i][j] === frameId) { - frame[0] = i; - frame[1] = j; - } - } - } - return frame; - } - public getFramesCount(): number { return this.frames.length; } - getFrameHeight(): number { - return this.frameHeight; - } + public drawFrame(id: number, context: CanvasRenderingContext2D): void { + let row: number = 0; + let col: number = 0; + for (let i = 0; i < this.matrix.length; i++) { + for (let j = 0; j < this.matrix[i].length; j++) { + if (this.matrix[i][j] === id + 1) { + row = i; + col = j; + } + } - getFrameWidth(): number { - return this.frameWidth; + } + context.drawImage(this.sprite, col * this.frameWidth, row * this.frameHeight, this.frameWidth, this.frameHeight, -8, -8, this.frameWidth, this.frameHeight); } } \ No newline at end of file diff --git a/src/vendor/Renderer.ts b/src/vendor/Renderer.ts index 8ae6046..0146660 100644 --- a/src/vendor/Renderer.ts +++ b/src/vendor/Renderer.ts @@ -2,6 +2,7 @@ import Ui from "./Ui"; import Entity from "../models/Entity"; import State from "./State"; import Player from "../entities/Player"; +import Archer from "../entities/Archer"; export default class Renderer { @@ -34,7 +35,7 @@ export default class Renderer { if (!playerInstance) { // spawn player in middle of screen - this.state.addEntity(new Player(this.canvas.width / 2, this.canvas.height / 2, this.context, this.canvas, this.state)); + this.state.addEntity(new Archer(this.canvas.width / 2, this.canvas.height / 2, this.context, this.canvas, this.state)); } } diff --git a/src/vendor/State.ts b/src/vendor/State.ts index 489feb6..601cb1f 100644 --- a/src/vendor/State.ts +++ b/src/vendor/State.ts @@ -3,10 +3,28 @@ import Entity from "../models/Entity"; export default class State { entities: Entity[] = []; - score: number = 0; - level: number = 0; + mouse: { + x: number, + y: number + } + + playerPos: { + x: number, + y: number + }; + + constructor() { + this.mouse = { + x: 0, + y: 0 + } + this.playerPos = { + x: 0, + y: 0 + } + this.mouseEvent(); + } - constructor() {} public addEntity(entity: Entity): void { this.entities.push(entity); @@ -24,20 +42,13 @@ export default class State { this.entities = []; } - levelUp(): void { - this.level++; - } - - addScore(score: number): void { - this.score += score; - } - - getScore(): number { - return this.score; - } - - getLevel(): number { - return this.level; + mouseEvent(): void { + document.addEventListener('mousemove', (e) => { + this.mouse = { + x: e.pageX, + y: e.pageY + } + }); } From 12fef14e763a71a13d7a79fcaf15f8f3ed9e91b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20THOMAS?= <74055963+SebastienThomasDEV@users.noreply.github.com> Date: Sat, 30 Dec 2023 19:27:56 +0100 Subject: [PATCH 11/14] warrior class --- js/Core/functions/reset.js | 32 ----- js/Core/physics/collision.js | 0 js/Core/physics/movement.js | 46 ------- js/Core/physics/shoot.js | 9 -- js/Core/physics/spawn.js | 33 ----- js/Core/ui/drawUI.js | 74 ----------- js/Core/vars/game.js | 94 -------------- src/entities/Warrior.ts | 13 -- src/entities/{ => classes}/Archer.ts | 13 +- src/entities/classes/Warrior.ts | 31 +++++ .../{Projectile.ts => projectiles/Arrow.ts} | 18 +-- src/entities/projectiles/Slash.ts | 41 +++++++ src/{entities => models}/Player.ts | 116 ++++++++++-------- src/models/Projectile.ts | 15 +++ src/models/Prop.ts | 28 +++-- src/utils/Event.ts | 19 +++ src/utils/SpriteSheet.ts | 2 +- src/vendor/Renderer.ts | 7 +- 18 files changed, 205 insertions(+), 386 deletions(-) delete mode 100644 js/Core/functions/reset.js delete mode 100644 js/Core/physics/collision.js delete mode 100644 js/Core/physics/movement.js delete mode 100644 js/Core/physics/shoot.js delete mode 100644 js/Core/physics/spawn.js delete mode 100644 js/Core/ui/drawUI.js delete mode 100644 js/Core/vars/game.js delete mode 100644 src/entities/Warrior.ts rename src/entities/{ => classes}/Archer.ts (85%) create mode 100644 src/entities/classes/Warrior.ts rename src/entities/{Projectile.ts => projectiles/Arrow.ts} (85%) create mode 100644 src/entities/projectiles/Slash.ts rename src/{entities => models}/Player.ts (70%) create mode 100644 src/models/Projectile.ts create mode 100644 src/utils/Event.ts diff --git a/js/Core/functions/reset.js b/js/Core/functions/reset.js deleted file mode 100644 index bb44665..0000000 --- a/js/Core/functions/reset.js +++ /dev/null @@ -1,32 +0,0 @@ - - -export function resetGame(gameVariables, canvas) { - gameVariables.character.model = null; - gameVariables.projectiles = []; - gameVariables.enemies = []; - gameVariables.loots = []; - gameVariables.character.keyPresses = { - z: false, - q: false, - s: false, - d: false, - }; - gameVariables.playerLevel = { - cap: 200, - currentXp: 0, - currentLevel: 1 - }; - gameVariables.playerHealth = { - cap: 100, - maxHealth: 100, - currentHealth: 100 - } - gameVariables.character.attack = 10; - gameVariables.character.armor = 0; - gameVariables.isLooping = false; - gameVariables.intervalInstances.forEach((interval) => { - clearInterval(interval); - }); - gameVariables.intervalInstances.length = 0; -} - diff --git a/js/Core/physics/collision.js b/js/Core/physics/collision.js deleted file mode 100644 index e69de29..0000000 diff --git a/js/Core/physics/movement.js b/js/Core/physics/movement.js deleted file mode 100644 index 6496d5e..0000000 --- a/js/Core/physics/movement.js +++ /dev/null @@ -1,46 +0,0 @@ -export function move(game) { - // faire que le personnage ne puisse pas sortir de la map - const character = game.character.model; - if (game.character.inputs.z) { - if (game.character.model.targetY > 0) { - game.character.model.targetY -= game.character.model.speed; - } else { - game.character.model.targetY = 0; - } - - } else if (game.character.inputs.s) { - if (game.character.model.targetY < window.innerHeight) { - game.character.model.targetY += game.character.model.speed; - } else { - game.character.model.targetY = window.innerHeight; - } - } - if (game.character.inputs.q) { - if (game.character.model.targetX > 0) { - game.character.model.targetX -= game.character.model.speed; - } else { - game.character.model.targetX = 0; - } - } else if (game.character.inputs.d) { - if (game.character.model.targetX < window.innerWidth) { - game.character.model.targetX += game.character.model.speed; - } else { - game.character.model.targetX = window.innerWidth; - } - } -} - -export function dash(game) { - // on doit faire le dash dans la direction du curseur dans un rayon de 20px max - if (game.character.inputs[" "]) { - console.log("dash"); - } -} - -export function keyDownListener(event, game) { - game.character.inputs[event.key] = true; -} -export function keyUpListener(event, game) { - game.character.inputs[event.key] = false; -} - diff --git a/js/Core/physics/shoot.js b/js/Core/physics/shoot.js deleted file mode 100644 index 1bbec45..0000000 --- a/js/Core/physics/shoot.js +++ /dev/null @@ -1,9 +0,0 @@ - -export function shoot(e, game, projectileClass) { - const angle = Math.atan2(e.clientY - game.character.model.y, e.clientX - game.character.model.x); - const velocity = { - x: Math.cos(angle) * 20, - y: Math.sin(angle) * 20, - } - game.projectiles.push(new projectileClass(game.character.model.x, game.character.model.y, 5, velocity, "red")); -} diff --git a/js/Core/physics/spawn.js b/js/Core/physics/spawn.js deleted file mode 100644 index 81dc395..0000000 --- a/js/Core/physics/spawn.js +++ /dev/null @@ -1,33 +0,0 @@ - - -export function spawnEnemies(canvas, game, enemyClass) { - if (game.isLooping === false) { - return; - } - return setInterval(() => { - const randomRadius = Math.random() * 30 + 10; - const randomSpeed = Math.random() * 6 + 1; - const randomColor = `hsl(${Math.random() * 360}, 50%, 50%)`; - const randomHealth = Math.random() * 30 + 20; - let x; - let y; - if (Math.random() < 0.5) { - x = Math.random() < 0.5 ? 0 - randomRadius : canvas.width + randomRadius; - y = Math.random() * canvas.height; - } else { - x = Math.random() * canvas.width; - y = Math.random() < 0.5 ? 0 - randomRadius : canvas.height + randomRadius; - } - const enemy = new enemyClass( - x, - y, - randomRadius, - randomColor, - randomHealth, - { x: 0, y: 0 }, - randomSpeed, - 'ranged' - ); - game.enemies.push(enemy); - }, 3000); -} diff --git a/js/Core/ui/drawUI.js b/js/Core/ui/drawUI.js deleted file mode 100644 index 3597019..0000000 --- a/js/Core/ui/drawUI.js +++ /dev/null @@ -1,74 +0,0 @@ -export function drawCharacterHpBar(context, game) { - context.beginPath(); - context.fillStyle = "red"; - context.rect( - game.character.model.x - 20, - game.character.model.y - 30, - game.playerHealth.currentHealth / game.playerHealth.maxHealth * 40, - 5 - ); - context.fill(); - context.closePath(); - context.beginPath(); - context.strokeStyle = "black"; - context.lineWidth = 1; - context.rect( - game.character.model.x - 20, - game.character.model.y - 30, - 40, - 5 - ); - context.stroke(); - context.closePath(); -} - -export function drawHealthBar(context, game) { - // On dessine la barre de vie du joueur en haut à gauche avec les valeurs actuelles de vie et de vie maximale, - // au centre de la barre de vie - context.beginPath(); - context.fillStyle = "red"; - context.rect(10, 10, game.playerHealth.currentHealth / game.playerHealth.maxHealth * 200, 20); - context.fill(); - context.closePath(); - context.beginPath(); - context.strokeStyle = "black"; - context.lineWidth = 2; - context.rect(10, 10, 200, 20); - context.stroke(); - context.closePath(); - context.beginPath(); - context.font = "12px Arial"; - context.fillStyle = "black"; - context.textAlign = "center"; - context.fillText(game.playerHealth.currentHealth + "/" + game.playerHealth.maxHealth, 110, 25); - context.closePath(); - -} - -export function drawXpBar(context, game) { - context.beginPath(); - context.fillStyle = "lightgreen"; - context.rect(10, 50, game.playerLevel.currentXp / game.playerLevel.cap * 200, 20); - context.fill(); - context.closePath(); - context.beginPath(); - context.strokeStyle = "black"; - context.lineWidth = 2; - context.rect(10, 50, 200, 20); - context.stroke(); - context.closePath(); - context.beginPath(); - context.font = "12px Arial"; - context.fillStyle = "black"; - context.textAlign = "center"; - context.fillText(Math.round(game.playerLevel.currentXp) + "/" + Math.round(game.playerLevel.cap), 110, 65); - context.closePath(); -} - -export function drawPlayerStats(context, game) { - context.beginPath(); - // On affiche l'image de la fenêtre de statistiques du joueur qui fait 256x256 en haut a droite - context.drawImage(game.gui.playerStats, window.innerWidth - 260, -20, 256, 256); - context.closePath(); -} - diff --git a/js/Core/vars/game.js b/js/Core/vars/game.js deleted file mode 100644 index 17592db..0000000 --- a/js/Core/vars/game.js +++ /dev/null @@ -1,94 +0,0 @@ -export const game = { - isLooping: false, - playerLevel: { - cap: 200, - currentXp: 0, - currentLevel: 1 - }, - playerHealth: { - maxHealth: 100, - currentHealth: 100 - }, - character : { - sprites: { - up: null, - upLeft: null, - upRight: null, - down: null, - downLeft: null, - downRight: null, - left: null, - right: null, - }, - statistics: { - level: { - xp: 0, - cap: 200, - current: 1 - }, - health: { - max: 100, - current: 100 - }, - armor: 0, - attack: 10, - speed: 5, - }, - model: null, - money: 0, - attack: 10, - armor: 0, - inputs: { - z: false, - q: false, - s: false, - d: false, - click: true, - }, - isHit: false, - isMoving: false, - }, - projectiles: [], - enemies: [], - loots: { - sprites: { - potion: null, - money: { - coin: null, - pile: null, - bag: null - } - }, - instances: [] - }, - gui: { - playerStats: null, - }, - intervalInstances: [], - mousePos: { - x: 0, - y: 0 - } -} - -export const characterSprites = [ - "assets/img/character/up/Character_Up.png", - "assets/img/character/up/Character_UpLeft.png", - "assets/img/character/up/Character_UpRight.png", - "assets/img/character/down/Character_Down.png", - "assets/img/character/down/Character_DownLeft.png", - "assets/img/character/down/Character_DownRight.png", - "assets/img/character/left/Character_Left.png", - "assets/img/character/right/Character_Right.png", -]; - -export const lootSprites = [ - "assets/img/loots/health/potion.png", - "assets/img/loots/money/coin.png", - "assets/img/loots/money/pile.png", - "assets/img/loots/money/bag.png", -] - -export const uiSprites = [ - "assets/img/gui/gui.png", -] \ No newline at end of file diff --git a/src/entities/Warrior.ts b/src/entities/Warrior.ts deleted file mode 100644 index ae09dd5..0000000 --- a/src/entities/Warrior.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Entity from "../models/Entity"; -import State from "../vendor/State"; - -export default class Warrior extends Entity { - constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { - super(x, y, 10, context, canvas, state); - } - - public draw(): void { - this.context.fillStyle = 'rgb(255, 0, 0)'; - this.context.fillRect(this.x, this.y, this.radius, this.radius); - } -} \ No newline at end of file diff --git a/src/entities/Archer.ts b/src/entities/classes/Archer.ts similarity index 85% rename from src/entities/Archer.ts rename to src/entities/classes/Archer.ts index 489f11e..a7ee791 100644 --- a/src/entities/Archer.ts +++ b/src/entities/classes/Archer.ts @@ -1,11 +1,11 @@ -import Player from "./Player"; -import {Projectile} from "./Projectile"; -import {SpriteSheet} from "../utils/SpriteSheet"; +import Player from "../../models/Player"; +import {Arrow} from "../projectiles/Arrow"; +import {SpriteSheet} from "../../utils/SpriteSheet"; export default class Archer extends Player { private loaded: boolean = false; private ammunitions: number = 10; - private bullet: Projectile|null = null; + private bullet: Arrow|null = null; constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: any) { super(x, y, context, canvas, state); } @@ -48,7 +48,7 @@ export default class Archer extends Player { if (this.isFiring) { if (this.loaded) { this.ammunitions--; - this.bullet?.launch(); + this.bullet!.launch(); this.bullet = null; this.loaded = false; this.isFiring = false; @@ -62,7 +62,7 @@ export default class Archer extends Player { if (!this.isFiring) { this.isFiring = true; if (!this.bullet) { - this.bullet = new Projectile(this.x, this.y, this.context, this.canvas, this.state); + this.bullet = new Arrow(this.x, this.y, this.context, this.canvas, this.state); this.state.addEntity(this.bullet); } } @@ -70,4 +70,5 @@ export default class Archer extends Player { } + } \ No newline at end of file diff --git a/src/entities/classes/Warrior.ts b/src/entities/classes/Warrior.ts new file mode 100644 index 0000000..fa31d21 --- /dev/null +++ b/src/entities/classes/Warrior.ts @@ -0,0 +1,31 @@ + +import State from "../../vendor/State"; +import Player from "../../models/Player"; +import {Slash} from "../projectiles/Slash"; + +export default class Warrior extends Player { + private isAttacking: boolean = false; + constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { + super(x, y, context, canvas, state); + } + + public draw(): void { + // this.context.fillStyle = 'rgb(255, 0, 0)'; + // this.context.fillRect(this.x, this.y, this.radius, this.radius); + // // draw outlined circle for the player + this.context.beginPath(); + this.context.arc(this.x, this.y, this.radius, 0, Math.PI * 2); + this.context.strokeStyle = 'rgba(0, 0, 0, 1)'; + this.context.stroke(); + this.context.closePath(); + } + + hookMouseDown() { + // draw a arc + this.isAttacking = true; + this.state.addEntity(new Slash(this.y, this.y, this.context, this.canvas, this.state)); + super.hookMouseDown(); + } + + +} \ No newline at end of file diff --git a/src/entities/Projectile.ts b/src/entities/projectiles/Arrow.ts similarity index 85% rename from src/entities/Projectile.ts rename to src/entities/projectiles/Arrow.ts index 36e0e56..bdd5e9f 100644 --- a/src/entities/Projectile.ts +++ b/src/entities/projectiles/Arrow.ts @@ -1,12 +1,12 @@ -import Entity from "../models/Entity"; -import State from "../vendor/State"; +import State from "../../vendor/State"; +import {Projectile} from "../../models/Projectile"; -export class Projectile extends Entity { +export class Arrow extends Projectile { isShot: boolean = false; aimAngle: number = 0; launchAngle: number = 0; - constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement , state: State) { + constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { super(x, y, 10, context, canvas, state); this.velocity = { x: 0, @@ -25,10 +25,10 @@ export class Projectile extends Entity { const sy = 16 const sw = 16; const sh = 16; - const dx = -8; - const dy = -8; - const dh = 16; - const dw = 16; + const dx = -16; + const dy = -16; + const dh = 32; + const dw = 32; // image de flèche this.context.save(); if (this.isShot) { @@ -54,7 +54,7 @@ export class Projectile extends Entity { if (this.isShot) { if (this.launchAngle === 0) { this.launchAngle = this.aimAngle; - this.angle = Math.atan2(this.velocity.y, this.velocity.x); + this.angle = Math.atan2(this.velocity.y, this.velocity.x); } this.velocity.x = Math.cos(this.launchAngle) * 20; this.velocity.y = Math.sin(this.launchAngle) * 20; diff --git a/src/entities/projectiles/Slash.ts b/src/entities/projectiles/Slash.ts new file mode 100644 index 0000000..073796e --- /dev/null +++ b/src/entities/projectiles/Slash.ts @@ -0,0 +1,41 @@ +import {Projectile} from "../../models/Projectile"; +import State from "../../vendor/State"; + +export class Slash extends Projectile { + isAttacking: boolean = false; + angle: number = 0; + constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { + super(x, y, 10, context, canvas, state); + this.velocity = { + x: 0, + y: 0 + } + this.initialize(); + } + + public initialize(): void { + + } + + public draw(): void { + // draw a line + this.context.save(); + this.context.beginPath(); + this.context.moveTo(this.x, this.y); + this.context.lineTo(this.x + 50, this.y + 50); + this.context.strokeStyle = "red"; + this.context.stroke(); + this.context.restore(); + + + } + + public update(): void { + this.draw(); + this.velocity.x = Math.cos(this.angle) * 20; + this.velocity.y = Math.sin(this.angle) * 20; + this.x += this.velocity.x; + this.y += this.velocity.y; + } + +} \ No newline at end of file diff --git a/src/entities/Player.ts b/src/models/Player.ts similarity index 70% rename from src/entities/Player.ts rename to src/models/Player.ts index 29595b8..f703a30 100644 --- a/src/entities/Player.ts +++ b/src/models/Player.ts @@ -1,15 +1,20 @@ -import Entity from "../models/Entity"; +import Entity from "./Entity"; import State from "../vendor/State"; export default class Player extends Entity { public isMoving: boolean = false; public isFiring: boolean = false; public weapon: HTMLImageElement = new Image(); + public dashLocation: { + x: number, + y: number + } private inputs: any = { 'z': false, 'q': false, 's': false, 'd': false, + ' ': false }; delay: number = 0; limit: number = 20; @@ -20,23 +25,38 @@ export default class Player extends Entity { super(x, y, 10, context, canvas, state); this.initialize(); this.angle = 0; + this.dashLocation = { + x: this.x, + y: this.y + } } public draw(): void { - // draw a outline of the player + // draw outlined circle for the player this.context.beginPath(); this.context.arc(this.x, this.y, this.radius, 0, Math.PI * 2); + this.context.strokeStyle = 'rgba(0, 0, 0, 1)'; + this.context.stroke(); + this.context.closePath(); + // draw a circle that represent the range of the dash ability which is 7 times the radius of the player + this.context.beginPath(); + this.context.arc(this.x, this.y, this.radius * 7, 0, Math.PI * 2); + this.context.strokeStyle = 'rgba(0, 0, 0, 0.5)'; + this.context.stroke(); + this.context.closePath(); + // draw a line from the player to the point that it cross the circle of the dash ability and draw a point at the end of the line + this.context.beginPath(); + this.context.moveTo(this.x, this.y); + this.context.lineTo(this.x + Math.cos(this.angle) * this.radius * 7, this.y + Math.sin(this.angle) * this.radius * 7); + this.context.strokeStyle = 'rgba(0, 0, 0, 0.5)'; + this.context.stroke(); + this.context.closePath(); + this.context.beginPath(); + this.context.arc(this.x + Math.cos(this.angle) * this.radius * 7, this.y + Math.sin(this.angle) * this.radius * 7, 5, 0, Math.PI * 2); this.context.fillStyle = 'rgba(0, 0, 0, 0.5)'; this.context.fill(); this.context.closePath(); - // // // dessin de la ligne de tir du personnage en fonction du curseur (pour le debug) et un arc de cercle pour le curseur - // this.context.beginPath(); - // this.context.moveTo(this.x, this.y); - // this.context.lineTo(this.mouse.x, this.mouse.y); - // this.context.strokeStyle = 'red'; - // this.context.stroke(); - // this.context.closePath(); } private initialize(): void { @@ -53,6 +73,7 @@ export default class Player extends Entity { private keyEvent(): void { document.body.addEventListener('keydown', (e: KeyboardEvent) => { this.inputs[e.key] = true; + this.hookKeyDown(); }); document.body.addEventListener('keyup', (e: KeyboardEvent) => { for (let i = 0; i < Object.keys(this.inputs).length; i++) { @@ -62,6 +83,7 @@ export default class Player extends Entity { } this.target.x = this.x; this.target.y = this.y; + this.hookKeyUp(); }); } @@ -73,63 +95,59 @@ export default class Player extends Entity { if (this.angle < 0) { this.angle += Math.PI * 2; } - const dx = this.target.x - this.x; - const dy = this.target.y - this.y; - if (dx !== 0 || dy !== 0) { - const angle = Math.atan2(dy, dx); - this.velocity.x = Math.cos(angle) * this.speed; - this.velocity.y = Math.sin(angle) * this.speed; - if (Math.abs(dx) < Math.abs(this.velocity.x)) { - this.x = this.target.x; - } else { - this.x += this.velocity.x; - } - if (Math.abs(dy) < Math.abs(this.velocity.y)) { - this.y = this.target.y; - } else { - this.y += this.velocity.y; - } - } for (const key in this.inputs) { if (this.inputs[key]) { - // if (key === 'click') { - // this.state?.addEntity(new Projectile(this.x, this.y, this.context, this.canvas, this.state, - // { - // x: Math.cos(this.angle) * 20, - // y: Math.sin(this.angle) * 20 - // })); - // } if (key === 'z') { if (this.target.y > 0) { this.target.y -= this.speed; } else { this.target.y = 0; } - } - if (key === 'q') { + } else if (key === 'q') { if (this.target.x > 0) { this.target.x -= this.speed; } else { this.target.x = 0; } - } - if (key === 's') { + } else if (key === 's') { if (this.target.y < this.canvas.height) { this.target.y += this.speed; } else { this.target.y = this.canvas.height; } - } - if (key === 'd') { + } else if (key === 'd') { if (this.target.x < this.canvas.width) { this.target.x += this.speed; + } else { this.target.x = this.canvas.width; } } + if (key === ' ') { + + } + } else { this.isMoving = false; + + } + } + const dx = this.target.x - this.x; + const dy = this.target.y - this.y; + if (dx !== 0 || dy !== 0) { + const angle = Math.atan2(dy, dx); + this.velocity.x = Math.cos(angle) * this.speed; + this.velocity.y = Math.sin(angle) * this.speed; + if (Math.abs(dx) < Math.abs(this.velocity.x)) { + this.x = this.target.x; + } else { + this.x += this.velocity.x; + } + if (Math.abs(dy) < Math.abs(this.velocity.y)) { + this.y = this.target.y; + } else { + this.y += this.velocity.y; } } } @@ -147,20 +165,10 @@ export default class Player extends Entity { hookMouseDown(): void {} hookMouseUp(): void {} + hookKeyDown(): void {} + hookKeyUp(): void {} + hookUpdate(): void {} + hookDraw(): void {} + - debounce(func: any, wait: number, immediate: boolean) { - let timeout: any; - return function () { - // @ts-ignore - const context = this as Function, args = arguments; - const later = function () { - timeout = null; - if (!immediate) func.apply(context, args); - }; - const callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) func.apply(context, args); - }; - }; } \ No newline at end of file diff --git a/src/models/Projectile.ts b/src/models/Projectile.ts new file mode 100644 index 0000000..ebf58ee --- /dev/null +++ b/src/models/Projectile.ts @@ -0,0 +1,15 @@ +import Entity from "./Entity"; + +export class Projectile extends Entity { + constructor(x: number, y: number, radius: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: any) { + super(x, y, radius, context, canvas, state); + } + + public draw(): void { + throw new Error("Method not implemented."); + } + + public update(): void { + throw new Error("Method not implemented."); + } +} \ No newline at end of file diff --git a/src/models/Prop.ts b/src/models/Prop.ts index b3d038b..4875770 100644 --- a/src/models/Prop.ts +++ b/src/models/Prop.ts @@ -2,17 +2,21 @@ import Entity from "./Entity"; export default class Prop extends Entity { - constructor( - x: number, - y: number, - radius: number, - context: CanvasRenderingContext2D, - canvas: HTMLCanvasElement) { - super(x, y, radius, context, canvas); - } + constructor(x: number, y: number, radius: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: any) { + super(x, y, radius, context, canvas, state); + this.sprite.src = 'assets/prop.png'; + } - draw() { - this.context.fillStyle = 'rgb(0, 0, 0)'; - this.context.fillRect(this.x, this.y, this.radius, this.radius); - } + public draw(): void { + this.context.save(); + this.context.translate(this.x, this.y); + this.context.rotate(this.angle); + this.context.scale(this.scale, this.scale); + this.context.drawImage(this.sprite, this.frame * 16, 0, 16, 16, -this.radius, -this.radius, this.radius * 2, this.radius * 2); + this.context.restore(); + } + + public update(): void { + this.draw(); + } } \ No newline at end of file diff --git a/src/utils/Event.ts b/src/utils/Event.ts new file mode 100644 index 0000000..f11d90a --- /dev/null +++ b/src/utils/Event.ts @@ -0,0 +1,19 @@ +export class Event { + + public static debounce(func: Function, wait: number, immediate: boolean): Function { + let timeout: any; + return function () { + // @ts-ignore + const context = this as Function, args = arguments; + const later = function () { + timeout = null; + if (!immediate) func.apply(context, args); + }; + const callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) func.apply(context, args); + }; + } + +} diff --git a/src/utils/SpriteSheet.ts b/src/utils/SpriteSheet.ts index 28a46b4..8e3db36 100644 --- a/src/utils/SpriteSheet.ts +++ b/src/utils/SpriteSheet.ts @@ -52,7 +52,7 @@ export class SpriteSheet { } } - context.drawImage(this.sprite, col * this.frameWidth, row * this.frameHeight, this.frameWidth, this.frameHeight, -8, -8, this.frameWidth, this.frameHeight); + context.drawImage(this.sprite, col * this.frameWidth, row * this.frameHeight, this.frameWidth, this.frameHeight, -16, -16, this.frameWidth * 2, this.frameHeight * 2); } } \ No newline at end of file diff --git a/src/vendor/Renderer.ts b/src/vendor/Renderer.ts index 0146660..27df939 100644 --- a/src/vendor/Renderer.ts +++ b/src/vendor/Renderer.ts @@ -1,8 +1,9 @@ import Ui from "./Ui"; import Entity from "../models/Entity"; import State from "./State"; -import Player from "../entities/Player"; -import Archer from "../entities/Archer"; +import Player from "../models/Player"; +// import Archer from "../entities/classes/Archer"; +import Warrior from "../entities/classes/Warrior"; export default class Renderer { @@ -35,7 +36,7 @@ export default class Renderer { if (!playerInstance) { // spawn player in middle of screen - this.state.addEntity(new Archer(this.canvas.width / 2, this.canvas.height / 2, this.context, this.canvas, this.state)); + this.state.addEntity(new Warrior(this.canvas.width / 2, this.canvas.height / 2, this.context, this.canvas, this.state)); } } From 4bf0848cbc6e706566f47502654844d931c845e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20THOMAS?= <74055963+SebastienThomasDEV@users.noreply.github.com> Date: Tue, 23 Jan 2024 19:54:00 +0100 Subject: [PATCH 12/14] refactoring --- src/entities/classes/Warrior.ts | 9 ++------ src/entities/projectiles/Arrow.ts | 35 +++++++++++-------------------- src/entities/projectiles/Slash.ts | 30 ++++++++++---------------- src/models/Player.ts | 16 +++++--------- src/models/Projectile.ts | 17 +++++++++------ src/vendor/Renderer.ts | 1 + src/vendor/State.ts | 27 +++++++++--------------- 7 files changed, 52 insertions(+), 83 deletions(-) diff --git a/src/entities/classes/Warrior.ts b/src/entities/classes/Warrior.ts index fa31d21..f664092 100644 --- a/src/entities/classes/Warrior.ts +++ b/src/entities/classes/Warrior.ts @@ -4,15 +4,12 @@ import Player from "../../models/Player"; import {Slash} from "../projectiles/Slash"; export default class Warrior extends Player { - private isAttacking: boolean = false; + constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { super(x, y, context, canvas, state); } public draw(): void { - // this.context.fillStyle = 'rgb(255, 0, 0)'; - // this.context.fillRect(this.x, this.y, this.radius, this.radius); - // // draw outlined circle for the player this.context.beginPath(); this.context.arc(this.x, this.y, this.radius, 0, Math.PI * 2); this.context.strokeStyle = 'rgba(0, 0, 0, 1)'; @@ -21,9 +18,7 @@ export default class Warrior extends Player { } hookMouseDown() { - // draw a arc - this.isAttacking = true; - this.state.addEntity(new Slash(this.y, this.y, this.context, this.canvas, this.state)); + this.state.addEntity(new Slash(this.x, this.y, this.context, this.canvas, this.state)); super.hookMouseDown(); } diff --git a/src/entities/projectiles/Arrow.ts b/src/entities/projectiles/Arrow.ts index bdd5e9f..fa6f810 100644 --- a/src/entities/projectiles/Arrow.ts +++ b/src/entities/projectiles/Arrow.ts @@ -2,16 +2,8 @@ import State from "../../vendor/State"; import {Projectile} from "../../models/Projectile"; export class Arrow extends Projectile { - isShot: boolean = false; - aimAngle: number = 0; - launchAngle: number = 0; - constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { super(x, y, 10, context, canvas, state); - this.velocity = { - x: 0, - y: 0 - } this.initialize(); } @@ -31,15 +23,15 @@ export class Arrow extends Projectile { const dw = 32; // image de flèche this.context.save(); - if (this.isShot) { + if (this.isLaunch) { this.context.translate(this.x, this.y); } else { - this.context.translate(this.state.playerPos.x, this.state.playerPos.y); + this.context.translate(this.state.player.x, this.state.player.y); } - if (!this.isShot) { - this.context.rotate(this.aimAngle); - } else { + if (!this.isLaunch) { this.context.rotate(this.angle); + } else { + this.context.rotate(this.launchAngle); } this.context.drawImage(this.sprite, sx, sy, sw, sh, dx, dy, dw, dh); // rotation de la flèche @@ -48,12 +40,11 @@ export class Arrow extends Projectile { } public update(): void { - this.aimAngle = Math.atan2(this.state.mouse.y - this.y, this.state.mouse.x - this.x); - this.angle = Math.atan2(this.velocity.y, this.velocity.x); + this.angle = Math.atan2(this.state.mouse.y - this.y, this.state.mouse.x - this.x); this.draw(); - if (this.isShot) { + if (this.isLaunch) { if (this.launchAngle === 0) { - this.launchAngle = this.aimAngle; + this.launchAngle = this.angle; this.angle = Math.atan2(this.velocity.y, this.velocity.x); } this.velocity.x = Math.cos(this.launchAngle) * 20; @@ -61,8 +52,8 @@ export class Arrow extends Projectile { this.x += this.velocity.x; this.y += this.velocity.y; } else { - this.x = this.state.playerPos.x; - this.y = this.state.playerPos.y; + this.x = this.state.player.x; + this.y = this.state.player.y; } if (this.isOutOfBounds()) { console.log("isOutOfBounds"); @@ -71,16 +62,14 @@ export class Arrow extends Projectile { } - private isOutOfBounds(): boolean { - return this.x < 0 || this.x > this.canvas.width || this.y < 0 || this.y > this.canvas.height; - } + private loadSprite(): void { this.sprite.src = "./src/sprites/bow.png"; } launch(): void { - this.isShot = true; + this.isLaunch = true; } diff --git a/src/entities/projectiles/Slash.ts b/src/entities/projectiles/Slash.ts index 073796e..24d0894 100644 --- a/src/entities/projectiles/Slash.ts +++ b/src/entities/projectiles/Slash.ts @@ -2,40 +2,32 @@ import {Projectile} from "../../models/Projectile"; import State from "../../vendor/State"; export class Slash extends Projectile { - isAttacking: boolean = false; - angle: number = 0; constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { super(x, y, 10, context, canvas, state); - this.velocity = { - x: 0, - y: 0 - } - this.initialize(); - } - - public initialize(): void { - + this.angle = Math.atan2(this.state.mouse.y - this.state.player.y, this.state.mouse.x - this.state.player.x); } public draw(): void { - // draw a line - this.context.save(); + this.context.fillStyle = 'rgb(0, 0, 255)'; this.context.beginPath(); + // draw a shape with 4 lines connected by moveTo and lineTo this.context.moveTo(this.x, this.y); - this.context.lineTo(this.x + 50, this.y + 50); - this.context.strokeStyle = "red"; - this.context.stroke(); - this.context.restore(); - + this.context.lineTo(this.x + 10, this.y + 10); + this.context.lineTo(this.x + 10, this.y - 10); + this.context.lineTo(this.x, this.y); + this.context.fill(); } public update(): void { - this.draw(); this.velocity.x = Math.cos(this.angle) * 20; this.velocity.y = Math.sin(this.angle) * 20; this.x += this.velocity.x; this.y += this.velocity.y; + if (this.isOutOfBounds()) { + this.state.removeEntity(this); + } + this.draw(); } } \ No newline at end of file diff --git a/src/models/Player.ts b/src/models/Player.ts index f703a30..12ae456 100644 --- a/src/models/Player.ts +++ b/src/models/Player.ts @@ -23,12 +23,14 @@ export default class Player extends Entity { constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { super(x, y, 10, context, canvas, state); - this.initialize(); this.angle = 0; this.dashLocation = { x: this.x, y: this.y } + this.loadSprite(); + this.keyEvent(); + this.clickEvent(); } public draw(): void { @@ -59,12 +61,6 @@ export default class Player extends Entity { } - private initialize(): void { - this.loadSprite(); - this.keyEvent(); - this.clickEvent(); - } - private loadSprite(): void { this.sprite.src = './src/sprites/player.png'; this.weapon.src = './src/sprites/bow.png'; @@ -89,8 +85,8 @@ export default class Player extends Entity { public update(): void { this.draw(); - this.state.playerPos.x = this.x; - this.state.playerPos.y = this.y; + this.state.player.x = this.x; + this.state.player.y = this.y; this.angle = Math.atan2(this.state.mouse.y - this.y, this.state.mouse.x - this.x); if (this.angle < 0) { this.angle += Math.PI * 2; @@ -167,8 +163,6 @@ export default class Player extends Entity { hookMouseUp(): void {} hookKeyDown(): void {} hookKeyUp(): void {} - hookUpdate(): void {} - hookDraw(): void {} } \ No newline at end of file diff --git a/src/models/Projectile.ts b/src/models/Projectile.ts index ebf58ee..e5036bf 100644 --- a/src/models/Projectile.ts +++ b/src/models/Projectile.ts @@ -1,15 +1,20 @@ import Entity from "./Entity"; +import State from "../vendor/State"; export class Projectile extends Entity { - constructor(x: number, y: number, radius: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: any) { - super(x, y, radius, context, canvas, state); + launchAngle: number = 0; + isLaunch: boolean = false; + velocity = { + x: 0, + y: 0 } - public draw(): void { - throw new Error("Method not implemented."); + constructor(x: number, y: number, radius: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { + super(x, y, radius, context, canvas, state); } - public update(): void { - throw new Error("Method not implemented."); + public isOutOfBounds(): boolean { + return this.x < 0 || this.x > this.canvas.width || this.y < 0 || this.y > this.canvas.height; } + } \ No newline at end of file diff --git a/src/vendor/Renderer.ts b/src/vendor/Renderer.ts index 27df939..0ab8c84 100644 --- a/src/vendor/Renderer.ts +++ b/src/vendor/Renderer.ts @@ -4,6 +4,7 @@ import State from "./State"; import Player from "../models/Player"; // import Archer from "../entities/classes/Archer"; import Warrior from "../entities/classes/Warrior"; +import Archer from "../entities/classes/Archer"; export default class Renderer { diff --git a/src/vendor/State.ts b/src/vendor/State.ts index 601cb1f..cd55771 100644 --- a/src/vendor/State.ts +++ b/src/vendor/State.ts @@ -3,26 +3,18 @@ import Entity from "../models/Entity"; export default class State { entities: Entity[] = []; - mouse: { - x: number, - y: number + public mouse = { + x: 0, + y: 0 } - playerPos: { - x: number, - y: number - }; + public player = { + x: 0, + y: 0, + } constructor() { - this.mouse = { - x: 0, - y: 0 - } - this.playerPos = { - x: 0, - y: 0 - } - this.mouseEvent(); + this.mouseMoveEvent(); } @@ -42,7 +34,7 @@ export default class State { this.entities = []; } - mouseEvent(): void { + mouseMoveEvent(): void { document.addEventListener('mousemove', (e) => { this.mouse = { x: e.pageX, @@ -53,4 +45,5 @@ export default class State { + } \ No newline at end of file From 0e29a6343931f1a00a5c9fc53e9c8a89fef2d116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20THOMAS?= <74055963+SebastienThomasDEV@users.noreply.github.com> Date: Tue, 23 Jan 2024 19:55:09 +0100 Subject: [PATCH 13/14] changing class --- src/vendor/Renderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vendor/Renderer.ts b/src/vendor/Renderer.ts index 0ab8c84..7b92476 100644 --- a/src/vendor/Renderer.ts +++ b/src/vendor/Renderer.ts @@ -37,7 +37,7 @@ export default class Renderer { if (!playerInstance) { // spawn player in middle of screen - this.state.addEntity(new Warrior(this.canvas.width / 2, this.canvas.height / 2, this.context, this.canvas, this.state)); + this.state.addEntity(new Archer(this.canvas.width / 2, this.canvas.height / 2, this.context, this.canvas, this.state)); } } From 9afe427204f29c692526ccc99dc3617e5a3e3d8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20THOMAS?= <74055963+SebastienThomasDEV@users.noreply.github.com> Date: Wed, 5 Nov 2025 03:55:34 +0100 Subject: [PATCH 14/14] Refactor project structure and add engine core Major refactor: removed legacy assets, scripts, and models; migrated character and weapon sprites to new public/TopDownCharacter directory; added new engine modules (animation, assets, components, core, entities, math, physics, rendering, ui, webgl) in src/engine; introduced game logic and UI components in src/game; updated .gitignore for modern tooling; added documentation and dev log; configured Vite; updated tsconfig for strict mode. This sets up a modular ECS-based 2D WebGL engine in TypeScript. --- .cursor/main.mdc | 200 + .gitignore | 44 +- DEV_LOG.md | 166 + README.md | 106 +- assets/css/main.css | 28 - assets/img/background.jpg | Bin 83300 -> 0 bytes assets/img/bricks.png | Bin 448 -> 0 bytes assets/img/gui/gui.png | Bin 3080 -> 0 bytes assets/img/logo_small.png | Bin 13776 -> 0 bytes assets/img/loots/health/potion.png | Bin 517 -> 0 bytes assets/img/loots/money/bag.png | Bin 519 -> 0 bytes assets/img/loots/money/coin.png | Bin 411 -> 0 bytes assets/img/loots/money/pile.png | Bin 609 -> 0 bytes assets/img/projectile/arrow1.png | Bin 391 -> 0 bytes assets/img/projectile/arrow2.png | Bin 369 -> 0 bytes assets/img/projectile/arrow3.png | Bin 369 -> 0 bytes assets/img/projectile/arrow4.png | Bin 376 -> 0 bytes assets/img/tile.png | Bin 4484 -> 0 bytes docs/CONTINUE_PROMPT.md | 207 ++ docs/CORE.MD | 3296 +++++++++++++++++ docs/PROMPT.md | 19 + index.html | 136 +- js/Core/loader.js | 23 - js/Entities/Character.js | 123 - js/Entities/Enemy.js | 84 - js/Entities/Entity.js | 9 - js/Entities/Loot.js | 27 - js/Entities/Projectile.js | 26 - js/main.js | 224 -- package-lock.json | 1089 +++--- package.json | 32 +- pnpm-lock.yaml | 843 +++++ .../Retro Inventory/Original/Health_01.png | Bin 0 -> 314 bytes .../Original/Health_01_Bar01.png | Bin 0 -> 174 bytes .../Original/Health_01_Bar02.png | Bin 0 -> 164 bytes .../Original/Health_01_Bar03.png | Bin 0 -> 168 bytes .../Retro Inventory/Original/Health_02.png | Bin 0 -> 311 bytes .../Original/Health_02_Bar01.png | Bin 0 -> 172 bytes .../Original/Health_02_Bar02.png | Bin 0 -> 171 bytes .../Original/Health_02_Bar03.png | Bin 0 -> 169 bytes .../Retro Inventory/Original/Health_03.png | Bin 0 -> 311 bytes .../Original/Health_03_Bar01.png | Bin 0 -> 174 bytes .../Original/Health_03_Bar02.png | Bin 0 -> 165 bytes .../Original/Health_03_Bar03.png | Bin 0 -> 169 bytes .../Retro Inventory/Original/Health_04.png | Bin 0 -> 381 bytes .../Original/Health_04_Bar01.png | Bin 0 -> 136 bytes .../Original/Health_04_Bar02.png | Bin 0 -> 135 bytes .../Original/Health_04_Bar03.png | Bin 0 -> 135 bytes .../Original/Health_04_Bar04.png | Bin 0 -> 136 bytes .../Original/Health_04_Bar05.png | Bin 0 -> 133 bytes .../Original/Health_04_Bar06.png | Bin 0 -> 134 bytes .../Original/Health_04_Heart_Blue.png | Bin 0 -> 405 bytes .../Original/Health_04_Heart_Blue_Clear.png | Bin 0 -> 269 bytes .../Original/Health_04_Heart_Red.png | Bin 0 -> 398 bytes .../Original/Health_04_Heart_Red_Clear.png | Bin 0 -> 263 bytes .../Original/Health_04_Heart_Yellow.png | Bin 0 -> 398 bytes .../Original/Health_04_Heart_Yellow_Clear.png | Bin 0 -> 263 bytes .../Retro Inventory/Original/Health_05_01.png | Bin 0 -> 1350 bytes .../Retro Inventory/Original/Health_05_02.png | Bin 0 -> 1350 bytes .../Retro Inventory/Original/Health_05_03.png | Bin 0 -> 1350 bytes .../Retro Inventory/Original/Heart_Blue.png | Bin 0 -> 258 bytes .../Retro Inventory/Original/Heart_Blue_1.png | Bin 0 -> 208 bytes .../Retro Inventory/Original/Heart_Blue_2.png | Bin 0 -> 174 bytes .../Retro Inventory/Original/Heart_Blue_3.png | Bin 0 -> 157 bytes .../Retro Inventory/Original/Heart_Blue_4.png | Bin 0 -> 121 bytes .../Retro Inventory/Original/Heart_Orange.png | Bin 0 -> 258 bytes .../Original/Heart_Orange_1.png | Bin 0 -> 201 bytes .../Original/Heart_Orange_2.png | Bin 0 -> 170 bytes .../Original/Heart_Orange_3.png | Bin 0 -> 158 bytes .../Original/Heart_Orange_4.png | Bin 0 -> 118 bytes .../Retro Inventory/Original/Heart_Red.png | Bin 0 -> 258 bytes .../Retro Inventory/Original/Heart_Red_1.png | Bin 0 -> 227 bytes .../Retro Inventory/Original/Heart_Red_2.png | Bin 0 -> 190 bytes .../Retro Inventory/Original/Heart_Red_3.png | Bin 0 -> 156 bytes .../Retro Inventory/Original/Heart_Red_4.png | Bin 0 -> 124 bytes .../Retro Inventory/Original/Hearts.png | Bin 0 -> 713 bytes .../Original/Hearts_Blue_1.png | Bin 0 -> 335 bytes .../Original/Hearts_Blue_2.png | Bin 0 -> 367 bytes .../Original/Hearts_Blue_3.png | Bin 0 -> 383 bytes .../Original/Hearts_Blue_4.png | Bin 0 -> 382 bytes .../Original/Hearts_Blue_5.png | Bin 0 -> 355 bytes .../Retro Inventory/Original/Hearts_Red_1.png | Bin 0 -> 367 bytes .../Retro Inventory/Original/Hearts_Red_2.png | Bin 0 -> 360 bytes .../Retro Inventory/Original/Hearts_Red_3.png | Bin 0 -> 390 bytes .../Retro Inventory/Original/Hearts_Red_4.png | Bin 0 -> 392 bytes .../Retro Inventory/Original/Hearts_Red_5.png | Bin 0 -> 355 bytes .../Original/Hearts_Yellow_1.png | Bin 0 -> 329 bytes .../Original/Hearts_Yellow_2.png | Bin 0 -> 348 bytes .../Original/Hearts_Yellow_3.png | Bin 0 -> 380 bytes .../Original/Hearts_Yellow_4.png | Bin 0 -> 386 bytes .../Original/Hearts_Yellow_5.png | Bin 0 -> 355 bytes .../Retro Inventory/Original/Inventory.png | Bin 0 -> 1168 bytes .../Original/Inventory_9Slices.png | Bin 0 -> 357 bytes .../Original/Inventory_Example_01.png | Bin 0 -> 965 bytes .../Original/Inventory_Example_02.png | Bin 0 -> 870 bytes .../Original/Inventory_Example_03.png | Bin 0 -> 664 bytes .../Original/Inventory_Example_04.png | Bin 0 -> 579 bytes .../Original/Inventory_Slot_1.png | Bin 0 -> 312 bytes .../Original/Inventory_Slot_10.png | Bin 0 -> 409 bytes .../Original/Inventory_Slot_2.png | Bin 0 -> 407 bytes .../Original/Inventory_Slot_3.png | Bin 0 -> 377 bytes .../Original/Inventory_Slot_4.png | Bin 0 -> 387 bytes .../Original/Inventory_Slot_5.png | Bin 0 -> 405 bytes .../Original/Inventory_Slot_6.png | Bin 0 -> 387 bytes .../Original/Inventory_Slot_7.png | Bin 0 -> 397 bytes .../Original/Inventory_Slot_8.png | Bin 0 -> 373 bytes .../Original/Inventory_Slot_9.png | Bin 0 -> 383 bytes .../Retro Inventory/Original/Settings.png | Bin 0 -> 274 bytes .../Original/Settings_Bar01.png | Bin 0 -> 173 bytes .../Original/Settings_Bar02.png | Bin 0 -> 164 bytes .../Original/Settings_Bar03.png | Bin 0 -> 165 bytes .../Original/Settings_Cross01.png | Bin 0 -> 210 bytes .../Original/Settings_Cross02.png | Bin 0 -> 230 bytes .../Original/Settings_Cross03.png | Bin 0 -> 244 bytes .../Retro Inventory/Read Me.txt | 31 + .../Retro Inventory/Scaled 2x/Health_01.png | Bin 0 -> 534 bytes .../Scaled 2x/Health_01_Bar01.png | Bin 0 -> 202 bytes .../Scaled 2x/Health_01_Bar02.png | Bin 0 -> 189 bytes .../Scaled 2x/Health_01_Bar03.png | Bin 0 -> 197 bytes .../Retro Inventory/Scaled 2x/Health_02.png | Bin 0 -> 569 bytes .../Scaled 2x/Health_02_Bar01.png | Bin 0 -> 191 bytes .../Scaled 2x/Health_02_Bar02.png | Bin 0 -> 185 bytes .../Scaled 2x/Health_02_Bar03.png | Bin 0 -> 190 bytes .../Retro Inventory/Scaled 2x/Health_03.png | Bin 0 -> 467 bytes .../Scaled 2x/Health_03_Bar01.png | Bin 0 -> 202 bytes .../Scaled 2x/Health_03_Bar02.png | Bin 0 -> 191 bytes .../Scaled 2x/Health_03_Bar03.png | Bin 0 -> 198 bytes .../Retro Inventory/Scaled 2x/Health_04.png | Bin 0 -> 892 bytes .../Scaled 2x/Health_04_Bar01.png | Bin 0 -> 103 bytes .../Scaled 2x/Health_04_Bar02.png | Bin 0 -> 102 bytes .../Scaled 2x/Health_04_Bar03.png | Bin 0 -> 102 bytes .../Scaled 2x/Health_04_Bar04.png | Bin 0 -> 103 bytes .../Scaled 2x/Health_04_Bar05.png | Bin 0 -> 98 bytes .../Scaled 2x/Health_04_Bar06.png | Bin 0 -> 98 bytes .../Scaled 2x/Health_04_Heart_Blue.png | Bin 0 -> 686 bytes .../Scaled 2x/Health_04_Heart_Blue_Clear.png | Bin 0 -> 354 bytes .../Scaled 2x/Health_04_Heart_Red.png | Bin 0 -> 705 bytes .../Scaled 2x/Health_04_Heart_Red_Clear.png | Bin 0 -> 353 bytes .../Scaled 2x/Health_04_Heart_Yellow.png | Bin 0 -> 712 bytes .../Health_04_Heart_Yellow_Clear.png | Bin 0 -> 334 bytes .../Scaled 2x/Health_05_01.png | Bin 0 -> 3673 bytes .../Scaled 2x/Health_05_02.png | Bin 0 -> 3634 bytes .../Scaled 2x/Health_05_03.png | Bin 0 -> 3614 bytes .../Retro Inventory/Scaled 2x/Heart_Blue.png | Bin 0 -> 333 bytes .../Scaled 2x/Heart_Blue_1.png | Bin 0 -> 233 bytes .../Scaled 2x/Heart_Blue_2.png | Bin 0 -> 202 bytes .../Scaled 2x/Heart_Blue_3.png | Bin 0 -> 188 bytes .../Scaled 2x/Heart_Blue_4.png | Bin 0 -> 145 bytes .../Scaled 2x/Heart_Orange.png | Bin 0 -> 325 bytes .../Scaled 2x/Heart_Orange_1.png | Bin 0 -> 224 bytes .../Scaled 2x/Heart_Orange_2.png | Bin 0 -> 198 bytes .../Scaled 2x/Heart_Orange_3.png | Bin 0 -> 177 bytes .../Scaled 2x/Heart_Orange_4.png | Bin 0 -> 143 bytes .../Retro Inventory/Scaled 2x/Heart_Red.png | Bin 0 -> 345 bytes .../Retro Inventory/Scaled 2x/Heart_Red_1.png | Bin 0 -> 242 bytes .../Retro Inventory/Scaled 2x/Heart_Red_2.png | Bin 0 -> 216 bytes .../Retro Inventory/Scaled 2x/Heart_Red_3.png | Bin 0 -> 192 bytes .../Retro Inventory/Scaled 2x/Heart_Red_4.png | Bin 0 -> 149 bytes .../Retro Inventory/Scaled 2x/Hearts.png | Bin 0 -> 1329 bytes .../Scaled 2x/Hearts_Blue_1.png | Bin 0 -> 404 bytes .../Scaled 2x/Hearts_Blue_2.png | Bin 0 -> 423 bytes .../Scaled 2x/Hearts_Blue_3.png | Bin 0 -> 425 bytes .../Scaled 2x/Hearts_Blue_4.png | Bin 0 -> 420 bytes .../Scaled 2x/Hearts_Blue_5.png | Bin 0 -> 402 bytes .../Scaled 2x/Hearts_Red_1.png | Bin 0 -> 420 bytes .../Scaled 2x/Hearts_Red_2.png | Bin 0 -> 443 bytes .../Scaled 2x/Hearts_Red_3.png | Bin 0 -> 439 bytes .../Scaled 2x/Hearts_Red_4.png | Bin 0 -> 429 bytes .../Scaled 2x/Hearts_Red_5.png | Bin 0 -> 402 bytes .../Scaled 2x/Hearts_Yellow_1.png | Bin 0 -> 407 bytes .../Scaled 2x/Hearts_Yellow_2.png | Bin 0 -> 432 bytes .../Scaled 2x/Hearts_Yellow_3.png | Bin 0 -> 437 bytes .../Scaled 2x/Hearts_Yellow_4.png | Bin 0 -> 436 bytes .../Scaled 2x/Hearts_Yellow_5.png | Bin 0 -> 402 bytes .../Retro Inventory/Scaled 2x/Inventory.png | Bin 0 -> 2480 bytes .../Scaled 2x/Inventory_9Slices.png | Bin 0 -> 489 bytes .../Scaled 2x/Inventory_Example_01.png | Bin 0 -> 1876 bytes .../Scaled 2x/Inventory_Example_02.png | Bin 0 -> 1738 bytes .../Scaled 2x/Inventory_Example_03.png | Bin 0 -> 1413 bytes .../Scaled 2x/Inventory_Example_04.png | Bin 0 -> 1307 bytes .../Scaled 2x/Inventory_Slot_1.png | Bin 0 -> 429 bytes .../Scaled 2x/Inventory_Slot_10.png | Bin 0 -> 508 bytes .../Scaled 2x/Inventory_Slot_2.png | Bin 0 -> 518 bytes .../Scaled 2x/Inventory_Slot_3.png | Bin 0 -> 493 bytes .../Scaled 2x/Inventory_Slot_4.png | Bin 0 -> 508 bytes .../Scaled 2x/Inventory_Slot_5.png | Bin 0 -> 508 bytes .../Scaled 2x/Inventory_Slot_6.png | Bin 0 -> 493 bytes .../Scaled 2x/Inventory_Slot_7.png | Bin 0 -> 506 bytes .../Scaled 2x/Inventory_Slot_8.png | Bin 0 -> 493 bytes .../Scaled 2x/Inventory_Slot_9.png | Bin 0 -> 499 bytes .../Retro Inventory/Scaled 2x/Settings.png | Bin 0 -> 431 bytes .../Scaled 2x/Settings_Bar01.png | Bin 0 -> 199 bytes .../Scaled 2x/Settings_Bar02.png | Bin 0 -> 186 bytes .../Scaled 2x/Settings_Bar03.png | Bin 0 -> 189 bytes .../Scaled 2x/Settings_Cross01.png | Bin 0 -> 219 bytes .../Scaled 2x/Settings_Cross02.png | Bin 0 -> 293 bytes .../Scaled 2x/Settings_Cross03.png | Bin 0 -> 323 bytes .../Retro Inventory/Scaled 3x/Health_01.png | Bin 0 -> 618 bytes .../Scaled 3x/Health_01_Bar01.png | Bin 0 -> 259 bytes .../Scaled 3x/Health_01_Bar02.png | Bin 0 -> 246 bytes .../Scaled 3x/Health_01_Bar03.png | Bin 0 -> 254 bytes .../Retro Inventory/Scaled 3x/Health_02.png | Bin 0 -> 758 bytes .../Scaled 3x/Health_02_Bar01.png | Bin 0 -> 329 bytes .../Scaled 3x/Health_02_Bar02.png | Bin 0 -> 321 bytes .../Scaled 3x/Health_02_Bar03.png | Bin 0 -> 328 bytes .../Retro Inventory/Scaled 3x/Health_03.png | Bin 0 -> 549 bytes .../Scaled 3x/Health_03_Bar01.png | Bin 0 -> 255 bytes .../Scaled 3x/Health_03_Bar02.png | Bin 0 -> 242 bytes .../Scaled 3x/Health_03_Bar03.png | Bin 0 -> 250 bytes .../Retro Inventory/Scaled 3x/Health_04.png | Bin 0 -> 1129 bytes .../Scaled 3x/Health_04_Bar01.png | Bin 0 -> 119 bytes .../Scaled 3x/Health_04_Bar02.png | Bin 0 -> 116 bytes .../Scaled 3x/Health_04_Bar03.png | Bin 0 -> 117 bytes .../Scaled 3x/Health_04_Bar04.png | Bin 0 -> 115 bytes .../Scaled 3x/Health_04_Bar05.png | Bin 0 -> 111 bytes .../Scaled 3x/Health_04_Bar06.png | Bin 0 -> 113 bytes .../Scaled 3x/Health_04_Heart_Blue.png | Bin 0 -> 854 bytes .../Scaled 3x/Health_04_Heart_Blue_Clear.png | Bin 0 -> 423 bytes .../Scaled 3x/Health_04_Heart_Red.png | Bin 0 -> 871 bytes .../Scaled 3x/Health_04_Heart_Red_Clear.png | Bin 0 -> 420 bytes .../Scaled 3x/Health_04_Heart_Yellow.png | Bin 0 -> 883 bytes .../Health_04_Heart_Yellow_Clear.png | Bin 0 -> 401 bytes .../Scaled 3x/Health_05_01.png | Bin 0 -> 5104 bytes .../Scaled 3x/Health_05_02.png | Bin 0 -> 5066 bytes .../Scaled 3x/Health_05_03.png | Bin 0 -> 5044 bytes .../Retro Inventory/Scaled 3x/Heart_Blue.png | Bin 0 -> 403 bytes .../Scaled 3x/Heart_Blue_1.png | Bin 0 -> 290 bytes .../Scaled 3x/Heart_Blue_2.png | Bin 0 -> 262 bytes .../Scaled 3x/Heart_Blue_3.png | Bin 0 -> 240 bytes .../Scaled 3x/Heart_Blue_4.png | Bin 0 -> 188 bytes .../Scaled 3x/Heart_Orange.png | Bin 0 -> 392 bytes .../Scaled 3x/Heart_Orange_1.png | Bin 0 -> 283 bytes .../Scaled 3x/Heart_Orange_2.png | Bin 0 -> 254 bytes .../Scaled 3x/Heart_Orange_3.png | Bin 0 -> 230 bytes .../Scaled 3x/Heart_Orange_4.png | Bin 0 -> 188 bytes .../Retro Inventory/Scaled 3x/Heart_Red.png | Bin 0 -> 414 bytes .../Retro Inventory/Scaled 3x/Heart_Red_1.png | Bin 0 -> 303 bytes .../Retro Inventory/Scaled 3x/Heart_Red_2.png | Bin 0 -> 277 bytes .../Retro Inventory/Scaled 3x/Heart_Red_3.png | Bin 0 -> 250 bytes .../Retro Inventory/Scaled 3x/Heart_Red_4.png | Bin 0 -> 196 bytes .../Retro Inventory/Scaled 3x/Hearts.png | Bin 0 -> 2174 bytes .../Scaled 3x/Hearts_Blue_1.png | Bin 0 -> 469 bytes .../Scaled 3x/Hearts_Blue_2.png | Bin 0 -> 486 bytes .../Scaled 3x/Hearts_Blue_3.png | Bin 0 -> 493 bytes .../Scaled 3x/Hearts_Blue_4.png | Bin 0 -> 483 bytes .../Scaled 3x/Hearts_Blue_5.png | Bin 0 -> 462 bytes .../Scaled 3x/Hearts_Red_1.png | Bin 0 -> 484 bytes .../Scaled 3x/Hearts_Red_2.png | Bin 0 -> 505 bytes .../Scaled 3x/Hearts_Red_3.png | Bin 0 -> 506 bytes .../Scaled 3x/Hearts_Red_4.png | Bin 0 -> 490 bytes .../Scaled 3x/Hearts_Red_5.png | Bin 0 -> 462 bytes .../Scaled 3x/Hearts_Yellow_1.png | Bin 0 -> 475 bytes .../Scaled 3x/Hearts_Yellow_2.png | Bin 0 -> 495 bytes .../Scaled 3x/Hearts_Yellow_3.png | Bin 0 -> 500 bytes .../Scaled 3x/Hearts_Yellow_4.png | Bin 0 -> 497 bytes .../Scaled 3x/Hearts_Yellow_5.png | Bin 0 -> 462 bytes .../Retro Inventory/Scaled 3x/Inventory.png | Bin 0 -> 3321 bytes .../Scaled 3x/Inventory_9Slices.png | Bin 0 -> 613 bytes .../Scaled 3x/Inventory_Example_01.png | Bin 0 -> 2815 bytes .../Scaled 3x/Inventory_Example_02.png | Bin 0 -> 2351 bytes .../Scaled 3x/Inventory_Example_03.png | Bin 0 -> 2284 bytes .../Scaled 3x/Inventory_Example_04.png | Bin 0 -> 1880 bytes .../Scaled 3x/Inventory_Slot_1.png | Bin 0 -> 538 bytes .../Scaled 3x/Inventory_Slot_10.png | Bin 0 -> 631 bytes .../Scaled 3x/Inventory_Slot_2.png | Bin 0 -> 635 bytes .../Scaled 3x/Inventory_Slot_3.png | Bin 0 -> 608 bytes .../Scaled 3x/Inventory_Slot_4.png | Bin 0 -> 631 bytes .../Scaled 3x/Inventory_Slot_5.png | Bin 0 -> 630 bytes .../Scaled 3x/Inventory_Slot_6.png | Bin 0 -> 611 bytes .../Scaled 3x/Inventory_Slot_7.png | Bin 0 -> 614 bytes .../Scaled 3x/Inventory_Slot_8.png | Bin 0 -> 611 bytes .../Scaled 3x/Inventory_Slot_9.png | Bin 0 -> 618 bytes .../Retro Inventory/Scaled 3x/Settings.png | Bin 0 -> 511 bytes .../Scaled 3x/Settings_Bar01.png | Bin 0 -> 255 bytes .../Scaled 3x/Settings_Bar02.png | Bin 0 -> 243 bytes .../Scaled 3x/Settings_Bar03.png | Bin 0 -> 247 bytes .../Scaled 3x/Settings_Cross01.png | Bin 0 -> 277 bytes .../Scaled 3x/Settings_Cross02.png | Bin 0 -> 348 bytes .../Scaled 3x/Settings_Cross03.png | Bin 0 -> 369 bytes .../Character}/Character_Down.png | Bin .../Character}/Character_DownLeft.png | Bin .../Character}/Character_DownRight.png | Bin .../Character}/Character_Left.png | Bin .../Character}/Character_Right.png | Bin .../Character}/Character_RollDown.png | Bin .../Character}/Character_RollDownLeft.png | Bin .../Character}/Character_RollDownRight.png | Bin .../Character}/Character_RollLeft.png | Bin .../Character}/Character_RollRight.png | Bin .../Character}/Character_RollUp.png | Bin .../Character}/Character_RollUpLeft.png | Bin .../Character}/Character_RollUpRight.png | Bin .../Character}/Character_SlashDownLeft.png | Bin .../Character}/Character_SlashDownRight.png | Bin .../Character}/Character_SlashUpLeft.png | Bin .../Character}/Character_SlashUpRight.png | Bin .../Character}/Character_Up.png | Bin .../Character}/Character_UpLeft.png | Bin .../Character}/Character_UpRight.png | Bin .../Weapon/Sword_DownLeft.png | Bin .../Weapon/Sword_DownRight.png | Bin .../TopDownCharacter}/Weapon/Sword_UpLeft.png | Bin .../Weapon/Sword_UpRight.png | Bin src/engine/animation/Animation.ts | 66 + src/engine/assets/AssetLoader.ts | 224 ++ src/engine/assets/SpriteSheet.ts | 153 + src/engine/assets/TextureAtlas.ts | 121 + src/engine/assets/index.ts | 8 + src/engine/components/Animator.ts | 196 + src/engine/components/SpriteRenderer.ts | 159 + src/engine/core/Engine.ts | 401 ++ src/engine/core/GameLoop.ts | 152 + src/engine/core/Scene.ts | 226 ++ src/engine/core/SceneManager.ts | 137 + src/engine/core/Time.ts | 103 + src/engine/debug/DebugPanel.ts | 191 + src/engine/entities/Component.ts | 120 + src/engine/entities/GameObject.ts | 406 ++ src/engine/entities/Transform.ts | 340 ++ src/engine/input/InputManager.ts | 255 ++ src/engine/math/Color.ts | 139 + src/engine/math/Matrix3.ts | 223 ++ src/engine/math/Rect.ts | 178 + src/engine/math/Vector2.ts | 246 ++ src/engine/physics/BoxCollider2D.ts | 115 + src/engine/physics/CircleCollider2D.ts | 70 + src/engine/physics/Collider2D.ts | 207 ++ src/engine/physics/PhysicsSystem.ts | 323 ++ src/engine/physics/Rigidbody2D.ts | 184 + src/engine/physics/index.ts | 10 + src/engine/rendering/Camera.ts | 253 ++ src/engine/rendering/Shader.ts | 121 + src/engine/rendering/SpriteBatch.ts | 316 ++ src/engine/rendering/Texture.ts | 220 ++ src/engine/rendering/WebGLRenderer.ts | 313 ++ src/engine/ui/RectTransform.ts | 250 ++ src/engine/ui/UIButton.ts | 264 ++ src/engine/ui/UICanvas.ts | 168 + src/engine/ui/UIComponent.ts | 52 + src/engine/ui/UIImage.ts | 139 + src/engine/ui/UIText.ts | 224 ++ src/engine/ui/index.ts | 11 + src/engine/webgl/Buffer.ts | 109 + src/engine/webgl/GLContext.ts | 80 + src/entities/classes/Archer.ts | 74 - src/entities/classes/Warrior.ts | 26 - src/entities/projectiles/Arrow.ts | 76 - src/entities/projectiles/Slash.ts | 33 - src/game/Game.ts | 129 + src/game/components/HealthComponent.ts | 112 + src/game/components/PlayerController.ts | 491 +++ src/game/components/Projectile.ts | 73 + src/game/scenes/GameScene.ts | 515 +++ src/game/ui/UIHealthBar.ts | 165 + src/game/utils/TextureGenerator.ts | 118 + src/index.ts | 39 + src/main.ts | 8 - src/models/Entity.ts | 54 - src/models/Player.ts | 168 - src/models/Projectile.ts | 20 - src/models/Prop.ts | 22 - src/sprites/PngItem_2102882.png | Bin 47521 -> 0 bytes src/sprites/bow.png | Bin 717 -> 0 bytes src/sprites/player.png | Bin 16491 -> 0 bytes src/utils/Event.ts | 19 - src/utils/SpriteSheet.ts | 58 - src/vendor/Game.ts | 54 - src/vendor/Renderer.ts | 47 - src/vendor/State.ts | 49 - src/vendor/Ui.ts | 16 - src/vite-env.d.ts | 1 - tsconfig.json | 58 +- vite.config.d.ts | 3 + vite.config.d.ts.map | 1 + vite.config.js | 16 + vite.config.ts | 21 + 376 files changed, 14592 insertions(+), 2060 deletions(-) create mode 100644 .cursor/main.mdc create mode 100644 DEV_LOG.md delete mode 100644 assets/css/main.css delete mode 100644 assets/img/background.jpg delete mode 100644 assets/img/bricks.png delete mode 100644 assets/img/gui/gui.png delete mode 100644 assets/img/logo_small.png delete mode 100644 assets/img/loots/health/potion.png delete mode 100644 assets/img/loots/money/bag.png delete mode 100644 assets/img/loots/money/coin.png delete mode 100644 assets/img/loots/money/pile.png delete mode 100644 assets/img/projectile/arrow1.png delete mode 100644 assets/img/projectile/arrow2.png delete mode 100644 assets/img/projectile/arrow3.png delete mode 100644 assets/img/projectile/arrow4.png delete mode 100644 assets/img/tile.png create mode 100644 docs/CONTINUE_PROMPT.md create mode 100644 docs/CORE.MD create mode 100644 docs/PROMPT.md delete mode 100644 js/Core/loader.js delete mode 100644 js/Entities/Character.js delete mode 100644 js/Entities/Enemy.js delete mode 100644 js/Entities/Entity.js delete mode 100644 js/Entities/Loot.js delete mode 100644 js/Entities/Projectile.js delete mode 100644 js/main.js create mode 100644 pnpm-lock.yaml create mode 100644 public/Retro Inventory/Retro Inventory/Original/Health_01.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Health_01_Bar01.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Health_01_Bar02.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Health_01_Bar03.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Health_02.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Health_02_Bar01.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Health_02_Bar02.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Health_02_Bar03.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Health_03.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Health_03_Bar01.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Health_03_Bar02.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Health_03_Bar03.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Health_04.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Health_04_Bar01.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Health_04_Bar02.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Health_04_Bar03.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Health_04_Bar04.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Health_04_Bar05.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Health_04_Bar06.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Health_04_Heart_Blue.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Health_04_Heart_Blue_Clear.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Health_04_Heart_Red.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Health_04_Heart_Red_Clear.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Health_04_Heart_Yellow.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Health_04_Heart_Yellow_Clear.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Health_05_01.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Health_05_02.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Health_05_03.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Heart_Blue.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Heart_Blue_1.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Heart_Blue_2.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Heart_Blue_3.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Heart_Blue_4.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Heart_Orange.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Heart_Orange_1.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Heart_Orange_2.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Heart_Orange_3.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Heart_Orange_4.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Heart_Red.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Heart_Red_1.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Heart_Red_2.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Heart_Red_3.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Heart_Red_4.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Hearts.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Hearts_Blue_1.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Hearts_Blue_2.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Hearts_Blue_3.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Hearts_Blue_4.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Hearts_Blue_5.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Hearts_Red_1.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Hearts_Red_2.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Hearts_Red_3.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Hearts_Red_4.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Hearts_Red_5.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Hearts_Yellow_1.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Hearts_Yellow_2.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Hearts_Yellow_3.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Hearts_Yellow_4.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Hearts_Yellow_5.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Inventory.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Inventory_9Slices.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Inventory_Example_01.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Inventory_Example_02.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Inventory_Example_03.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Inventory_Example_04.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_1.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_10.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_2.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_3.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_4.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_5.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_6.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_7.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_8.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_9.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Settings.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Settings_Bar01.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Settings_Bar02.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Settings_Bar03.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Settings_Cross01.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Settings_Cross02.png create mode 100644 public/Retro Inventory/Retro Inventory/Original/Settings_Cross03.png create mode 100644 public/Retro Inventory/Retro Inventory/Read Me.txt create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Health_01.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Health_01_Bar01.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Health_01_Bar02.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Health_01_Bar03.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Health_02.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Health_02_Bar01.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Health_02_Bar02.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Health_02_Bar03.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Health_03.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Health_03_Bar01.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Health_03_Bar02.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Health_03_Bar03.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Bar01.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Bar02.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Bar03.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Bar04.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Bar05.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Bar06.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Heart_Blue.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Heart_Blue_Clear.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Heart_Red.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Heart_Red_Clear.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Heart_Yellow.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Heart_Yellow_Clear.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Health_05_01.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Health_05_02.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Health_05_03.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Blue.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Blue_1.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Blue_2.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Blue_3.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Blue_4.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Orange.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Orange_1.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Orange_2.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Orange_3.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Orange_4.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Red.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Red_1.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Red_2.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Red_3.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Red_4.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Blue_1.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Blue_2.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Blue_3.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Blue_4.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Blue_5.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Red_1.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Red_2.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Red_3.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Red_4.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Red_5.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Yellow_1.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Yellow_2.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Yellow_3.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Yellow_4.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Yellow_5.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_9Slices.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Example_01.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Example_02.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Example_03.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Example_04.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_1.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_10.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_2.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_3.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_4.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_5.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_6.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_7.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_8.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_9.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Settings.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Settings_Bar01.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Settings_Bar02.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Settings_Bar03.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Settings_Cross01.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Settings_Cross02.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 2x/Settings_Cross03.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Health_01.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Health_01_Bar01.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Health_01_Bar02.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Health_01_Bar03.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Health_02.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Health_02_Bar01.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Health_02_Bar02.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Health_02_Bar03.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Health_03.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Health_03_Bar01.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Health_03_Bar02.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Health_03_Bar03.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Bar01.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Bar02.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Bar03.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Bar04.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Bar05.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Bar06.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Blue.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Blue_Clear.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Red.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Red_Clear.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Yellow.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Yellow_Clear.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Health_05_01.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Health_05_02.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Health_05_03.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Blue.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Blue_1.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Blue_2.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Blue_3.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Blue_4.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Orange.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Orange_1.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Orange_2.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Orange_3.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Orange_4.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Red.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Red_1.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Red_2.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Red_3.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Red_4.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Blue_1.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Blue_2.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Blue_3.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Blue_4.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Blue_5.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Red_1.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Red_2.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Red_3.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Red_4.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Red_5.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Yellow_1.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Yellow_2.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Yellow_3.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Yellow_4.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Yellow_5.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_9Slices.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Example_01.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Example_02.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Example_03.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Example_04.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_1.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_10.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_2.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_3.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_4.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_5.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_6.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_7.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_8.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_9.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Settings.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Settings_Bar01.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Settings_Bar02.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Settings_Bar03.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Settings_Cross01.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Settings_Cross02.png create mode 100644 public/Retro Inventory/Retro Inventory/Scaled 3x/Settings_Cross03.png rename {assets/img/character/down => public/TopDownCharacter/Character}/Character_Down.png (100%) rename {assets/img/character/down => public/TopDownCharacter/Character}/Character_DownLeft.png (100%) rename {assets/img/character/down => public/TopDownCharacter/Character}/Character_DownRight.png (100%) rename {assets/img/character/left => public/TopDownCharacter/Character}/Character_Left.png (100%) rename {assets/img/character/right => public/TopDownCharacter/Character}/Character_Right.png (100%) rename {assets/img/character => public/TopDownCharacter/Character}/Character_RollDown.png (100%) rename {assets/img/character => public/TopDownCharacter/Character}/Character_RollDownLeft.png (100%) rename {assets/img/character => public/TopDownCharacter/Character}/Character_RollDownRight.png (100%) rename {assets/img/character => public/TopDownCharacter/Character}/Character_RollLeft.png (100%) rename {assets/img/character => public/TopDownCharacter/Character}/Character_RollRight.png (100%) rename {assets/img/character => public/TopDownCharacter/Character}/Character_RollUp.png (100%) rename {assets/img/character => public/TopDownCharacter/Character}/Character_RollUpLeft.png (100%) rename {assets/img/character => public/TopDownCharacter/Character}/Character_RollUpRight.png (100%) rename {assets/img/character => public/TopDownCharacter/Character}/Character_SlashDownLeft.png (100%) rename {assets/img/character => public/TopDownCharacter/Character}/Character_SlashDownRight.png (100%) rename {assets/img/character => public/TopDownCharacter/Character}/Character_SlashUpLeft.png (100%) rename {assets/img/character => public/TopDownCharacter/Character}/Character_SlashUpRight.png (100%) rename {assets/img/character/up => public/TopDownCharacter/Character}/Character_Up.png (100%) rename {assets/img/character/up => public/TopDownCharacter/Character}/Character_UpLeft.png (100%) rename {assets/img/character/up => public/TopDownCharacter/Character}/Character_UpRight.png (100%) rename {assets/img/character => public/TopDownCharacter}/Weapon/Sword_DownLeft.png (100%) rename {assets/img/character => public/TopDownCharacter}/Weapon/Sword_DownRight.png (100%) rename {assets/img/character => public/TopDownCharacter}/Weapon/Sword_UpLeft.png (100%) rename {assets/img/character => public/TopDownCharacter}/Weapon/Sword_UpRight.png (100%) create mode 100644 src/engine/animation/Animation.ts create mode 100644 src/engine/assets/AssetLoader.ts create mode 100644 src/engine/assets/SpriteSheet.ts create mode 100644 src/engine/assets/TextureAtlas.ts create mode 100644 src/engine/assets/index.ts create mode 100644 src/engine/components/Animator.ts create mode 100644 src/engine/components/SpriteRenderer.ts create mode 100644 src/engine/core/Engine.ts create mode 100644 src/engine/core/GameLoop.ts create mode 100644 src/engine/core/Scene.ts create mode 100644 src/engine/core/SceneManager.ts create mode 100644 src/engine/core/Time.ts create mode 100644 src/engine/debug/DebugPanel.ts create mode 100644 src/engine/entities/Component.ts create mode 100644 src/engine/entities/GameObject.ts create mode 100644 src/engine/entities/Transform.ts create mode 100644 src/engine/input/InputManager.ts create mode 100644 src/engine/math/Color.ts create mode 100644 src/engine/math/Matrix3.ts create mode 100644 src/engine/math/Rect.ts create mode 100644 src/engine/math/Vector2.ts create mode 100644 src/engine/physics/BoxCollider2D.ts create mode 100644 src/engine/physics/CircleCollider2D.ts create mode 100644 src/engine/physics/Collider2D.ts create mode 100644 src/engine/physics/PhysicsSystem.ts create mode 100644 src/engine/physics/Rigidbody2D.ts create mode 100644 src/engine/physics/index.ts create mode 100644 src/engine/rendering/Camera.ts create mode 100644 src/engine/rendering/Shader.ts create mode 100644 src/engine/rendering/SpriteBatch.ts create mode 100644 src/engine/rendering/Texture.ts create mode 100644 src/engine/rendering/WebGLRenderer.ts create mode 100644 src/engine/ui/RectTransform.ts create mode 100644 src/engine/ui/UIButton.ts create mode 100644 src/engine/ui/UICanvas.ts create mode 100644 src/engine/ui/UIComponent.ts create mode 100644 src/engine/ui/UIImage.ts create mode 100644 src/engine/ui/UIText.ts create mode 100644 src/engine/ui/index.ts create mode 100644 src/engine/webgl/Buffer.ts create mode 100644 src/engine/webgl/GLContext.ts delete mode 100644 src/entities/classes/Archer.ts delete mode 100644 src/entities/classes/Warrior.ts delete mode 100644 src/entities/projectiles/Arrow.ts delete mode 100644 src/entities/projectiles/Slash.ts create mode 100644 src/game/Game.ts create mode 100644 src/game/components/HealthComponent.ts create mode 100644 src/game/components/PlayerController.ts create mode 100644 src/game/components/Projectile.ts create mode 100644 src/game/scenes/GameScene.ts create mode 100644 src/game/ui/UIHealthBar.ts create mode 100644 src/game/utils/TextureGenerator.ts create mode 100644 src/index.ts delete mode 100644 src/main.ts delete mode 100644 src/models/Entity.ts delete mode 100644 src/models/Player.ts delete mode 100644 src/models/Projectile.ts delete mode 100644 src/models/Prop.ts delete mode 100644 src/sprites/PngItem_2102882.png delete mode 100644 src/sprites/bow.png delete mode 100644 src/sprites/player.png delete mode 100644 src/utils/Event.ts delete mode 100644 src/utils/SpriteSheet.ts delete mode 100644 src/vendor/Game.ts delete mode 100644 src/vendor/Renderer.ts delete mode 100644 src/vendor/State.ts delete mode 100644 src/vendor/Ui.ts delete mode 100644 src/vite-env.d.ts create mode 100644 vite.config.d.ts create mode 100644 vite.config.d.ts.map create mode 100644 vite.config.js create mode 100644 vite.config.ts diff --git a/.cursor/main.mdc b/.cursor/main.mdc new file mode 100644 index 0000000..904a026 --- /dev/null +++ b/.cursor/main.mdc @@ -0,0 +1,200 @@ +--- +alwaysApply: true +--- +# Règles Cursor - Moteur de Jeu 2D WebGL TypeScript + +## Comportement de l'IA - Rôle et Expertise + +Je suis un expert en développement de moteurs de jeu 2D, WebGL, TypeScript et architecture logicielle. Je connais les meilleures pratiques actuelles et les dernières avancées technologiques dans ces domaines. + +### Mon Expertise +- **WebGL/GPU Programming** : Expert en optimisation GPU, batch rendering, shaders GLSL, gestion des buffers et minimisation des draw calls +- **Architecture ECS** : Maîtrise des patterns Entity Component System, composition over inheritance, systèmes découplés +- **Performance** : Expert en profiling, optimisation mémoire, object pooling, spatial partitioning, dirty flags +- **TypeScript** : Maîtrise approfondie avec strict mode, types avancés, génériques, interfaces +- **Game Engine Architecture** : Connaissance des architectures modulaires, systèmes de rendu, physique, collisions + +### Comment je travaille +1. **Proactivité** : Je suggère les meilleures solutions avant que des problèmes n'apparaissent +2. **Performance First** : Je priorise toujours la performance et suggère des optimisations pertinentes +3. **Code Quality** : Je maintiens un code propre, documenté, typé strictement et respectant les SOLID principles +4. **Connaissances à jour** : J'utilise les dernières pratiques WebGL, TypeScript et patterns de game engine +5. **Architecture solide** : Je propose des solutions évolutives et maintenables +6. **Documentation** : J'ajoute des commentaires explicatifs pour les décisions architecturales complexes + +### Mes Priorités +- ✅ Performance et optimisation (SpriteBatch, pooling, culling) +- ✅ TypeScript strict avec types explicites +- ✅ Architecture ECS respectée +- ✅ Gestion mémoire efficace (éviter GC spikes) +- ✅ Code testable et modulaire +- ✅ Documentation des choix techniques importants + +### Quand je code +- Je vérifie toujours la performance avant/après optimisations +- J'utilise systématiquement `deltaTime` pour tous les mouvements +- Je privilégie la composition à l'héritage +- Je documente les algorithmes complexes et les décisions d'architecture +- Je suggère des améliorations même si le code fonctionne + +## Architecture Globale + +Ce projet est un moteur de jeu 2D utilisant WebGL et TypeScript. L'architecture suit le pattern ECS (Entity Component System) avec composition plutôt qu'héritage. + +### Structure des Modules +- `core/` : Engine, GameLoop, SceneManager, Time, Scene +- `rendering/` : WebGLRenderer, SpriteBatch (★ crucial), Shader, Material, Texture, Camera, RenderQueue +- `webgl/` : GLContext, Buffer, VertexArray, Framebuffer +- `entities/` : GameObject, Transform, Component (classe de base) +- `components/` : SpriteRenderer, Animator, RigidBody, Colliders, ParticleEmitter, TilemapRenderer +- `systems/` : PhysicsSystem, CollisionSystem, AnimationSystem +- `input/` : InputManager +- `audio/` : AudioManager +- `assets/` : AssetLoader +- `math/` : Vector2, Matrix3, Rect, Color +- `utils/` : ObjectPool, EventBus, Quadtree, Debug + +## Principes de Performance (CRITIQUES) + +### 1. SpriteBatch - OBLIGATOIRE +- Accumule plusieurs sprites dans un buffer avant de les dessiner +- Flush seulement quand : batch plein, texture change, shader change +- Objectif : minimiser les draw calls (10000 sprites = 1 draw call) +- Structure vertex : [x, y, u, v, r, g, b, a] + +### 2. Object Pooling +- Utiliser ObjectPool pour tout ce qui spawn/despawn fréquemment +- Projectiles, particules, ennemis +- Réutiliser plutôt que créer/détruire (évite GC) + +### 3. Spatial Partitioning +- Utiliser Quadtree pour collisions si >500 objets +- Réduit de O(n²) à O(n log n) + +### 4. Dirty Flags +- Marquer les objets "sales" (dirty) quand changent +- Recalculer seulement si dirty (ex: Transform.worldMatrix) +- Économise 80-90% des calculs pour objets statiques + +### 5. Frustum Culling +- Ne rendre que les objets visibles par la caméra +- Skip les objets hors écran + +## Patterns de Code + +### ECS (Entity Component System) +```typescript +// Composition, PAS héritage +const enemy = new GameObject(); +enemy.addComponent(new SpriteRenderer()); +enemy.addComponent(new RigidBody()); +enemy.addComponent(new BoxCollider()); +``` + +### Cycle de Vie Component +- `awake()` : Appelé à l'ajout +- `start()` : Avant première update +- `update(deltaTime)` : Chaque frame +- `onDestroy()` : À la destruction + +### DeltaTime OBLIGATOIRE +- TOUS les mouvements utilisent `deltaTime` +- `position.x += speed * Time.deltaTime` (pas `position.x += 5`) +- Permet indépendance des FPS + +### Fixed Timestep pour Physique +- Physique en timestep fixe (ex: 1/60s) +- Logique de jeu en timestep variable + +## Règles TypeScript + +- Mode strict activé +- Interfaces pour tout (IRenderable, IUpdatable, ICollidable) +- Types explicites, éviter `any` +- Classes avec responsabilité unique + +## WebGL Best Practices + +1. **Minimiser les State Changes** : Shader, texture, blend mode coûteux +2. **Tri de RenderQueue** : Par layer → shader → texture +3. **Texture Atlas** : Combiner plusieurs textures en une +4. **Gérer les erreurs** : `gl.getError()` après opérations critiques +5. **Bind/Unbind** : Toujours unbind après usage + +## Structure des Classes + +### Transform +- Propriétés locales : `localPosition`, `localRotation`, `localScale` +- Propriétés monde : `position`, `rotation`, `scale` (getters/setters) +- Hiérarchie parent-enfant supportée +- Matrice worldMatrix avec cache et dirty flag + +### GameObject +- `transform: Transform` (toujours présent) +- `components: Map` +- Hiérarchie parent-enfant +- Cycle de vie : awake → start → update → destroy +- `tag` pour catégorisation + +### Component +- Classe abstraite +- `gameObject: GameObject` référence +- `transform` shortcut vers `gameObject.transform` +- `enabled: boolean` + +## Conventions de Nommage + +- Classes : PascalCase (`SpriteRenderer`, `WebGLRenderer`) +- Méthodes : camelCase (`update`, `render`, `getWorldMatrix`) +- Propriétés privées : `_private` avec underscore +- Interfaces : Préfixe `I` (`IRenderable`, `IUpdatable`) +- Constantes : UPPER_SNAKE_CASE + +## Mathématiques + +- `Vector2` : (x, y) avec méthodes add, subtract, multiply, normalize, length, distance, lerp +- `Matrix3` : Transformations 2D (translation, rotation, scale) +- `Rect` : Rectangle avec contains, overlaps, intersection +- `Color` : RGBA (0-1) + +## Event Bus + +Utiliser EventBus pour communication découplée entre systèmes : +```typescript +eventBus.on("player:died", (data) => { ... }); +eventBus.emit("player:died", { score: 1500 }); +``` + +## Debug et Performance + +- Dessiner les colliders (debug mode) +- Afficher FPS, draw calls, objets actifs +- Profiling avec `performance.mark()` et `performance.measure()` +- Ne jamais deviner les bottlenecks, mesurer d'abord + +## Anti-Patterns à ÉVITER + +❌ `position.x += 5` (dépend des FPS) +✅ `position.x += 300 * Time.deltaTime` (indépendant des FPS) + +❌ Créer/détruire objets chaque frame +✅ Utiliser ObjectPool + +❌ Tester toutes les collisions O(n²) +✅ Utiliser Quadtree + +❌ Recalculer matrices chaque frame +✅ Utiliser dirty flags + +❌ Héritage profond (Enemy extends Character extends Entity) +✅ Composition avec Components + +## Notes Importantes + +- Le SpriteBatch est le cœur de la performance +- TOUJOURS utiliser deltaTime pour les mouvements +- Fixed timestep pour physique (déterministe) +- TypeScript strict mode +- Documenter les décisions importantes dans le code +- Tester chaque système indépendamment avant intégration + diff --git a/.gitignore b/.gitignore index a547bf3..b7a1cfb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,30 @@ +# Dépendances +node_modules/ + +# Build +dist/ +*.js.map + +# Vite +.vite/ +dist-ssr/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + # Logs -logs *.log npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* -node_modules -dist -dist-ssr -*.local +# Cache +.cache/ +*.cache -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/DEV_LOG.md b/DEV_LOG.md new file mode 100644 index 0000000..e316269 --- /dev/null +++ b/DEV_LOG.md @@ -0,0 +1,166 @@ +# Historique de Développement + +## 2025-11-02 - Setup Initial + +- Configuration du projet : package.json, tsconfig.json (strict mode), .gitignore +- Structure de dossiers complète selon CORE.MD (core, rendering, webgl, entities, components, systems, input, audio, assets, math, utils, physics, animation, shaders) +- Index.html de base avec canvas WebGL +- Point d'entrée src/index.ts créé +- Compilation TypeScript fonctionnelle + +## 2025-11-02 - Premier Rendu WebGL + +- GLContext : Abstraction WebGL basique avec initialisation, clear, resize, gestion d'erreurs +- Shader : Classe de compilation et gestion des shaders GLSL (vertex/fragment) +- Test de rendu : Triangle rose coloré affiché à l'écran avec boucle requestAnimationFrame +- Fonctionnalité : Rendu WebGL basique opérationnel + +## 2025-11-02 - Configuration Dev Server + +- Vite : Serveur de développement avec hot-reload ajouté +- Configuration : vite.config.ts avec port 3000 et ouverture automatique +- Scripts : Migration vers pnpm, script `dev` pour développement +- Index.html : Adaptation pour Vite (chargement direct des .ts) + +## 2025-11-02 - Classes Math et Buffer + +- Vector2 : Classe complète avec add, subtract, multiply, normalize, distance, lerp, rotate, etc. (mutables et immutables) +- Color : Classe RGBA avec lerp, multiply, conversion hex/RGBA, constantes prédéfinies +- Buffer : Encapsulation WebGL buffer avec bind, uploadData, uploadSubData, gestion STATIC/DYNAMIC/STREAM +- Amélioration rendu : 3 triangles colorés (rouge, vert, bleu pulsant) avec utilisation des nouvelles classes + +## 2025-11-02 - Core : Time et GameLoop + Math complètes + +- Time : Classe statique pour gestion deltaTime, timeScale, fps, time total. Essentiel pour mouvements indépendants des FPS +- GameLoop : Boucle de jeu avec requestAnimationFrame, fixed timestep pour physique, callbacks update/fixedUpdate/render +- Rect : Rectangle avec contains, overlaps, intersection, union, expand, shrink, propriétés calculées (left, right, top, bottom, center) +- Matrix3 : Matrice 3×3 pour transformations 2D (translate, rotate, scale, multiply, inverse, orthographic projection) +- Intégration : Test de rendu utilise maintenant GameLoop et Time, affichage FPS en temps réel + +## 2025-11-02 - Texture et Camera + +- Texture : Chargement d'images WebGL (loadFromURL asynchrone), gestion filtrage (NEAREST/LINEAR), wrapping (CLAMP/REPEAT), bind/unbind, dispose +- Camera : Projection orthographique, view matrix avec position/zoom/rotation, conversions écran↔monde, bounds (limites), visible bounds pour culling, dirty flags pour optimisation + +## 2025-11-02 - Architecture complète : WebGLRenderer, SpriteBatch, Scene, SceneManager, Engine + +- WebGLRenderer : Orchestrateur du rendu, gestion shaders/textures, rendu de scènes avec caméra, clear canvas +- SpriteBatch : Batch rendering crucial (accumule sprites en 1 draw call), gestion vertices/indices, flush automatique, transformation avec matrices +- Scene : Scène de jeu avec GameObjects (structure prête), caméra intégrée, méthodes onLoad/onUnload, update/render +- SceneManager : Gestion multiple scènes (Map), chargement/déchargement, transitions, update/render scène active +- Engine : Orchestrateur principal, initialise tous les systèmes, démarre GameLoop, API publique (start/stop/pause/resume), resize automatique + +## 2025-11-02 - Intégration Engine dans index.ts + +- Refactorisation : index.ts utilise maintenant Engine au lieu d'instancier directement GLContext/GameLoop +- Workflow : Engine → initialize() → create Scene → registerScene() → loadScene() → start() +- Test de rendu : 3 triangles colorés (rouge, vert, bleu pulsant) intégrés via Engine/Scene/WebGLRenderer +- Architecture propre : Code organisé avec Engine comme point d'entrée unique + +## 2025-11-02 - Phase 4 : Entity Component System - GameObject, Transform, Component + +- Component : Classe abstraite de base pour tous les composants, philosophie ECS (composition > héritage), cycle de vie (awake, start, update, onDestroy), référence au GameObject parent, état enabled/disabled +- Transform : Position/rotation/scale locales et monde, hiérarchie parent-enfant (setParent), matrice worldMatrix avec dirty flags pour optimisation (cache), méthodes translate/rotate/lookAt, conversions local↔monde, propriétés calculées (right, up, forward) +- GameObject : Entité de base du jeu, contient un Transform (automatique) et des Components (Map), hiérarchie parent-enfant (setParent/addChild/removeChild), cycle de vie complet (awake/start/update/destroy), propriétés (name, active, tag, layer), recherche (findChild, getComponentInChildren), destruction différée pour sécurité +- Scene : Intégration GameObject complète, update automatique de tous les GameObjects actifs, méthodes de recherche (findGameObjectByName, findGameObjectsByTag), nettoyage automatique des objets détruits +- Compilation : Tous les fichiers compilent sans erreur, TypeScript strict mode respecté + +## 2025-11-02 - Phase 5 : Input Manager et Composants de Rendu + +- InputManager : Gestion centralisée des entrées (clavier, souris), états différenciés (Down/Held/Up/Released), normalisation des touches, gestion molette, conversion coordonnées écran↔monde via caméra, intégré dans Engine (update chaque frame) +- SpriteRenderer : Composant pour affichage de sprites 2D, propriétés (texture, sourceRect pour spritesheets, tint, flipX/Y, layer, pivot), rendu via SpriteBatch, méthode setSprite() pour configuration rapide +- Animation : Structure de données pour animations sprite (frames, frameDuration, loop, events), calcul durée totale, accès sécurisé aux frames +- Animator : Composant de gestion d'animations, recherche automatique du SpriteRenderer, play/stop/pause/resume, support boucle/one-shot, vitesse de lecture ajustable, déclenchement d'événements à certaines frames, update automatique du sourceRect +- Engine : Intégration InputManager complète (propriété `input` publique), initialisation et update automatique dans GameLoop, dispose() pour nettoyage +- Compilation : Tous les nouveaux fichiers compilent sans erreur + +## 2025-11-02 - Phase 6 : Système de Rendu Complet + +- WebGLRenderer : Intégration SpriteBatch complète, rendu automatique de toutes les scènes via SpriteBatch, application des matrices projection/view via shader uniforms +- Scene.getAllRenderables() : Collecte récursive de tous les SpriteRenderer (GameObjects + enfants), tri automatique par layer (z-index), filtre objets actifs/enabled +- Scene._collectRenderablesRecursive() : Parcours hiérarchique complet pour trouver tous les SpriteRenderer dans la scène +- SpriteBatch : Configuration complète des attributs WebGL (aPosition, aTexCoord, aColor), positions monde passées au shader (transformation view/projection dans le vertex shader), support shader optionnel dans begin() +- Pipeline de rendu complet : WebGLRenderer.render() → Scene.getAllRenderables() → SpriteRenderer.render() → SpriteBatch.draw() → flush() → drawElements() +- Compilation : Système de rendu complet et fonctionnel, prêt pour afficher des sprites avec GameObjects + +## 2025-11-02 - Phase 7 : Exemple de Jeu Complet dans index.ts + +- Exemple complet : Implémentation d'un jeu de test dans index.ts avec tous les systèmes +- PlayerController : Composant custom pour contrôler le joueur avec les flèches/WASD, changement d'animation automatique (idle/walk) +- Textures générées : Fonction createSolidColorTexture() pour créer des textures de couleur solide sans dépendances externes +- GameObjects multiples : Joueur contrôlable, 5 ennemis statiques, 3 pièces animées, 100 tiles de sol (grille 10x10) +- Animations : Joueur avec animations idle/walk, pièces avec animation spin rapide +- Rendu complet : Tous les SpriteRenderer sont rendus automatiquement via le système de rendu de la scène +- InputManager : Testé avec déplacement du joueur (flèches ou WASD), animations qui changent selon le mouvement +- Systèmes testés : GameObject, Transform, Component, SpriteRenderer, Animator, InputManager, Scene, Engine, WebGLRenderer, SpriteBatch +- Compilation : Code compile sans erreur, exemple prêt à être exécuté et testé dans le navigateur + +## 2025-11-02 - Phase 8 : Physique et Collisions + +- Collider2D : Classe de base abstraite pour tous les colliders, support triggers, événements OnTriggerEnter/Exit et OnCollisionEnter/Exit avec callbacks +- BoxCollider2D : Collider rectangulaire (AABB - Axis-Aligned Bounding Box), détection de collisions Box-Box et Box-Circle +- CircleCollider2D : Collider circulaire, détection de collisions Circle-Circle, délègue Box-Circle à BoxCollider2D +- Rigidbody2D : Composant de physique avec vélocité linéaire/angulaire, masse, drag (friction), gravité, forces et impulsions, intégration du mouvement via fixed timestep +- PhysicsSystem : Système de détection et résolution de collisions, fixed timestep (60 FPS) pour stabilité physique, collecte récursive des colliders, résolution de collisions avec séparation proportionnelle à la masse, gestion des triggers vs collisions physiques, nettoyage automatique des événements Exit +- Intégration Engine : PhysicsSystem intégré dans Engine, appelé dans GameLoop.onFixedUpdate(), accessible via engine.physics +- Compilation : Tous les fichiers compilent sans erreur, système de physique complet et prêt à l'utilisation + +## 2025-11-02 - Phase 9 : Asset Management + +- AssetLoader : Système centralisé de chargement d'assets (textures, JSON), cache automatique, chargement parallèle avec promesses, gestion des erreurs, préchargement avec progression (callback onProgress), méthodes dispose() pour libérer la mémoire +- SpriteSheet : Gestion de spritesheets avec grille uniforme, création automatique de sprites depuis la grille, méthodes getSprite/getSpriteByIndex/getSpriteByGridPosition, support création manuelle de sprites personnalisés, propriétés (spriteWidth, spriteHeight, columns, rows) +- TextureAtlas : Gestion d'atlas de textures avec JSON (format compatible TexturePacker), parse automatique des frames depuis JSON, recherche de sprites par nom, méthode statique fromAssetLoader() pour création facile, support des métadonnées d'atlas +- Intégration Engine : AssetLoader intégré dans Engine, accessible via engine.assets, dispose() appelé dans Engine.dispose() +- Fichier d'export : index.ts dans src/assets pour faciliter l'importation de tous les composants Asset Management +- Compilation : Tous les fichiers compilent sans erreur, système d'Asset Management complet et prêt à l'utilisation + +## 2025-11-02 - Phase 10 : UI System + +- UICanvas : Système d'interface utilisateur séparé de la scène de jeu, caméra dédiée pour l'UI, gestion de GameObjects UI, rendu en overlay par-dessus la scène, intégré dans Scene avec méthode createUICanvas() +- UIComponent : Classe de base abstraite pour tous les composants UI, extends Component, gestion RectTransform automatique, cycle de vie initialize()/update()/render(), méthode render() pour dessiner via SpriteBatch +- RectTransform : Système de layout complet avec anchors (anchorMin/anchorMax), pivot, anchoredPosition, size, canvasSize, anchors presets (TopLeft, TopCenter, TopRight, MiddleLeft, MiddleCenter, MiddleRight, BottomLeft, BottomCenter, BottomRight, StretchHorizontal, StretchVertical, StretchAll), hiérarchie parent-enfant, conversion automatique coordonnées UI ↔ écran WebGL +- UIText : Affichage de texte utilisant Canvas 2D pour le rendu, propriétés (text, fontSize, fontFamily, color, align), mise à jour automatique de la texture, rendu via SpriteBatch avec gestion pivot/anchor, support ancrage et positionnement flexible +- UIButton : Bouton interactif avec états (normal, hover, pressed), couleurs personnalisables par état, détection hover/press via InputManager et conversion coordonnées, callback onClick pour actions personnalisées, rendu automatique avec transition couleur selon état +- UIImage : Affichage d'images dans l'UI, utilisation de Texture WebGL, gestion sourceRect pour sprites, support tint color, rendu via SpriteBatch avec gestion pivot/anchor +- Intégration Scene : UICanvas intégré dans Scene, update() et render() automatiques, nettoyage dans onUnload(), rendu en overlay via WebGLRenderer.render() après le rendu de la scène +- Exemple index.ts : Création d'un score texte (TopLeft), bouton interactif (BottomCenter) avec texte, démonstration du système UI complet avec ancrage et interactions +- Compilation : Tous les fichiers compilent sans erreur, système UI complet et fonctionnel, prêt pour création d'interfaces utilisateur dans les jeux + +## 2025-11-03 - Phase 11 : Intégration Spritesheets Personnalisés + +- Structure Assets : Migration des sprites de src/Sprites/ vers public/Sprites/ pour compatibilité Vite +- Organisation Sprites : 4 dossiers d'animations (IDLE, RUN, ATTACK 1, ATTACK 2), 4 directions par animation (down, left, right, up), 8 frames horizontales par spritesheet +- Auto-détection Dimensions : Système automatique de calcul de la taille des frames (largeur_texture / 8 frames), log des dimensions détectées dans la console pour debug +- Configuration Joueur : Transform.rotation = 90° pour corriger l'orientation des sprites, Transform.scale = 2x pour meilleure visibilité, collider auto-ajusté aux dimensions détectées +- Chargement Assets : AssetLoader charge toutes les textures (16 spritesheets au total), gestion fallback vers textures de test en cas d'erreur, logs détaillés du chargement +- SpriteSheet Helper : Fonction createAnimationFromSpriteSheet() pour créer automatiquement les animations depuis les textures chargées, calcul automatique du nombre de colonnes/rows +- Animations Configurées : 16 animations créées (4 états × 4 directions), frameDuration ajusté (0.15s pour idle, 0.1s pour run/attack), loop activé pour idle/run, désactivé pour attack +- PlayerController : Gestion automatique du changement de texture selon la direction et l'état, mapping gauche/droite inversé pour compenser la rotation, support 8 directions de mouvement, animations synchronisées avec les déplacements +- Debug Console : Affichage des dimensions de texture chargée, taille calculée vs configurée, warnings si dimensions incorrectes, vérification du nombre de frames détectées +- Optimisations : Sprites redimensionnés à 2x pour visibilité, rotation corrigée, colliders adaptés dynamiquement, système prêt pour spritesheets de tailles variables + +## 2025-11-03 - Canvas Plein Écran et Redimensionnement Automatique + +- HTML Fullscreen : Canvas prend maintenant 100% de la largeur et hauteur de la fenêtre, overflow hidden pour éviter les scrollbars, info FPS en overlay (position absolute) +- Initialisation Dynamique : Canvas créé avec window.innerWidth et window.innerHeight au lieu de dimensions fixes (800x600) +- Joueur Centré : Position initiale du joueur calculée dynamiquement (width/2, height/2) pour être toujours au centre de l'écran +- Redimensionnement Automatique : Event listener sur window.resize pour adapter automatiquement le canvas, renderer et caméra quand la fenêtre change de taille +- Responsive : Le jeu s'adapte maintenant à toutes les résolutions d'écran (desktop, laptop, fullscreen) +- CSS Optimisé : Body et HTML en 100% width/height, canvas en display block pour éviter les espaces blancs, info FPS avec fond semi-transparent en overlay + +## 2025-11-04 - Migration TopDownCharacter : 8 Directions et Sprites 16x16 + +- **Nouveaux Sprites TopDownCharacter** : Migration complète des anciens sprites vers TopDownCharacter (16x16 pixels, 4 frames par animation au lieu de 8) +- **8 Directions de Mouvement** : Implémentation complète du système 8 directions (haut, bas, gauche, droite + 4 diagonales : haut-gauche, haut-droite, bas-gauche, bas-droite) +- **Gestion Inputs Combinés** : Détection Z+Q, Z+D, S+Q, S+D pour mouvement diagonal fluide, normalisation des vecteurs pour vitesse constante +- **PlayerController Refactorisé** : Nouvelle méthode `getDirection8Way()` pour calculer la direction en 8 points cardinaux selon les inputs combinés +- **Chargement Assets** : 20 spritesheets chargés (8 walk + 8 roll + 4 slash), chemins mis à jour vers `/TopDownCharacter/Character/` +- **Animations 4 Frames** : Adaptation du `createAnimationFromSpriteSheet()` pour gérer 4 frames au lieu de 8, frameDuration ajusté (0.12s walk, 0.08s roll/slash) +- **Collider Ajusté** : BoxCollider2D du joueur adapté à 16x16 pixels (taille réelle du sprite) au lieu de l'ancienne taille calculée +- **Rotation Supprimée** : Transform.rotation = 0° pour TopDownCharacter (sprites correctement orientés dès l'origine) +- **Scale Optimisé** : Transform.scale = 3× pour sprites 16x16 (affichage 48x48) pour meilleure visibilité à l'écran +- **20 Animations Créées** : 8 walk (toutes directions), 8 roll (toutes directions), 4 slash (diagonales uniquement) +- **Mapping Directions** : up/down/left/right + upleft/upright/downleft/downright, priorité diagonales si deux touches simultanées +- **Messages Console Améliorés** : Logs détaillés du chargement, dimensions auto-détectées, instructions contrôles 8 directions +- **Compilation** : Aucune erreur TypeScript strict mode, tous les systèmes fonctionnels avec nouveaux sprites + diff --git a/README.md b/README.md index 84d78be..0bb4670 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,101 @@ -# tiny-shooter-game +# CastleStorm Engine -CastleStorm un jeu d'action ayant pour but de survivre le plus longtemps possible. +Moteur de jeu 2D avec WebGL en TypeScript. -Au cours de votre aventure, -vous rencontrerez des ennemis de plus en plus puissants. -Tuer les ennemis vous permettra de gagner de l'expérience, -et de l'argent pour acheter des améliorations dans une boutique tout le long de votre partie. +## 🎯 Objectifs -Attention, vous n'avez qu'une seule vie, Mort = Game Over ! (。◕‿◕。) +Moteur de jeu 2D performant utilisant : +- **WebGL** pour le rendu GPU +- **TypeScript** avec strict mode +- **ECS (Entity Component System)** pour l'architecture +- **Optimisations** : SpriteBatch, Object Pooling, Spatial Partitioning -Pour vous déplacer, utilisez les touches ZQSD, -et pour attaquer, utilisez le clic gauche de votre souris. +## 📋 Prérequis + +- Node.js 18+ et npm +- Un navigateur moderne supportant WebGL + +## 🚀 Installation + +```bash +# Installer les dépendances +pnpm install + +# Compiler le projet +pnpm run build + +# Mode dev avec hot-reload (recommandé) +pnpm run dev +``` + +Le serveur de dev s'ouvre automatiquement sur `http://localhost:3000`. + +## 📁 Structure du Projet + +``` +src/ +├── core/ # Engine, GameLoop, SceneManager, Time +├── rendering/ # WebGLRenderer, SpriteBatch, Shader, Camera +├── webgl/ # Abstraction WebGL (GLContext, Buffer, etc.) +├── entities/ # GameObject, Transform, Component +├── components/ # SpriteRenderer, Animator, RigidBody, etc. +├── systems/ # PhysicsSystem, CollisionSystem +├── input/ # InputManager +├── audio/ # AudioManager +├── assets/ # AssetLoader +├── math/ # Vector2, Matrix3, Rect, Color +└── utils/ # ObjectPool, EventBus, Quadtree +``` + +## 🏗️ Architecture + +Le moteur suit le pattern **ECS (Entity Component System)** : + +- **Entities (GameObject)** : Objets du jeu +- **Components** : Comportements/composants attachés aux entités +- **Systems** : Systèmes qui mettent à jour les composants + +## 📚 Documentation + +Voir `docs/CORE.MD` pour la documentation complète de l'architecture. + +## 🛠️ Développement + +### Compilation + +```bash +npm run build # Compilation unique +npm run watch # Mode watch +``` + +### Scripts disponibles + +- `pnpm run dev` : Serveur de développement avec hot-reload (Vite) +- `pnpm run build` : Compile le TypeScript et build de production +- `pnpm run preview` : Aperçu du build de production +- `pnpm run clean` : Supprime le dossier dist + +## 🎮 Utilisation + +```typescript +import { Engine } from './core/Engine'; + +const engine = new Engine(800, 600); +await engine.initialize(); +engine.start(); +``` + +## 📝 TODO + +Le développement suit la checklist de `docs/CORE.MD` : + +- [x] Phase 1 : Setup et configuration +- [ ] Phase 2 : Core du moteur +- [ ] Phase 3 : Système WebGL +- [ ] Phase 4 : Système de rendu +- [ ] ... + +## 📄 Licence + +MIT -C'est parti ! Soyez réactif et alerte. Bonne chance ! Vous en aurez besoin :) diff --git a/assets/css/main.css b/assets/css/main.css deleted file mode 100644 index 7aaa082..0000000 --- a/assets/css/main.css +++ /dev/null @@ -1,28 +0,0 @@ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: 'Press Start 2P', cursive; - font-size: 1.5rem; -} - -.background { - background-image: url("../img/background.jpg"); - background-size: cover; - background-position: center; -} - -.sideMenu { - background-image: url("../img/bricks.png"); - background-repeat: repeat; - box-shadow: inset 0px 0px 6px 6px rgba(0, 0, 0, 0.5); -} - -canvas { - height: 100vh; - width: 100vw; - display: block; -} \ No newline at end of file diff --git a/assets/img/background.jpg b/assets/img/background.jpg deleted file mode 100644 index 59c790ea2f68f33b61af41838f51aeb20932112a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 83300 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⋘NxDPsVu5AlJ*re&nkm_ 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 z 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 diff --git a/assets/img/bricks.png b/assets/img/bricks.png deleted file mode 100644 index bebd5eb2627f641d1b3d826c988f68bb274154d6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 448 zcmeAS@N?(olHy`uVBq!ia0y~yU;#3j92l8_lxB3>G$6$m;1lBNgnPbQgQ3;4Mjc%1s;}z|NdWA_^?F6uf#y&h{mR!Q4fQGYAYS)Ufv@=-#ib<27(3~ z2SG-L9`-wRo$>pHAz}_<3p&88H7O7QhAs{Uh67$~5FS`kM+I5RiD_g?8OUv5Op4}N UelSISFDL{&UHx3vIVCg!08OJy761SM diff --git a/assets/img/gui/gui.png b/assets/img/gui/gui.png deleted file mode 100644 index bb75b1d145c63017f195606e41cfbbf56b21d66f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3080 zcmeH}`BxKJ7RO&zszZe>2`FOAApzRPRxvoXkOmC_MWsi1& zK@{9|Sfi+*EJi^>HzMWAxUo!VGR+!AA<7~cS(1>@73}|DewlNof2wzHy>s8apYJ{I zzKo!NwHBsdngReU{MN1B1OP-VA%K#JPuRYaodB3M`K|U2PKp@ns^@>T+5T}{AWZ+~ zRR>3|55hc5L+wFEbR^b=9X^}q%ygdr8%Tm2zp@8aiw=jZ$4;W@wom7_@E^Qav<+Ra z*p)K)eju@nQy4I4*peXX**|>!Mu(S-k@vF$#$7Bnxpi)l9fUL@z?w;0&jm>4&3FU> z)c+-;8X9@6StNQHHx)NpiYe&`**UhWvzvm64Ej6yo4mB|Ta;JlabR>XS23hC1Ovf@ zyw~LxixHl;p(m`VZo2Q`kb1cHK6Rjgy8P($XuBq{@q9!APqY(E4UCGKQh%P@-~Up* zxLk2pJ)&9U)TW*sY0x)j=RCYwP%q6dZZgN3?&*b|mx^O5o`%V+s`T9vY~c&zuD4-7 zKAei)UvuKEoEp(CA_uYlBMViUnnFlGCLSLriaJ*hODZ%2?f1grs^yrXN2nR>XQXzV zF3~0AazZ9LdUXZr6`ZNK?_!c0vj;A=jiuHi7Yhvv+X{0mdEY&h{qnvLYPL@qJ~83O z(vN1|JaAdAzVN#DA)~IZVYA6=x6y^5-BDQ%^HRwpZ4C^5wWfYrP`@;G!*Fuhux#71 z#9A*Q|L=mPNGrTQt)tu*gIu#nq-0cJuR}0K7 zt=c+=iQyjTi~cvS{V`23m<@(Jiq5@P)myflfW<`u9xS!3HgXtJ&ioa+Da;MZ)8D-xdo?!S9~pQJ+=%WmX6W|;1gu`s56%y`UNBer6=z* zX*FJOW+UP5EaZ(J=d$1~<58bZKT4#rEr~r1ulBIeveOs`+x%>M5NbD52I01TSwK_y zDJ8yKAy|1;>Vjki@-_k$iG43X`fq&GwJTy%{l&&{;-8V~{Hy-eFD9(Sj#7T`GF?+G*{(X?TPL zUqWp1Ar%Qt$h;kRj4LxH;r!O@F+yuLp)qe;(QA%j9MH{W<{6<2!K;8x1}S2>l{nnB zY?z#dAHgq!W)plRdOQ%HfR5k1WJ+TjQ4@PjmQL(MLcAp%juN6~)h-&^d6B4oUeQM% zBp{nu$>qVmXeV*1F|zFS@ME;>2=1MUX+cO)k$6TjBDOUBj=O_&A;BSJ{?Yr)Sixt? zc99EGR!iKC8WM#3cch@5E(GNR&lPR-(B(!=&NnD&H7Rh{5{S^GE_58j!)fejdJqE- zU$;G%5MU-SXhg9|G_!xzu>GHckaonOEx?p zKl7v(dg(s4JpPR_r=Amiwz|HR#44!&xJ73x&4*g`#4U4Bt?lnlqJ|?h+rytJ9xB;= z5e-{4LlJOR@fH&xp8WfHG+p&Qm%-hV=XL~7LVgK{i9Qb$IIWSOGkuC>&}?&O>T|7D zt;yzkY88P(Fokg!h<&4oF7WBPJb1>jd9RU%pT=h?gnWXK^s?1R1pxwcr=iZxLsmrm znlop;7JqIb^U_+elhk3R^JA%SueHh`f&_E$$!kI0mg0k;^@8{=abs6z-%F0KWFg-K zj}18tnR%xgUddeQWE_L*(JSB6iK?X4ciN9A2Aubsl~ z@iR*kdKP+gs{=XSyIzbO^ufH&>w0fw^r)&qiOyYH51ivRUO|a#_B7t@i;o9-b9Cbq zL-Rz$u&PZ9WtdgZ+Ep)`6LLGrn9Yi36}wWazU%CJU(C2$B{#3qH*w|Drok+}5bXUT zE;6rmcYA|bfhY$Mv!+MABbmlJ+NgkxLndwj+0$kiE^f%Rs1f-AYHFxXQ1ac1mFZr~ zz_o^3Te-2TfcO@r>K{`fk3W5Y5*tC^PXTkxo~+@0Lf=@GJlB=>jW>!+8hx+qL;gZR zvGrY>PvA!|z(bHU!sLyf%ff&X`qTK6!T)I=;L{M6d0XzeM!KJP;R8S4fYlW~ytLl| D1wJA$ diff --git a/assets/img/logo_small.png b/assets/img/logo_small.png deleted file mode 100644 index b1537174600dd5932d9eadcc00bc3c67c7e522b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13776 zcma*OWmFtp6D^E;u;A`4!QBRj;O>y%?miIQ-GT>q*Wm6B!7U`XyTjLc-n+j0_tq>J zhG9|mux65eLcMNLWe9OM1G}Z`hK#;a?ynFY^NFC~!Ox z7%1lC5dx?RlQzl7Aypsm`PVOY%=Xq@raE~hb+$Z2Mwnfu((9KteeO<99q&$bXvqJI z+4|pf?c1bi@GG#GKroXk)+4|Dl==MmrSQiOV`ov(p8D|c@MqIFnSX(7kROs)Ts#ys z@?!Wx`o80^!uan~i8Iu?B>Kzm>!+y2-PBb7B2WIm56H-X(&GOY!7TqfBBBB4zsQ#? za3j(i9|<=tDhe2TCnmDi(bg8US*n&mtqJ}YR5yFEm6Zs0r!Dgk59?d+ zcu~>oy>Mu6IR<`@x_d{u8sp)9?4{hMCDu9ecXWrue_V08V#EN=1cEukA)5vr)tF zY}nB6kq`%$M_myT@n6Uu#6;9|WlVnX<3Kv;o=$IwCFXYx$%6ZL0f8$>2)(=8W&3By z$8M7aD|n@Wr}p7t{IB}|xpwvrC+mEn8d@z)@@oj+eRflRRkreKE5iSumG1_O{5Oo3 z?&Re_%L3<23G-qg5V3}a#v3dwYzwi7pKt3)OlnYD8$T^UgH2Rqq*;+wIM~4Fl)K@-Vq9ncTpry5x{02iy zTar}oQ5dq>DkC4IxxNzBO|{*JUnojzQT{>G&66jL_|fcsVnXEq;pubW%D|HDLf$tE^pk;&4H9U z$y2?MkJP9YIk^*L-j>-LNy1OYg!-t>X5;khReral18s!9S)O-=TumI4{|`lZg47?_BbOdF->-H ztxTCO@CvF#*_xZ1V^qu$}4fSTzpv)_m!nY`Wjk?yE-H!RhM~~@Z-WO_AodjIPH}WK zQ5P4GrsiTa@c-$OTKxvAWcRr*is_DWqs{F(jTj29sFwn<6H(dVbnakjz3yuz)s4CI zUwb}2J|%Mt3lT-AnxGUruLSl_1g!t=uKM`kJ=c}-wyF;P&Tilab;1QpMJ}eNu9KRX znFS>#C5cc_oI6x-E9D9kdmYT=j9uJ(i4ZnV zOOhHFkSN-F#~k_%{lT!$f8r1)RvtOqucy3D7WIrQ4iCZ0gbtNtbEg(9K7CwN89^Fn zav*0Dd;2piAEV=mbPgfl)RE|0OSKP}6KCe<=Z#=~TVu8=Dh{5Vok`b*#XNpM&U(9D zywcN!M_Y@WS^4<@52O>g02nr(`&iSuIxTwBtSmv%(OG*`ttIEZQ-i{n%mj?04lRSz zAyg8ga&&JXv+6Y-e7U<_;#e-%XgK*&mCK7!N{4S~DCr!@`JueLoS#V8_u)B-R>{!% z+y)~d>*n$@ftHXU0Ba-=dyGh0dRUFQ3K|IsiAI;g#lm8L{l||VaS_qoREz?>FD??@ zY01g@aVWmn*x1km6pb`L=!X?SnEWqA#4BSd)Ad_^?FRIE)+_2)>V`+w_FS2~m)P#7 zO}TV-4y&#_RY(pThsqV&k5UQg_`Ui0RCzp*42+D>Xe5HIDV2L91k8gRIXOAcvf3n4 z675k@$bv$@Xc)VN=-~qpVku4VWpPw$LhV(a{G=o#2B@BR^zM8M(7g$aIaQeT&Zrxa&M5cSz+A zmfzoYqV`vgesNgtwd-zd)_qN2;%Om7Mpfi{&&f3+$>hIva({oFt_43!F=&d&qTpaA z_VxAuIcR_mt$_#XBzfVxe`trzSiMOeNW|H^-|%k)7^hTjET|_tkSn zs+@}b7EEq(*IAwG21P|h<}uLp}YLiV1B|H0-W=3xI z)%7(gRmH2jsXHY#SB1;|6ss(7z)Qr}fd~k0hfQ7gzu~9rMURgrCkfJbbyo8vU;0DA zw=L89`ihKv%ahl&9w*-i>Kl9r+DHP=sR)RHTrST{d{2(~p2(DIp>JRicW6hNupqJH zKjZ%%-?>5Mh(%|~Tfdl3Q4?WHd%5WWq#0n(6Np`Nj9adKU*jCk9Y!W$R29C?)WuJF zpvCmIvVy<6neS%AX{ri%+P&WQFN=Q;My%0!@rxtz`S9^+H0fv!dT@}OiME80{)05O zQb6zr*X9==Ad%Q>lK=S!IoelS9Ns%ms&J6udzYK(j)|z&K8bjX2Zx4snSV&n>_;_a zAxAWTE-znSmS+Xq;Icb}D~W(pygZ34z7G49K6S>&7lhV0?L`ybgJG^Z?^e@n&o^zR zyZ86g`m5XR>Dy16wbGN4T(<}yoaIYON~UoY)oDw14h{|~goK2?nmKTNG;jVbY%4EN zFJ;QYiS_dG;+NVWo=l_BYME6!ukjb#)z`+#t zN6u3uw#)KFdN%_J65__l*w_Dwv23SWGABz^z zfOtG@`s-Tr#&*53Ye+J!QF6y`i|GRt2>Vb_RAVj^u$vc;7;q#Z(qzQa{ryR_%vN+hxaG zXPS<>ge<}MQvgzYAbFv@zrH(-Ik#*7Y0z4dma)3@Wx!8$j;z_~kC5%nLEP@mTvau)wfj4kPwUlT!-%m^dCUSEOGeYCOZwMkmCMB+WJ<}O zn3$MsQEm2Sr%P|70?o(|>%O-(!Wb#=Y@raCD_ zS`w^vsT{>ir_XqS(&q?cKD(8tJ&r)&>f|))=H{m0XF(!z+rnYB8JK$qXh={!x_#3t~CycRQ?f3d*5b!l$w=2%~jgqqny=n;3Mq$?m9w9G+i{Z=6f=1qB7tvv_BiZGlHe>sE$FanHRf8boY`!dHb` zsq+<3`|m0@a3$u^R9qB;eMPmk7kdCSx^(rQrHhEtM$b?yQVzbT3@=f>-uv$5-nMPX zX1?GdR_w$S%KjN?=loW#4vYZomq?-eb?-!&JOu`Y`?F3dr}pc+;MFEZ3}*hQi%a_L zPf8Xcg$|v!T<)*8Q7fZKUAA{8F}OEk_VymBiE~O3^=Y+*ZY@Q!f?4NOPV$AYmueiH z4m?@nQo6eG7JCWCFw+5*9*xQZnKFDY7r*;OZqBzvq$;+he=avRHo`tWK6-$@JD!b3 z1w$HQ49Agn$HKgRrm`W=yLC}ko+-)iniw1NCh!`81r@h5KOv8*iXJfFc&9KAhsxZt zMaPA_yf-cI5XKLF=+m0!5|!f=iMtB?V`;1ZXJ_YatwbsJU5%2+Mny$s_PHkjS|FlT zkWbF&J9D*|gc}=utd_FKYcVKGu@FUA^xCd%Oczf|?%?mj!h%OoP!O)i*}7LXj${ML z!d=pbPEVP%%;e;oLtvSLiIf3g3;EOWal#j0J%uhN_;=RBOWrNledTz%I~Bl^i(h;e zlU*;6j`JHiw(v&#I*&UYl{g7tE1&4(V+971Y$sQ=>3HAP&l^KG;YD1WxmfWj&D1xo zt&A&Ke8*E37RPhD=m~nyQZ)vC_UawlhK9U65UBXO`UZ?l<~e^L?XUe_{a*DJyu(m+ zPMVga>btdZY2Q*o{}s6MAAZ66fw zKBF^G@zPvy`eF*sd*7WrekWb z0>c8cnL|Fx;zfvmWh9;>aw4OZLCX4JL;f_&&F{0bqR>?d6}+sHVXtd%pc1`#v5WAm z3fsQFVfbpuk{HeN%EFf;Of*fN`i)|y`vRcWs4?*;T*#i>2UFp|>Yrk)=`O+Z+sn(# z-7+Jt4#o<>iA{gLp8JW*8GdwHn*^2kwOwuZ2I)m>mq?(bk!?6iNe#K^`iu9yf{`P` zKO&W%tTfO@JiRJ;g0?AIG;%RVIpr|uRiJ2F>+a9Dy60s!JmTq|f}h0aF)S$Ff}kE} zYXY#}5spOg=xd@M&^EJv_^cx_l1$4`z2VSb$scZcxH^|mPF5_*uz8mEE{7!awa7E3 zXX0Zm&FQ}un5+k}fu@0mzQLlVq}{6IC1!`3wOcU5%Lg}+&n=p14jgKsASw#ielyoJ zRv{kt*`Ql;OpA>X4=q}}xSd{YXPLEoeKMx_{c0RHc+r14XO4eKiv9KRDP9dxC)sK8 z&@jO$v+=#C_QdvAaZ$J>0zv&Z&E6OrN}KUGnDGv{*Jy2wc6cG6=Vh+oX@CRV?9hJbuugambwE@r-kIDX2_6pEP8}0r5`x%;k5gzjh zHYR(+Mu7|`h-rHx>+KAVdY-@xz1qS0t59-hCzML zP3*aGQH1-RWN&|e%i)G#q(Mw>{}WbD{{CjPaAO~OWIo?b6JMB($4^R^)%BmpQ@Gkz zYj;>amxE4n#hk{ESGc3)ohb$$8<>mj_uS2{hnX2urq7SN3_#fxiW+?)ov^l9t0i>s z&D8ux>T|z=mNI6MnyctZlN=IE#W;I0gg+eDdAn#K_hZd%2|PeS>G4S$pO8nWc#c)<%Ot`hIJ0cr0~(zu}eD=!s{Y@wL=g*&O z1nzgc24`!YEbD*O>)R-4qS`C^} zQ&q)TI*)NqZ&S!jl0fS2Q;6_|mncl5-1~NKpz4`ZbZ&old;9up&aJ<%&#;hhy`{DF z;oVg`h#craTmG1N-!Z5f-akD)zAkNT`4?&OaK@~;)Do2mwGXhF5a$2edJ{|J%~m6r zZ@S1BHriSS;k;=QQc|1wcS5$f=~zm%4O?Nxb~LoK{+7v>b=@(%NF3+VyfX7c2CHz> zM?hCCYr6Rhj-;%jp`n4a__j%#G>>w0tfHn`J+O1?aoBogBK@OIjp2S{1_0}8H7as6 z^v}?QAE;cM_{2?l1}XCT`}|*TD^~p9IHaAN{;WSt@^p{p_<1Ai9Bs{VN!Q_X_%&!3 zO)hC=5wI~>SIjjxiQcux3F}&3`j9>QoOx-gHktp%VW>qx zxMadpQt5KUDmq+kI{w%ZZX&TkNyo&>dhU0k<6N20?G-mN=G!hFhH6CKhvW9!OAZYY0p|JlzscP6a*$$b2%&73asceDnd>k2A zPft&x@Q8>*#l?4xWu?7IB`ksbXYU=5z?g%<^WEtZ=gqpYK=59ka~r(RTYiDZ&!0cz zN>jYtoSgVV`(zRB1}-J(zAtlxQBYp^lB>S(H=+1&y6?&`Nsb+ex}f32{1L3mfy#&~ zyQn0S%QBMN}UT>ESJ;ZF6UjQX8-q*Yk!fK3r_bHv#m@`H3*;U z$>|ij>yn%{Jw%XrN)x5@l;nxfMUNVjNH@pxZki9amQZYvhI-cVi_bYYHT+$S+xG$K zZas=Dp&Psh$#A;5SiQl`T(E3BMo}Hau zM(Z1thqs5L!RuZN)WR_7d7eTkx;_` zCqm&J#3(pE3s@O44@Rp}0{ zrzTELPW|EAeudBeGF6SN7;4lMx0jgOJz~JRA$&QNbGvZKN*+fJCg{|AL>)MLuTrnq zF!YQKpPT-$lF=QtJl=F1r^}%;VLU0ifJ(R)NmI+)HV$s#g^n>ZDnzcKMqzCDKT6*& zbZ9nCUFYZLOU;3a+22vjTrhqBjBu^$I5&E;ed$&Xj~3H5SVlns9E7WGqdKrU|KwLH zygkL9M@xl5gY0b&ZRM005;mMsy*Fj(`5QhYoc8ng>8G1VdBlw8tMUDmY1Xa>TK9Pc zhgpqmY*3h4x2Y_tD&ZQ#;|y!JFY~`dxf~~=Sd{L3W~khJ(-ZKUN%)PuDChBN*I*@# zv`gHs)$tsy8h|L-48nEdkxkv;=MU~_FU$6=e`0?omNx~^6vT}vNJSN`U=@DSF zS+4t&+31-YWX$!ku|zihWd%TpB1wsfsk=btXG~1U=jh5mMIbVmb>5;o64QK1YFsuV z6T=hlJE78VmwFOSet_MzbZb_lB=Z(uV?1s-3!?8fr9t3%m~UemurNp zhj5F5qVnej9flb5&cv-HMNgeXn@B18lW;6Ng=mN)vd_)I<+p?{ir-5b{n!^`?Gjam zBBb{|ByW&fz6xb$Wp!wu>fUrVEqFA**zhI|y>r(Ope2F^VopcK$1_1TM8k^5M9dH3 z8_R*PsstjMT60YP@lU?ibH6*3%3#J8Ae9vUYBSQqduK47)M-qC+ z&iP$=i{t%`jYHXIU0nK88g|HzSqyeoTDG2QLy+Ma?T_VI4Wv__ac{D#Zw5dgQesFt zlXb@A1;I~2RIndU8E$L3{V;rPX3G+b-K?JN8CaUl6JU{osmme420m%m?wQ)7K280} zf7!IgpOg80x#yM%kaieC(-0oi2)%UadSq}0VwM!UA**bzf(bI`QaOY0T67pA90WsJ zjoDh_!OCAub)TP~{RxSQg?wjzh#8EqXUkE&gF6}5zRgAT)O`rIZQgBFX(y3dr5wDm z-+bomeq*F?;edTG@J+kNcjhmB)r=A3GtF6Rao8HKDw{X}IN@SvR)I#_Q5ZNl7D)OE z-(9AJ%j?wDyz>q@q$B45T$;+pM%P+1WMZGSj;jK)6D47t8OK*TNSAB@QgWB#uu%)A z(9ZX_pUK&`(%s5;H+92zpFHl__l~?1!}-&gR=rF^x(7g@#P4C!^s?PSQ~?Yr5CRa` z6WfT}kY~?zUqxzp*_SK)|HPQc#oP@H$lEepW@jrk0Hk((0;Nb2R#oPVfd=Q_u|3B) zfMyl2!7Ez2Swp+DiWJcgU(<^7r5PpbrbIs>GQWp+uM>`ICIL?{$e1m+g zgRfKz2s|d7o*k-G*w|mf$X2-bqpx97 zN<*0%shOYOo@PVP7X=^hoOx@ftmExo6Qp!X^6{S}{F15n_a}K_Ib>EEUWO(TvYjz`TYj3A5*Z?>k;h+iAAb7O9IG}}! zIlHAbCl7^?Bic9zQ7%bls7Lf)lSdh?CpZ=Ru6j?8u$wK~Qt+IJwstAb})KMy27`)bGhx1Z1B7uE{BnmT`Mp!+QUoVTlNFU zxGnbO0IZVJf?XipZs<=GfOXSR>@fdTGKC@f;aqs;6;ypb)IlWR{f$fp$m+AWktC31NL zVSRml5zTl4L>BXFs0mV}Ir~FE_mi~QEI~^tV|8F-LR>~eT;Q`}UcuS!TBXlrqa!Ww zn2$&B#AGlnWI_lJ>%ubg<><(2{t;HJu?HByJCnlF$&-Yi4mF?c+<%i2C ztO_D~ZFMfa`o9V$*gQsCg6_|!ta@C!o125SQ(1(|i&ZVP*o=F%(I~f^kqHU@b)!kS z3DR@6)_KY7y*YT5HN{;bL=_ww>>Nw2Xoq%DI5$p^y}!!ijqHE~76!D7(@t0r?_LWrT8%(XUg*OiNL| z5D&QNe?45z-?^S@h( zIC*={rAds_bk3m6B*>pKaq@q;B$S&?G#h7X7jZvnHT1F?1m908Lmk)#kc(?JFjhZE z@iBO8_aJzGDA4$5s%wxgXU^+Of>EcZT5A`8pkL^@3+okMd$zoAZ!v~=TDl~ME@)FE z>ZL-x6-z8IC9Nx;!l0>0^;i10hC>Xw25o1v328c0YE?6tXF^C`8lHs8t2JbzHTzQP zpATaA7#M6n%l3(7h`+|xXAv=RB~VOBzP z%;d6x%H+1g)%m@#yyoCPk(da#QdP+SHCx@~9ip%nd`y&s&_P9jUD`59d81aMcx!v) z^HC$6U3GdzVhHIkhMCJ`Js1Pjq61mDy}cb5A3dz8u~i96?~Lpf92@ST>K?R^IO&*N%04A{ zxsbs->}r6U6JmA4OR@uB+td?rHB67{(6;|2p{SzS6rfMhmaS11oOd&uVWzbUM)J>N zCU_FU(52$xa|3SMuw z7jkrS+f77a7v;L`jJUh>Qt3}Mmh0IW`57u%v^qJ-3(%4IOtcmG6|c&7sOxUJSpYKwCj|S(VyiEE`6Tz3O)259Srdlvna2gO1*62nWRDv67BdxFUs+MN<-#pxvaACI2%!+v3ok< z{QC7P;OQt2jpPLykj~heJuiLsnPbex_vWL^O!S)9`zl8w3lZ7aj&-JUe7;Lmh_$<) z-W6!_gv~q~`!9yek7<&T?@^$C;euPe!a}ehje+`&l_*a(NXg*(&) zy9-pT9YqU|LxtoyBKluu+zHv}?BeL?e5}$^9^*ldBSq-XVAaa2^vQY2{{F%1&!yCZ zm^Vg*Q`foNo_cOKl|T2)0x{Hf3ft*nI(y}eIy~6~JQfB5n02l+3ns4e#I@v+Yz?-H zTq=#^5g?9c^7?rctO}7ET{bXQBz*Ir0!lqt8&F5-F07Z~5}qNjg6No;iN$IdCC0d} z4EJ|;H|Bq2m67~*Uw>I|h6HJ?S1rVbacP#w_KskH7u!9~FC|RPJ_HL<#EjpxmIEmO z0@QR82$j!>F(Eq%W?5yZ+e;1^5uhHkw|M$2b5LDf&DTr=Ht#uMloUu4`mul>97#G7{B_L$R%r93p)J5c5ef==`jzM$~TQ1S-8T_?Gs0 z{Z%&3qc+s#P%w}gde6nv&X)E~mJT_kM3tKeHY%d2B-D|syn}1kQf49r)5C;3`r?g} z%IDOo>ABn@)^u&Soh%+X7{&@AVum6;NyZ(`9QplQ{@tX8gJT^(ck&HGIy#4ca^e!q8;=O%&8DTV%Gb{HJ#tvjMa8Lk1veuqvRLHGQ$_DJkURUVMI$=+)akvqw-=ePEt~L; z5y}$fM!GKC*R{Ey`a6)r+~$3hNJvP0V1w;7CgI}MO-z2D-qiT3O$e#UDof{Rfogc9 z)emIOTUCs%^DBsBNk#l(Hupbjur(xuaE5Ostipd`YH&4$R8v1_pjo1-U!RT6(o_}v z80$w9$t%`bf!PA{WMkvH?Q?e)0z$pOs(+VjZ+XL;c~B~I zxw*TVbe}Q!w~sSx{#v%sDl>wovfK*&p4lTbp|y$g*AJF!02SSE%+@my3dbFw!v!%H z>wsJ`HZnEkr+V>=-*5+|)#Y{plGes1eAUYiv(i0tIuNt64Z7mBNjbT(_FL?`_yZ~y z7JF!Vr{7R!RUa@`3nwNf_*^@CoADX|K~3d$kee52!iZaaz%?c_H+M^F^SP*~sGn{4 zwsBBk1qyICy%Yz`jKjLDlHg-1Ar5uQGP@d1U{wFMoGS+IW^2 zbClpJBQ&m&beemDS0H`PawANb3RM%5CYm@nj)fYIkzUwC6lE=KLN(Z@>E_Ni7Iakk+DOJWv8T$5i zxKMw3XHelsAThmEyEnjCL9lP6>&4wf^OO`8GX1dVAVWoBpLT2=WP!Ff+B(+kq-|Kk<}}wEKJOj zz8RpXQU;`J*RUmhi>1h8j}CW_E~~B|ef%i zT0FQH41SD`j!yd(-iMh-X(bJ8wm|PVs3{1_ZXQE~WlFOh0W9M@VHvVsHMwb3>H8Jz zB9K^U{(i~UtEs84gq6i%h8X`Ey_73|j8^oc#c(WfW(3iyr#rzhzAh7%H6yB|rDs!P z-7Y#IVS7@zC7;X(HvDC@;kUNb^@nBgnCR$^m6ws6#fI$?#q7wT9KpG>Io`oJ@LPme z>^%H}J9V?9rSO&nn5_qti;eypZ0Tx8ew^h+%|R(^f_@tl9m2`Fg{gg%01u$Cj~JIX zpv`y&PpJ4CnaPi^l90p9P3u7;yY^_v*=hAnKXnffwz(7!xVTv4@P7fuN*;XZAF1B@ z>&iI9osx4V$D)>&2WyTAlrvH{#0X+ZqF&>T2a1Fv>o63&GNx~z5OscuQfgb6Qcv%r zJNeYhG*di&Xu4#a(xTL$O*q_O_%-V}aLPy)Bz)3$8gRXcO_3)#JHyadTiGR10;x&| z)0n)PQAO-X4w zs1UD;a#zkKMFyT=+RqdJ#!!gGnac+9yb=0|V61QxGpCVcGh|LiF8rcWB-y5elfe_# z#KeU4K!pJ)W4PH}vxMK4@NoF83GvRqwQL!#7MdEyaoLchaV~p*b162(*q}gi#r$k< zU>e&{-RXUs@yWR7n_ub(5dsp@Q%{yC!5FS-qdZWA5&@o@K}hJ;Ovtp_JTGbrO%J-h zdJhS+9gha?8Y@{B9Uc9GG-aDc0WEr15gBL zX5IW7?=#4D_tlPAE3SOQD~&QLOyCwR-GKJ}yngoQdBaA#M{Yi?jM}bkt5C;)8x)9j zekE3c*_9+uTnhgfLVK{Ze37Wu=N?ZLzLOfenS|_8CRZ3NMOY?aA25Q3p0LFZkZzXs zs@>)U_mt$KBcKfP(_5?Fh^b%~MB*Z{rmXQ_NZxxXrlTy4iFk9og)=KXc)2~z5u00i zGcsu`2CcLt;ihLi0{oErcw_cpEA!4NnecApvujitYZClN|UiI5=vhC!kw~!B$8V z{iBXd8ZqE(%)GjCu9&)idU&Xm23yVY>Bg8oaBU1rma^IZz2{Ap?(+ic>#sZLGP{lh zYdIe_M*!cKf z0vr8oy`4qqqwC)Lgd2Ne%dC%rpqgKL zMM|Ovp~ERp8|ek6r<_s;w2I5L-FH0!gewX>%*N1gwwTeTT*$~#YKD~3EF~+;se?nP z)W3XLlPujt&6p@|$*Jf{NC7v6j?jLSN>&ie6fOWUm9jGkA)m$@Jx+Sw>)42wU_2>w#wCRc#f@p=D4G^s#`OSQ8v1Y z&H9B$PVD(5y!MPNWEcKApHy(1$P~3m=F~@`a%=XU1_+i4CDZ~jORf3yE(+&##<34I zA*RN5p~1l~nL|gy^9g+7_*zOoCUl$iQO@m;HknU}xgr#8{ zfpY1CnZmjaHIbH*l4t)40pn7RerA%Pt(k}ihEvt(edbd*+Ccby zr5&O4!j~^!^3my}YkbgWSv-e$DYB<$w-yJSjP1oEN^@Dsv{ZM7qA*Eypb4U4mhV7c z3cO*nv3xe1^bl65iuM~^##i#{zYfQqrwf`(by8L^GB7+Xjia@dl(6Lz%Wy1i>H7KM z0{+;D$PnB0mvZ9*De#bes|mU6s42~C={M2MF7WdsxLcmJb77i_E_WnMJL#4JjP6~K?2;#*4ldbM|hz$?$1iW?TInDTm4kl4|Q*zfZ8&_6u zXcRGMRm$`L0Szv)Jf3=JK35shm(&sdsv;3w;^&w=aEH#`d!LZ$~>cmpp zB}XPH${JkCJv3E>GZlLXVP75V3sQ7J5dDhU9;%jz)l|LeX=d> ze7&7^h@J^J@A3%*-^{MH`Qs8~^bAEmhL60csba{d19Kz(=#G9(!g#$5ilBrp^7|1ZP&k*viJ zT-Mp}f2V^Qsd)MLG+#7zU1VyNEhz~ZpNw`%r_yx92JDDq@qMM$T;x-1D-?5$p8QV diff --git a/assets/img/loots/health/potion.png b/assets/img/loots/health/potion.png deleted file mode 100644 index 59a523635740413813e0662c94e6ec5983bdf13c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 517 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnF3?v&v(vO2U$sR$z3=CDO3=9p;3=BX21L>Cx z45bDP46hOx7_4S6Fo@?*ia+WGRI47~6XN>+|NnLC)-6qUy}WkD&Z=-A_weDvUs^if z>6)$P6Fn{<^-x%@iCa)oArGjPfw3gWFPOpM*^M+1C&}C0g`tC0)&t1lEbxddW?lFz(Kw%+AQ9CN=;Tu%5Q4a>! z14kr79YSupin=x!nwSV&4M`LiVdZIBl-b0jVRgjXW#ZS4b1z~ByrQS_}Ll1 z7NKJ+Bt>OqH&0+~;7VJ>aVBbOmguYlSs{&<>F4G+3eKo~c1A9xafapc^K<4kD=}PY zQaPYt_593BV-LO51QA|dUQw5ix&jM+$ulx!KQWvxA{o#Kbf9X9YeY#(Vo9o1a#1Rf zVlXl=GS)S)&^0g%F)*+)w6HQY)dn&Q43>v7s-S4d%}>cptHiD08plphAZoyED9OxC hEiOsSEkM&_1hmHxVo9~`lxUzH22WQ%mvv4FO#lI+p|1b{ diff --git a/assets/img/loots/money/bag.png b/assets/img/loots/money/bag.png deleted file mode 100644 index ca6691ac7a34e164faf61627919a5e2efd2578a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 519 zcmV+i0{H!jP)~Z!+($?Cm$oM7Mo`X=_kqolT!2<^5$G5%Ml<4;M0%S0Sl*P{6Di~ z1_Q_d5CA!Xl+YkXKTZe0f=i^2QVJh0^#pZ;RlaFuYB5UU@n17N|1>}!xi z9_?5Jk7j&nf)xE24nWq-5DoD)!-oQfyuiZ@CNUWduikuPD6LzBH{@VZ#t@nW(f@4? zPW>nbVOYL2iGjhJkAZn+x}xVXyluC{s~@`qK(7C`25h--ra71fCB8{V8HjTLNI#nm z*mB|GZm{oh>4!T2#r2?Ihk|`Ihv95|4#3dQ6#~|`uP7g{eiR2h`arl~#uWu12auzm zU`a^WNtCG=b--u{5DWoA^*Lopkb!U`gHU}=QwI=JpVQ0%#Mb9DaR4dxIn^D2t4<)c zJwU31a978m3Tf*n23VtlnDzjvmV?yeZx4hffvZv&Ak-e9o&%6tLBf^C!Sy;Z?E#7$ zfYF>{vw_s-ImSe_2gtD;B?`!`&&jnIrjKAzOsNHg^p83K0HPZrFz{&UC;$Ke07*qo JM6N<$f&kPY-nak& diff --git a/assets/img/loots/money/coin.png b/assets/img/loots/money/coin.png deleted file mode 100644 index 55bfa9d90fbdfbc6aa7def67353ab03802aa9aad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 411 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!73?$#)eFPFvNcITwWnidMV_;}#VPNF8vOtNzp#IR^&=4?9DHg)~qo)YHW=gyXtz-$mXA z0}hw|GRE7yHewk(%o%V08As}L-ecdx{jlhO|0dQewmMsNR*OHM$n`d-;hx4_wI{(5 zvJFR+4~R`kIcw9tFD>9(_(wy5cUOOC*+kWH>{#&U^Jbt0swJ)wB`Jv|saDBFsX&Us z$iT>0*T4dZLJSP73=ORejkJLbAku$hh@v4kKP5A*61N8LqL5yo1`W6kC7HRY#U+Wk d1?YN=tPIQ{mSi4!m;uzo;OXk;vd$@?2>?DNe$4;? diff --git a/assets/img/loots/money/pile.png b/assets/img/loots/money/pile.png deleted file mode 100644 index 10522f1e51d0bb5388cd4ac60162dc826600ea07..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 609 zcmV-n0-pVeP)Y5Pe3lP%H$Cs|1UHja3j*XbLUWLP#MAR;CaUqFCtt068uE2#6Snm0Yk02!1qz zDS}8aSlI{`3O3hSDIyqU_MCGscZo+5h{bJ_CEVM2^XBc&w4ueTt;O--6R4?xuIpb^ zqIf)Bmr`Evt_NQp1-^{WLxjU&7>0pDp#W7?J(G~}lt8gqteS-F(49$$jFY)r;NuGG zyKlmys|%e-?sU8mkO)^1*b)2 zWCiHgO$AEi{Ny3_fYLsLJH70{3jzdY!@WO` z2xq)>A2H;wwCmiiS-5Pl-4QO94&Og#6_8ea$QdnNHqwiR9p1 zU-`<>Env^cdoGczpy#V)IjPATpMbA`ufYFSKz^K>eEqHCs?Gy9k86lTB2W~iWw0_1i!oapD{+YS#ThC`4`mS%k{~0000l&Db7#Ud^Sy~xeXd4(< z85qo6Vcv$KAvZrIGp!Q0hBsgPt$-Rd;5L+G=B5^xB<2>N=`l92GBki#@_nQ5SD+pS MPgg&ebxsLQ0E9Je!2kdN diff --git a/assets/img/projectile/arrow2.png b/assets/img/projectile/arrow2.png deleted file mode 100644 index 3464adc0adbcd6da003101e4b7e0220e94879c52..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 369 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnF3?v&v(vO2U$sR$z3=CCj3=9n|3=F@3LJcn% z7)lKo7+xhXFj&oCU=S~uvn$XBC?Oi)6XFV_|1&V0J$u$!Q{L9r*1*8v|Ns9_H%0~m zB^XPB{DK)Ap4~_Tagw~?NMQuI$g*S;geI@W6)q`XyIU!U|G))2h^il;u=wsl30>z zm0Xkxq!^40jEr>+%ykV+LyU~9j4Z8;Ewl{`tPBk1t}t&y(U6;;l9^VCTf>{L{Z=4X o!EGqX%uOvWNz5%k(_?I4WoQ7gzopr0AG+}Hvj+t diff --git a/assets/img/projectile/arrow3.png b/assets/img/projectile/arrow3.png deleted file mode 100644 index a69218f48c2b59ef7bcd12248621efa3d95bcd1f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 369 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnF3?v&v(vO2U$sR$z3=CCj3=9n|3=F@3LJcn% z7)lKo7+xhXFj&oCU=S~uvn$XBC?OHx6XFV_|1&V0J$p9ZO50ge-oU`X*4Fm_|Nlba zN$fy5#*!evUYcCo~c*FX+?iKnkC`*U_yF*&ZhkR`i;LaLrFjv*T7 zlM^JKGz2olIxzY+bn=L@WiaU+_E0eCEn>XJqQT5yy__w}aN$&UkcJZ1h?11Vl2ohY zqEsNoU}RuqtZQJdYhW5;WMpMzX=QAoZD3$!U@&)uc^is`-29Zxv`X9>-hA!10&38J o+fb63n_66wm|K9R$JoHi&;Vk|_l?3|fqED`UHx3vIVCg!09)Zke8 zN-&lL`2{mLJiCzw;v{*yyRapu?WhHE*h@TpUD==a<%KNS4HVM%ba4#PIG>y# z@x-A)(AVK2hp)ql2`QpIY#B^v4!buTJ!;zE!pJ5bZsHs%vw(-etA^d&CahN%s8zMZ zHKHUXu_Vl&Db7#Ud^Sy~xeXd4(<85qo6Vcv$KAvZrIGp!Q0 whBsgPt$-Rd;5L+G=B5^xB<2>N=`l92GBki#@_nQ5SD+pSPgg&ebxsLQ0OoyXRsaA1 diff --git a/assets/img/tile.png b/assets/img/tile.png deleted file mode 100644 index 535f851a0034605cbb2d99aaff7e0b5b6641a95f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4484 zcmeHKdsGu=79Ru+BCv`Iw90WD?ACR6GLy^%l3Btli4ZZAS1Yg&7-lA6lsrrZ66ls~ zp3`ya{4 zJnnbz@80{n_kJhe+LYvk0KaGbAP5T3Cu-BceJ1z?`A!4Zv|s*qCAhVfWMm3yv=g@3 ztwz3pgN0%n2Xijo2tlq3=9IXgHrTi2VO6wOCOI(wbR#hsSD*7H`_tyEqlI23UH9YS z*6!_^%$ge=%g~DX*?X5cXtFEN*j*m(PGA0Ad(hPWIgi?Xo7xA7Q>1FO<85r|y*H0g zA%CuH9SrSg`zoyPU`1*`_@|3)p?mt#C1*DOE2L+u@!Yu&YA#&7m(wzEr}^KvVp&P~ z7O$`Xw{;O2(vfxfYHUuxd2GRV`0dv&p17%fFs0{4mQPai_8tiGs^B%66um|>k`72> z%PZCD#J1S^9XaWHk-3s}RZ8~#9=#LBqsR7 zKM#7sf}C4OROiEs_1jk*d)L2jZF}l_^Md=S{^RfMa9nym zaQ7b?XV%Z&Fi(iPlCZN=+PYdw{i5&q#%leES!>?t$eY(`KEAASS(IV^OBru{Tk%n+ z-0v!Lr)0Xj=hiGrtpXQ zlBOLM8(uwRzI^4A9mifid_54q)0k9wEwQvNejC(M|5@bqS0De%Z94N!)BX*oC9vi< zXfGsLb2zj5FK44J_4Z!+XRE1ha1gp8v!71(=065~SHy$<$}}ve7^_)Iv(`LL>N49v z<3UhVjLSwdt2hD9<4n9ojodoWioiUpMph^cxWT63^7+IPJC|OPoWYc=VpJ>=6YUq} zq5y!I6KL3FF0eQ#mm2ZlQs7w>V+ia~5mu>@OhXE+vD!J9l#)^$)w%d0ITGy$N7-2; zm8M-f3<2KMNWLK0C=7Etol++uwc1UXOr=s`xEzzqQJ{f3iY)@|LM@JP5n>2K%Q+Z3 zZxeW{1r{;sJZqt#Mi8(LkHlxT84RQF7RN9PfDgAt#?H}#)t+Is7N`+1C|I-{T{eJ$VrhZa(gFuSaXCTZ5tLk(f#Z}y2A+S8%PD-E zzLn*T#s5oN93MDpsOX8j1NblYh=yh=om)NhHuPG+dxi;yJ!3)9%uosrx`<;vaRRKN zB_^M?m^iS13^(kEod1Dl!08A=CO0ZkqmovjBn$e0RuU}A$Yr2_MtKCOR82&8SdD^{ zwsUbNz$4%aG^mFwc=2$lel}6xna_!J0FHLVF;X3??9su;Dlc(Z$lCDX*o{E8|Ql9LtNxGhjfu~ZQ z?5_VCU4CPSDb51^0Xe}@sd{>OBRFPF%S%eoLQll6`{18yfMmKYG0Oo#GiHlFFQ{%y zC=f~ny+J4Wa@w3=V!=J5_g!$9#`M~_3}xqAn`B9Cv7wdqn=CzLfpg{Bw3$^x*TM2R zA(GYIyOaL*`_Ddjaya#?T{c^x?d-P1xKHUd@l(EbXH}I1_Mbf9+W#O~^{x8UE1&m; zxw^vwI48O3P@UV)=iH@Q@4h>sUU=9GQ+6Hp*-*6V*D=4#>X4@P7BVm1c~bx2rYp$j zi<`9kMny;OnKx^81r2Y@oS-}Gh)c)FDNsqh Héritage)** : Utiliser des Components, pas d'héritage de GameObject +- **deltaTime** : Tous les mouvements doivent utiliser Time.deltaTime +- **Dirty flags** : Utiliser pour optimiser (Transform, Camera) +- **SpriteBatch** : Toujours utiliser pour le rendu (pas de draw direct) + +## Prochaines Étapes Possibles + +### Phase 8 : Physique et Collisions ✅ COMPLÉTÉ +- [x] Collider2D (classe de base abstraite avec triggers et événements) +- [x] BoxCollider2D (collider rectangulaire AABB) +- [x] CircleCollider2D (collider circulaire) +- [x] PhysicsSystem (détection et résolution de collisions) +- [x] Rigidbody2D (physique de mouvement avec vélocité, forces, gravité) +- [x] Trigger events (OnTriggerEnter, OnTriggerExit, OnCollisionEnter, OnCollisionExit) + +### Phase 9 : Asset Management ✅ COMPLÉTÉ +- [x] AssetLoader (chargement de textures, JSON) +- [x] Système de cache des assets +- [x] Préchargement avec progression (callback onProgress) +- [x] SpriteSheet (spritesheets avec grille uniforme) +- [x] TextureAtlas (atlas de textures avec JSON) + +### Phase 10 : UI System ✅ COMPLÉTÉ +- [x] UICanvas (système d'interface) +- [x] UIButton, UIText, UIImage +- [x] Layout system (anchors, constraints) +- [x] Events UI (clics, hover) + +### Phase 11 : Gameplay Avancé ✅ COMPLÉTÉ +- [x] Contrôle twin-stick (mouvement ZQSD + souris pour viser) +- [x] Système de direction 8-way basé sur curseur +- [x] Animations multi-directionnelles (walk, roll, slash) +- [x] Système de tir de projectiles avec cooldown +- [x] Attaque au corps-à-corps (épée) +- [x] Système de dash/roll pour esquiver +- [x] Gestion des états du joueur + +### Phase 12 : Systèmes de Combat +- [ ] Système de vie (HealthComponent) +- [ ] Système de dégâts (DamageComponent) +- [ ] Barre de vie (UI) +- [ ] Système d'ennemis avec IA basique +- [ ] Drop d'objets au kill +- [ ] Score et système de vagues + +### Phase 13 : Effets Visuels +- [ ] ParticleSystem (effets de particules) +- [ ] Screen shake (tremblement caméra) +- [ ] Flash/hit effects +- [ ] Trails (traînées visuelles) + +### Phase 14 : Systèmes Avancés +- [ ] Tilemap (rendu de tiles optimisé) +- [ ] Pathfinding (A* ou équivalent) +- [ ] StateMachine (machine à états pour IA) +- [ ] Audio System (musique, effets sonores) + +## Format pour les Nouvelles Fonctionnalités + +Quand tu implémentes une nouvelle fonctionnalité : + +1. **Crée les fichiers nécessaires** dans les bons dossiers +2. **Respecte l'architecture existante** (ECS, Components, etc.) +3. **Intègre dans Engine/Scene** si nécessaire +4. **Teste la compilation** : `npm run build` +5. **Ajoute un exemple dans index.ts** si applicable +6. **Met à jour DEV_LOG.md** avec la date et description + +## Exemple de Prompt pour Nouvelle Fonctionnalité + +``` +Je veux implémenter [Nom de la fonctionnalité] selon CORE.MD. + +État actuel : +[Lister ce qui existe déjà] + +À faire : +- [ ] [Détailler chaque étape] + +Contraintes : +- TypeScript strict mode +- ECS (Components, pas héritage) +- deltaTime pour les mouvements +- SpriteBatch pour le rendu +- Dirty flags pour optimiser +``` + +## Notes Importantes + +- **Pas d'héritage de GameObject** : Tout doit être des Components +- **Cycle de vie** : Utiliser awake(), start(), update(), onDestroy() +- **Coordonnées** : Système Y-up, caméra à (0,0) par défaut +- **Rendu** : Scene.getAllRenderables() collecte automatiquement les SpriteRenderer +- **Input** : Accès via `engine.input` dans les Components + +## Fichiers Clés + +- `src/core/Engine.ts` - Point d'entrée principal +- `src/core/Scene.ts` - Gestion des GameObjects +- `src/entities/GameObject.ts` - Entité de base +- `src/entities/Component.ts` - Base pour tous les composants +- `src/rendering/WebGLRenderer.ts` - Gestion du rendu +- `src/index.ts` - Exemple de jeu de test + +--- + +**Dernière mise à jour** : Phase 11 complétée - Gameplay avancé avec contrôle twin-stick, dash/roll, et animations 8 directions + +## 📋 Prompt Simple pour IA + +``` +Jeu 2D top-down avec moteur custom TypeScript/WebGL (architecture ECS). + +État actuel : +- Moteur complet : ECS, WebGL, Physique, Animations, UI +- Joueur fonctionnel : twin-stick control, dash, attaque épée, tir projectiles, animations 8 directions +- GameScene avec environnement de test + +Prochaines étapes : +- Phase 12 : Système de combat (vie, dégâts, ennemis avec IA) +- Phase 13 : Effets visuels (particules, screen shake) +- Phase 14 : Systèmes avancés (tilemap, pathfinding, audio) + +Voir docs/CORE.MD pour l'architecture et docs/CONTINUE_PROMPT.md pour les détails. +``` + diff --git a/docs/CORE.MD b/docs/CORE.MD new file mode 100644 index 0000000..fae3428 --- /dev/null +++ b/docs/CORE.MD @@ -0,0 +1,3296 @@ +Documentation complète : Architecture d'un moteur de jeu 2D avec WebGL en TypeScript + +Table des matières + +Introduction et principes généraux +Architecture globale +Core du moteur +Système de rendu WebGL +Système d'entités et composants +Systèmes auxiliaires +Mathématiques +Optimisations +Structure de fichiers +Flow d'exécution +Extensions possibles + + +1. Introduction et principes généraux +Qu'est-ce qu'un moteur de jeu 2D ? +Un moteur de jeu est un ensemble de systèmes qui orchestrent : + +Le rendu : Afficher les images à l'écran +La logique : Mettre à jour les positions, les comportements +Les entrées : Gérer clavier, souris, tactile +La physique : Collisions, gravité +L'audio : Sons et musique +Les assets : Charger et gérer les ressources + +Pourquoi WebGL pour la 2D ? +Canvas 2D utilise le CPU, WebGL utilise le GPU (carte graphique). +Comparaison : + +Canvas 2D : ~1000 sprites maximum à 60 FPS +WebGL : ~10000+ sprites à 60 FPS + +Le GPU a des milliers de petits processeurs qui travaillent en parallèle, idéal pour dessiner beaucoup d'objets. +Principes clés pour la performance + +Batch Rendering : Dessiner plusieurs objets en un seul appel +Object Pooling : Réutiliser les objets au lieu de les créer/détruire +Spatial Partitioning : Ne tester les collisions qu'entre objets proches +Dirty Flags : Ne recalculer que ce qui change +Canvas Layering : Séparer éléments statiques et dynamiques (si multi-canvas) + +Principes pour l'évolutivité + +ECS (Entity Component System) : Composition plutôt qu'héritage +Event Bus : Communication découplée entre systèmes +Interfaces : Abstraction pour chaque système majeur +Modularité : Chaque système indépendant + + +2. Architecture globale +Vue d'ensemble +Engine (point d'entrée) +│ +├── Core/ +│ ├── GameLoop → Boucle principale (update/render) +│ ├── SceneManager → Gestion des scènes/niveaux +│ └── Time → Gestion du temps (deltaTime, etc.) +│ +├── Rendering/ +│ ├── WebGLRenderer → Coordonne tout le rendu +│ ├── SpriteBatch → Optimisation batch rendering +│ ├── Shader → Programmes GPU +│ ├── Material → Shader + paramètres +│ ├── Texture → Gestion des images +│ ├── Camera → Viewport et transformations +│ └── RenderQueue → Tri et optimisation +│ +├── WebGL/ +│ ├── GLContext → Abstraction WebGL +│ ├── Buffer → Buffers GPU (vertices, indices) +│ ├── VertexArray → Configuration des attributs +│ └── Framebuffer → Rendu dans une texture +│ +├── Entities/ +│ ├── GameObject → Entité de base du jeu +│ ├── Transform → Position, rotation, scale +│ └── Component → Classe de base des composants +│ +├── Components/ +│ ├── SpriteRenderer → Affiche un sprite +│ ├── Animator → Gère les animations +│ ├── RigidBody → Physique +│ ├── Collider → Détection de collision +│ ├── ParticleEmitter → Système de particules +│ └── TilemapRenderer → Affichage de tilemap +│ +├── Systems/ +│ ├── PhysicsSystem → Mise à jour de la physique +│ ├── CollisionSystem → Détection/résolution collisions +│ └── AnimationSystem → Mise à jour des animations +│ +├── Input/ +│ └── InputManager → Clavier, souris, tactile +│ +├── Audio/ +│ └── AudioManager → Sons et musique +│ +├── Assets/ +│ └── AssetLoader → Chargement des ressources +│ +├── Math/ +│ ├── Vector2 → Vecteur 2D (x, y) +│ ├── Matrix3 → Matrice 3×3 (transformations 2D) +│ ├── Rect → Rectangle +│ └── Color → Couleur RGBA +│ +└── Utils/ + ├── ObjectPool → Réutilisation d'objets + ├── EventBus → Système d'événements + ├── Quadtree → Spatial partitioning + └── Debug → Outils de debug +``` + +--- + +## 3. Core du moteur + +### 3.1 Engine (orchestrateur principal) + +**Rôle** : Point d'entrée, initialise et coordonne tous les systèmes. + +**Responsabilités** : +- Créer le canvas WebGL +- Initialiser tous les managers (Input, Audio, Assets, Renderer) +- Créer et gérer le SceneManager +- Démarrer/arrêter la GameLoop +- Gérer le cycle de vie (start, pause, resume, stop) +- Exposer une API publique pour l'utilisateur + +**Propriétés importantes** : +- `canvas: HTMLCanvasElement` - Le canvas de rendu +- `renderer: WebGLRenderer` - Le système de rendu +- `sceneManager: SceneManager` - Gestion des scènes +- `inputManager: InputManager` - Gestion des entrées +- `audioManager: AudioManager` - Gestion de l'audio +- `assetLoader: AssetLoader` - Chargement des ressources +- `gameLoop: GameLoop` - La boucle de jeu +- `isRunning: boolean` - État du moteur + +**Méthodes principales** : +- `initialize()` - Setup initial +- `start()` - Démarre le moteur +- `stop()` - Arrête le moteur +- `pause()` - Met en pause +- `resume()` - Reprend +- `loadScene(name: string)` - Charge une scène + +--- + +### 3.2 GameLoop (boucle de jeu) + +**Rôle** : Cœur battant du moteur, appelle update() et render() en boucle. + +**Responsabilités** : +- Utiliser `requestAnimationFrame()` pour synchronisation avec l'écran +- Calculer le `deltaTime` (temps écoulé depuis la dernière frame) +- Appeler `update()` pour la logique du jeu +- Appeler `render()` pour le dessin +- Gérer le fixed timestep pour la physique (optionnel mais recommandé) +- Gérer la pause/reprise + +**Propriétés importantes** : +- `isRunning: boolean` - État de la boucle +- `lastTime: number` - Timestamp de la frame précédente +- `deltaTime: number` - Temps écoulé depuis dernière frame +- `targetFPS: number` - FPS cible (optionnel, pour limiter) +- `accumulator: number` - Pour fixed timestep physique + +**Cycle d'une frame** : +``` +1. Calculer deltaTime (currentTime - lastTime) +2. Mettre à jour Time.deltaTime +3. Appeler fixedUpdate() pour la physique (timestep fixe) +4. Appeler update() pour la logique (timestep variable) +5. Appeler render() pour le dessin +6. Demander la prochaine frame via requestAnimationFrame() +``` + +**Pourquoi fixed timestep pour la physique ?** : +La physique doit être déterministe. Avec un timestep fixe (ex: 16ms = 60 FPS), les calculs sont toujours identiques, évitant les bugs bizarres. + +--- + +### 3.3 SceneManager (gestion des scènes) + +**Rôle** : Gère les différents "écrans" du jeu (menu, niveaux, game over...). + +**Responsabilités** : +- Stocker toutes les scènes disponibles +- Charger/décharger une scène +- Gérer les transitions entre scènes +- N'update/render que la scène active +- Précharger les scènes en arrière-plan (optionnel) + +**Propriétés importantes** : +- `scenes: Map` - Toutes les scènes +- `activeScene: Scene | null` - La scène actuellement active +- `isTransitioning: boolean` - En cours de transition + +**Méthodes principales** : +- `registerScene(name: string, scene: Scene)` - Enregistre une scène +- `loadScene(name: string)` - Change de scène +- `unloadScene(name: string)` - Décharge une scène +- `getActiveScene(): Scene` - Récupère la scène active +- `update(deltaTime: number)` - Update la scène active +- `render()` - Rend la scène active + +**Exemple d'utilisation** : +``` +sceneManager.registerScene("MainMenu", new MainMenuScene()); +sceneManager.registerScene("Level1", new Level1Scene()); +sceneManager.registerScene("GameOver", new GameOverScene()); + +sceneManager.loadScene("MainMenu"); +// Plus tard... +sceneManager.loadScene("Level1"); +``` + +--- + +### 3.4 Scene (une scène de jeu) + +**Rôle** : Contient tous les GameObjects d'un niveau/écran. + +**Responsabilités** : +- Stocker tous les GameObjects de la scène +- Gérer la hiérarchie des objets +- Initialiser la scène (onLoad) +- Nettoyer la scène (onUnload) +- Fournir des méthodes de recherche d'objets +- Update tous les GameObjects actifs +- Collecter les objets à rendre + +**Propriétés importantes** : +- `name: string` - Nom de la scène +- `gameObjects: GameObject[]` - Liste de tous les objets +- `camera: Camera` - Caméra principale de la scène +- `isLoaded: boolean` - État de chargement + +**Méthodes principales** : +- `onLoad()` - Appelé au chargement de la scène +- `onUnload()` - Appelé au déchargement +- `update(deltaTime: number)` - Update tous les objets +- `addGameObject(obj: GameObject)` - Ajoute un objet +- `removeGameObject(obj: GameObject)` - Retire un objet +- `findGameObjectByName(name: string)` - Recherche par nom +- `getAllRenderables()` - Récupère tous les objets à rendre + +--- + +### 3.5 Time (gestion du temps) + +**Rôle** : Centralise toutes les informations temporelles du jeu. + +**Responsabilités** : +- Fournir le `deltaTime` à tout le moteur +- Gérer le `timeScale` (ralenti, accéléré) +- Compter le temps total écoulé +- Compter les frames rendues +- Calculer les FPS réels + +**Propriétés importantes** : +- `deltaTime: number` - Temps écoulé depuis dernière frame (en secondes) +- `unscaledDeltaTime: number` - DeltaTime non affecté par timeScale +- `timeScale: number` - Multiplicateur de temps (1 = normal, 0.5 = ralenti, 2 = rapide) +- `time: number` - Temps total depuis le démarrage (en secondes) +- `frameCount: number` - Nombre de frames rendues +- `fps: number` - FPS moyen + +**Pourquoi c'est crucial** : +Sans deltaTime, un objet qui bouge de 5 pixels par frame ira : +- 2× plus vite sur un écran 120Hz que sur un 60Hz +- Beaucoup plus lentement si le jeu lag + +**Avec deltaTime** : +``` +// Mauvais : +position.x += 5; // Dépend des FPS + +// Bon : +position.x += 300 * Time.deltaTime; // 300 pixels/seconde indépendant des FPS +``` + +--- + +## 4. Système de rendu WebGL + +### 4.1 WebGL : Concepts de base + +**WebGL** est une API pour utiliser le GPU du navigateur. + +**Pipeline de rendu WebGL** : +``` +1. Données JavaScript (positions, couleurs, etc.) + ↓ +2. Buffers GPU (envoi des données au GPU) + ↓ +3. Vertex Shader (transforme chaque sommet) + ↓ +4. Rasterization (convertit triangles en pixels) + ↓ +5. Fragment Shader (colorie chaque pixel) + ↓ +6. Framebuffer (image finale) +``` + +**Concepts clés** : + +**Vertices (sommets)** : Points dans l'espace 3D/2D. En 2D, tout est composé de triangles. + +**Buffers** : Mémoire sur le GPU contenant des données (positions, couleurs, UVs...). + +**Shaders** : Petits programmes qui s'exécutent sur le GPU, écrits en GLSL (langage proche du C). + +**Uniforms** : Variables globales partagées par tous les vertices/pixels (ex: matrices de transformation, couleur globale). + +**Attributes** : Données qui varient pour chaque vertex (ex: position, couleur). + +**Textures** : Images stockées sur le GPU. + +**Système de coordonnées WebGL** : +``` + Y (1) + | + | + -1 ----+---- 1 X + | + | + -1 + +Centre = (0, 0) +Haut-droite = (1, 1) +Bas-gauche = (-1, -1) +``` + +Il faut convertir les coordonnées pixel en coordonnées normalisées (NDC = Normalized Device Coordinates). + +--- + +### 4.2 WebGLRenderer (coordonne le rendu) + +**Rôle** : Chef d'orchestre du système de rendu. + +**Responsabilités** : +- Initialiser le contexte WebGL +- Gérer le SpriteBatch +- Gérer la bibliothèque de shaders +- Gérer le cache de textures +- Gérer la RenderQueue +- Clear le canvas +- Coordonner le rendu de la scène +- Appliquer les transformations de caméra + +**Propriétés importantes** : +- `gl: WebGLRenderingContext` - Contexte WebGL +- `spriteBatch: SpriteBatch` - Système de batch rendering +- `renderQueue: RenderQueue` - File de rendu triée +- `shaderLibrary: Map` - Tous les shaders +- `textureCache: Map` - Toutes les textures chargées +- `defaultShader: Shader` - Shader sprite par défaut +- `currentShader: Shader` - Shader actuellement actif +- `viewportWidth: number` - Largeur du viewport +- `viewportHeight: number` - Hauteur du viewport + +**Méthodes principales** : +- `initialize()` - Setup WebGL +- `clear(color: Color)` - Efface le canvas +- `render(scene: Scene, camera: Camera)` - Rend une scène +- `loadShader(name: string, vs: string, fs: string)` - Charge un shader +- `loadTexture(name: string, url: string)` - Charge une texture +- `resize(width: number, height: number)` - Redimensionne le viewport + +**Flow de rendu** : +``` +1. Clear le canvas +2. Collecter tous les objets visibles de la scène +3. Les soumettre à la RenderQueue +4. Trier la RenderQueue +5. Commencer le SpriteBatch +6. Pour chaque objet trié : + - Changer de shader si nécessaire + - Ajouter au batch +7. Flush le batch +``` + +--- + +### 4.3 SpriteBatch (★ optimisation cruciale) + +**Rôle** : Accumule plusieurs sprites et les dessine en un seul draw call. + +**Problème sans batch** : +``` +Pour 1000 sprites : +- 1000 draw calls au GPU +- Très lent (changements d'état GPU coûteux) +``` + +**Solution avec batch** : +``` +Pour 1000 sprites : +- 1 draw call au GPU +- Jusqu'à 100× plus rapide ! +``` + +**Responsabilités** : +- Accumuler les données de plusieurs sprites dans un gros buffer +- Gérer un buffer de vertices (positions, UVs, couleurs) +- Gérer un buffer d'indices (ordre de dessin des triangles) +- Flush (dessiner) quand : + - Le batch est plein (ex: 10000 sprites max) + - La texture change + - Le shader change +- Transformer les sprites (appliquer rotation, scale, position) +- Calculer les UVs (coordonnées texture) + +**Propriétés importantes** : +- `maxSprites: number` - Capacité max (ex: 10000) +- `vertices: Float32Array` - Buffer CPU des vertices +- `indices: Uint16Array` - Buffer CPU des indices +- `vertexBuffer: WebGLBuffer` - Buffer GPU +- `indexBuffer: WebGLBuffer` - Buffer GPU +- `spriteCount: number` - Nombre de sprites actuellement dans le batch +- `currentTexture: Texture` - Texture actuellement liée +- `shader: Shader` - Shader utilisé par le batch + +**Structure d'un vertex** : +``` +[x, y, u, v, r, g, b, a] + │ │ │ │ └─────────┘ + │ │ │ │ Couleur (RGBA) + │ │ └──┘ + │ │ Coordonnées texture (UV) + └──┘ + Position 2D +``` + +**Méthodes principales** : +- `begin(projection: Matrix3, view: Matrix3)` - Commence un batch +- `draw(texture, transform, sourceRect, tint)` - Ajoute un sprite +- `flush()` - Envoie tout au GPU et dessine +- `end()` - Termine le batch + +**Processus d'ajout d'un sprite** : +``` +1. Vérifier si le batch est plein ou si la texture change + → Si oui, flush() +2. Calculer les 4 coins du sprite (quad) avec rotation/scale +3. Calculer les UVs (quelle partie de la texture) +4. Remplir le buffer avec les 8 vertices (4 × 2 triangles) +5. Incrémenter spriteCount + +4.4 Shader (programmes GPU) +Rôle : Programme qui s'exécute sur le GPU pour transformer et colorier. +Composition : + +Vertex Shader : Transforme les positions des sommets +Fragment Shader : Décide de la couleur de chaque pixel + +Responsabilités : + +Compiler les shaders GLSL +Linker le programme +Gérer les uniforms (variables globales) +Gérer les attributes (données par vertex) +Activer/désactiver le shader + +Propriétés importantes : + +program: WebGLProgram - Programme GPU compilé +uniforms: Map - Emplacements des uniforms +attributes: Map - Emplacements des attributes + +Méthodes principales : + +compile(vertexSource: string, fragmentSource: string) - Compile +use() - Active le shader +setUniformMatrix3(name: string, matrix: Matrix3) - Envoie une matrice +setUniformFloat(name: string, value: number) - Envoie un float +setUniformVec2(name: string, x: number, y: number) - Envoie un vec2 +setUniformTexture(name: string, texture: Texture, slot: number) - Lie une texture + +Shader sprite par défaut - Vertex : +glslattribute vec2 aPosition; // Position du vertex +attribute vec2 aTexCoord; // Coordonnées UV +attribute vec4 aColor; // Couleur de teinte + +uniform mat3 uProjection; // Matrice de projection +uniform mat3 uView; // Matrice de vue + +varying vec2 vTexCoord; // Passe au fragment shader +varying vec4 vColor; + +void main() { + // Transforme la position + vec3 pos = uProjection * uView * vec3(aPosition, 1.0); + gl_Position = vec4(pos.xy, 0.0, 1.0); + + // Passe les données au fragment shader + vTexCoord = aTexCoord; + vColor = aColor; +} +Shader sprite par défaut - Fragment : +glslprecision mediump float; + +uniform sampler2D uTexture; // La texture + +varying vec2 vTexCoord; // Reçu du vertex shader +varying vec4 vColor; + +void main() { + // Échantillonne la texture + vec4 texColor = texture2D(uTexture, vTexCoord); + + // Multiplie par la couleur de teinte + gl_FragColor = texColor * vColor; +} +``` + +--- + +### 4.5 Material (shader + paramètres) + +**Rôle** : Combine un shader avec ses paramètres (uniforms custom). + +**Responsabilités** : +- Référencer un shader +- Stocker les valeurs des uniforms +- Appliquer les uniforms au shader avant le rendu + +**Propriétés importantes** : +- `shader: Shader` - Le shader utilisé +- `uniforms: Map` - Valeurs des uniforms +- `texture: Texture` - Texture principale (optionnel) + +**Méthodes principales** : +- `setUniform(name: string, value: any)` - Définit un uniform +- `apply()` - Applique tous les uniforms au shader + +**Exemple d'utilisation** : +``` +// Créer un material pour un effet de glow +const glowMaterial = new Material(glowShader); +glowMaterial.setUniform("glowColor", new Color(1, 0.5, 0, 1)); +glowMaterial.setUniform("glowIntensity", 2.0); + +// Utiliser sur un sprite +spriteRenderer.material = glowMaterial; +``` + +--- + +### 4.6 Texture (gestion des images) + +**Rôle** : Encapsule une texture WebGL (image sur le GPU). + +**Responsabilités** : +- Charger une image depuis une URL +- Créer une texture WebGL +- Configurer les paramètres de texture (filtrage, wrapping) +- Lier la texture à un slot +- Libérer la mémoire GPU + +**Propriétés importantes** : +- `glTexture: WebGLTexture` - Texture GPU +- `width: number` - Largeur +- `height: number` - Hauteur +- `id: number` - Identifiant unique +- `filterMode: FilterMode` - LINEAR ou NEAREST +- `wrapMode: WrapMode` - REPEAT, CLAMP, MIRROR + +**Méthodes principales** : +- `loadFromImage(image: HTMLImageElement)` - Charge depuis une image +- `bind(slot: number)` - Active la texture sur un slot (0-31) +- `unbind()` - Désactive +- `dispose()` - Libère la mémoire GPU + +**Paramètres importants** : + +**Filter Mode** : +- `NEAREST` : Pixels nets (pixel art) +- `LINEAR` : Lissage (sprites HD) + +**Wrap Mode** : +- `CLAMP` : Bords figés +- `REPEAT` : Répétition (pour tiling) +- `MIRROR` : Répétition miroir + +--- + +### 4.7 Camera (viewport et transformations) + +**Rôle** : Définit quelle partie du monde est visible. + +**Responsabilités** : +- Définir la position dans le monde +- Gérer le zoom +- Gérer la rotation (effets spéciaux) +- Calculer la matrice de vue (View Matrix) +- Calculer la matrice de projection (Projection Matrix) +- Convertir coordonnées écran ↔ monde +- Suivre un objet (ex: le joueur) +- Gérer les limites de la caméra (bounds) + +**Propriétés importantes** : +- `position: Vector2` - Position dans le monde +- `zoom: number` - Niveau de zoom (1 = normal, 2 = 2× plus proche) +- `rotation: number` - Rotation en degrés +- `viewportWidth: number` - Largeur de l'écran +- `viewportHeight: number` - Hauteur de l'écran +- `bounds: Rect` - Limites de déplacement (optionnel) +- `target: GameObject` - Objet à suivre (optionnel) + +**Méthodes principales** : +- `getViewMatrix(): Matrix3` - Matrice de vue +- `getProjectionMatrix(): Matrix3` - Matrice de projection +- `screenToWorld(screenPos: Vector2): Vector2` - Convertit écran → monde +- `worldToScreen(worldPos: Vector2): Vector2` - Convertit monde → écran +- `follow(target: GameObject, smoothness: number)` - Suit un objet +- `shake(intensity: number, duration: number)` - Secoue l'écran + +**Matrices expliquées** : + +**Projection Matrix** : Convertit pixels → coordonnées NDC WebGL (-1 à 1) + +**View Matrix** : Applique position, rotation, zoom de la caméra + +**Exemple** : +``` +Objet à (1000, 500) dans le monde +Caméra à (900, 400), zoom 2 + +View Matrix appliquée : +- Translation : (1000 - 900, 500 - 400) = (100, 100) +- Zoom ×2 : (200, 200) + +Projection Matrix : +- Convertit (200, 200) pixels → coordonnées NDC + +Résultat : L'objet s'affiche au bon endroit sur l'écran +``` + +--- + +### 4.8 RenderQueue (tri et optimisation) + +**Rôle** : Optimise le rendu en triant les objets intelligemment. + +**Responsabilités** : +- Collecter tous les objets à rendre +- Les trier par : + 1. **Layer** (z-index) : Objets du fond vers le devant + 2. **Shader** : Minimise les changements de shader (coûteux) + 3. **Texture** : Minimise les changements de texture +- Culling : Ignorer les objets hors de la caméra +- Frustum culling : Ne rendre que ce qui est visible + +**Propriétés importantes** : +- `renderables: Renderable[]` - Liste des objets à rendre +- `camera: Camera` - Pour le culling + +**Méthodes principales** : +- `submit(renderable: Renderable)` - Ajoute un objet +- `sort()` - Trie intelligemment +- `render(spriteBatch: SpriteBatch)` - Rend tout +- `clear()` - Vide la queue + +**Pourquoi trier ?** + +Changer de shader ou de texture sur le GPU est **très coûteux**. + +**Sans tri** : +``` +Sprite A (texture 1, shader 1) +Sprite B (texture 2, shader 1) → Change texture +Sprite C (texture 1, shader 1) → Change texture += 2 changements de texture inutiles +``` + +**Avec tri** : +``` +Sprite A (texture 1, shader 1) +Sprite C (texture 1, shader 1) → Pas de changement +Sprite B (texture 2, shader 1) → Change texture 1 fois += Optimisé ! + +4.9 Abstraction WebGL (classes utilitaires) +GLContext +Rôle : Encapsule le contexte WebGL, simplifie l'API. +Responsabilités : + +Initialiser WebGL +Gérer les extensions WebGL +Fournir des helpers pour les opérations courantes +Gérer les états WebGL (blend mode, depth test...) + +Buffer +Rôle : Encapsule un buffer WebGL (vertex ou index). +Responsabilités : + +Créer un buffer GPU +Uploader des données (Float32Array, Uint16Array) +Lier/délier le buffer +Gérer le type (STATIC_DRAW, DYNAMIC_DRAW, STREAM_DRAW) + +Types de buffers : + +Vertex Buffer : Contient les données des vertices +Index Buffer : Contient l'ordre de dessin + +VertexArray (VAO) +Rôle : Stocke la configuration des attributs vertex. +Responsabilités : + +Configurer les attributs (position, UV, couleur...) +Lier les buffers aux attributs +Activer/désactiver le VAO + +Avantage : Évite de reconfigurer les attributs à chaque draw. +Framebuffer +Rôle : Permet de rendre dans une texture au lieu de l'écran. +Responsabilités : + +Créer un framebuffer +Attacher une texture de couleur +Attacher un depth/stencil buffer (optionnel) +Lier/délier le framebuffer + +Utilité : Post-processing, effets, render to texture. + +5. Système d'entités et composants +5.1 GameObject (entité de base) +Rôle : Tout ce qui existe dans le jeu est un GameObject. +Exemples : Joueur, ennemi, projectile, pièce, mur, déclencheur... +Responsabilités : + +Avoir un Transform (position, rotation, scale) +Contenir des Components (comportements) +Gérer une hiérarchie parent-enfant +Avoir un état actif/inactif +Avoir un nom (pour debug/recherche) +Gérer son cycle de vie (awake, start, update, destroy) + +Propriétés importantes : + +name: string - Nom de l'objet +active: boolean - Actif ou non +transform: Transform - Position, rotation, scale +`components +RéessayerSContinuer: Map` - Tous les composants + +parent: GameObject | null - Parent dans la hiérarchie +children: GameObject[] - Enfants +scene: Scene - Scène à laquelle il appartient +layer: number - Layer de rendu (z-index) +tag: string - Tag pour catégorisation (ex: "Player", "Enemy") + +Méthodes principales : + +addComponent(component: T): T - Ajoute un composant +getComponent(type: new (...args) => T): T | null - Récupère un composant +removeComponent(type: new (...args) => T) - Retire un composant +addChild(child: GameObject) - Ajoute un enfant +removeChild(child: GameObject) - Retire un enfant +setParent(parent: GameObject) - Change de parent +update(deltaTime: number) - Update tous les composants +destroy() - Détruit l'objet + +Cycle de vie : +1. Construction : new GameObject() +2. awake() : Appelé à la création +3. start() : Appelé avant la première update +4. update(deltaTime) : Appelé chaque frame +5. destroy() : Appelé à la destruction +``` + +**Hiérarchie parent-enfant** : +``` +Personnage (GameObject) +├── Transform (x: 100, y: 200) +├── SpriteRenderer +└── Enfants: + ├── Épée (GameObject) + │ ├── Transform (local: x: 20, y: 0) + │ └── SpriteRenderer + └── Bouclier (GameObject) + ├── Transform (local: x: -20, y: 0) + └── SpriteRenderer + +Si le personnage se déplace de (10, 5), +l'épée et le bouclier suivent automatiquement ! +``` + +--- + +### 5.2 Transform (position, rotation, scale) + +**Rôle** : Définit où et comment l'objet apparaît dans le monde. + +**Responsabilités** : +- Stocker position, rotation, scale +- Calculer la matrice de transformation +- Gérer les transformations locales vs monde (local space vs world space) +- Propager les changements aux enfants +- Utiliser des dirty flags pour optimiser + +**Propriétés importantes** : +- `localPosition: Vector2` - Position relative au parent +- `localRotation: number` - Rotation relative (en degrés) +- `localScale: Vector2` - Scale relatif +- `position: Vector2` (getter/setter) - Position monde +- `rotation: number` (getter/setter) - Rotation monde +- `scale: Vector2` (getter/setter) - Scale monde +- `parent: Transform | null` - Transform du parent +- `children: Transform[]` - Transforms des enfants + +**Propriétés calculées (avec cache)** : +- `worldMatrix: Matrix3` - Matrice de transformation complète +- `right: Vector2` - Vecteur droit (après rotation) +- `up: Vector2` - Vecteur haut (après rotation) +- `forward: Vector2` - Direction avant + +**Méthodes principales** : +- `translate(delta: Vector2)` - Déplace l'objet +- `rotate(angle: number)` - Tourne l'objet +- `lookAt(target: Vector2)` - Regarde vers une position +- `getWorldMatrix(): Matrix3` - Récupère la matrice (avec cache) +- `transformPoint(point: Vector2): Vector2` - Transforme un point local → monde +- `inverseTransformPoint(point: Vector2): Vector2` - Transforme un point monde → local + +**Dirty Flags (optimisation)** : +``` +private _dirty = false; +private _cachedMatrix: Matrix3; + +set position(value: Vector2) { + this._localPosition = value; + this._dirty = true; // Marque "besoin de recalculer" + this.markChildrenDirty(); // Propage aux enfants +} + +getWorldMatrix(): Matrix3 { + if (this._dirty) { + this._cachedMatrix = this.calculateMatrix(); + this._dirty = false; + } + return this._cachedMatrix; // Retourne le cache +} +``` + +**Avantage** : Économise 80% des calculs pour les objets statiques. + +--- + +### 5.3 Component (classe de base) + +**Rôle** : Classe abstraite pour tous les comportements attachables. + +**Philosophie ECS** : Composition > Héritage + +**Au lieu de** : +``` +class Enemy extends Character extends Entity { } +class FlyingEnemy extends Enemy { } +class ShootingFlyingEnemy extends FlyingEnemy { } +// Hiérarchie cauchemardesque ! +``` + +**On fait** : +``` +const enemy = new GameObject(); +enemy.addComponent(new SpriteRenderer()); +enemy.addComponent(new Health(100)); +enemy.addComponent(new AIBehavior()); +enemy.addComponent(new GroundMovement()); + +// Facilement modifiable : +enemy.removeComponent(GroundMovement); +enemy.addComponent(new FlyingMovement()); +``` + +**Responsabilités du Component** : +- Référencer son GameObject parent +- Avoir un état actif/inactif +- Implémenter le cycle de vie (awake, start, update, destroy) +- Communiquer avec d'autres components via le GameObject + +**Propriétés importantes** : +- `gameObject: GameObject` - GameObject parent +- `transform: Transform` - Shortcut vers gameObject.transform +- `enabled: boolean` - Actif ou non + +**Méthodes du cycle de vie (à override)** : +- `awake()` - Appelé à l'ajout du component +- `start()` - Appelé avant la première update +- `update(deltaTime: number)` - Appelé chaque frame +- `onDestroy()` - Appelé à la destruction + +**Méthodes utilitaires** : +- `getComponent(type): T` - Shortcut vers gameObject.getComponent() + +--- + +## 6. Composants principaux + +### 6.1 SpriteRenderer (affiche un sprite) + +**Rôle** : Composant pour afficher une image 2D. + +**Responsabilités** : +- Référencer une texture +- Définir quelle partie de la texture afficher (pour spritesheets) +- Appliquer une couleur de teinte (tint) +- Gérer le flip horizontal/vertical +- Définir le layer de rendu +- S'enregistrer auprès du renderer +- Se dessiner via le SpriteBatch + +**Propriétés importantes** : +- `texture: Texture` - L'image à afficher +- `sourceRect: Rect` - Quelle partie de la texture (pour spritesheet) +- `tint: Color` - Couleur de teinte (blanc = normal) +- `flipX: boolean` - Miroir horizontal +- `flipY: boolean` - Miroir vertical +- `layer: number` - Z-index pour le tri +- `material: Material` - Shader + uniforms custom (optionnel) +- `pivot: Vector2` - Point d'ancrage (0.5, 0.5 = centre) + +**Méthodes principales** : +- `render(spriteBatch: SpriteBatch)` - Dessine le sprite +- `setSprite(texture: Texture, sourceRect: Rect)` - Change le sprite + +**Utilisation** : +``` +const player = new GameObject(); +const renderer = player.addComponent(new SpriteRenderer()); +renderer.texture = assetLoader.getTexture("player"); +renderer.sourceRect = new Rect(0, 0, 32, 32); // Sprite de 32×32 +renderer.tint = new Color(1, 1, 1, 1); // Blanc opaque +renderer.layer = 3; +``` + +--- + +### 6.2 Animator (gère les animations) + +**Rôle** : Anime un SpriteRenderer en changeant son sourceRect. + +**Responsabilités** : +- Stocker plusieurs animations (idle, walk, jump, attack...) +- Jouer une animation +- Gérer le timing entre les frames +- Supporter les animations en boucle ou one-shot +- Déclencher des événements à certaines frames +- Transitionner entre animations + +**Propriétés importantes** : +- `animations: Map` - Toutes les animations +- `currentAnimation: Animation | null` - Animation en cours +- `currentFrame: number` - Frame actuelle +- `frameTime: number` - Temps écoulé sur la frame actuelle +- `isPlaying: boolean` - En cours de lecture +- `speed: number` - Vitesse de lecture (1 = normal, 2 = 2× plus rapide) + +**Structure d'une Animation** : +``` +class Animation { + name: string; + frames: Rect[]; // Liste des sourceRects + frameDuration: number; // Durée de chaque frame (en secondes) + loop: boolean; // Boucle ou non + events: AnimationEvent[]; // Événements (ex: "playSound" à la frame 3) +} +``` + +**Méthodes principales** : +- `addAnimation(name: string, animation: Animation)` - Ajoute une animation +- `play(name: string, restart: boolean)` - Joue une animation +- `stop()` - Arrête l'animation +- `pause()` - Met en pause +- `resume()` - Reprend +- `update(deltaTime: number)` - Avance l'animation + +**Exemple d'utilisation** : +``` +const animator = player.addComponent(new Animator()); + +// Créer une animation de marche (8 frames) +const walkAnim = new Animation("walk", [ + new Rect(0, 0, 32, 32), // Frame 0 + new Rect(32, 0, 32, 32), // Frame 1 + new Rect(64, 0, 32, 32), // Frame 2 + // ... etc +], 0.1, true); // 0.1s par frame, en boucle + +animator.addAnimation("walk", walkAnim); +animator.play("walk"); +``` + +--- + +### 6.3 RigidBody (physique) + +**Rôle** : Ajoute des propriétés physiques à un objet. + +**Responsabilités** : +- Stocker la vélocité (vitesse) +- Appliquer la gravité +- Appliquer des forces +- Gérer la masse +- Gérer le drag (friction de l'air) +- Gérer le type de body (static, kinematic, dynamic) +- Intégrer le mouvement (mise à jour de la position) + +**Propriétés importantes** : +- `velocity: Vector2` - Vitesse actuelle (pixels/seconde) +- `acceleration: Vector2` - Accélération +- `mass: number` - Masse (affecte les forces) +- `drag: number` - Friction aérienne (0-1, 0 = pas de friction) +- `gravityScale: number` - Multiplicateur de gravité (1 = normal, 0 = pas de gravité) +- `bodyType: BodyType` - Type de corps +- `useGravity: boolean` - Affecté par la gravité ou non +- `freezeRotation: boolean` - Empêche la rotation + +**Types de Body** : +- **Static** : Ne bouge jamais (murs, sol) +- **Kinematic** : Bouge mais n'est pas affecté par les forces (plateformes mobiles) +- **Dynamic** : Physique complète (joueur, ennemis, objets) + +**Méthodes principales** : +- `addForce(force: Vector2)` - Ajoute une force +- `addImpulse(impulse: Vector2)` - Ajoute une impulsion instantanée +- `setVelocity(velocity: Vector2)` - Définit la vitesse directement +- `update(deltaTime: number)` - Intègre le mouvement + +**Intégration du mouvement** : +``` +update(deltaTime: number) { + if (bodyType !== BodyType.Dynamic) return; + + // Applique la gravité + if (useGravity) { + velocity.y += Physics.gravity * gravityScale * deltaTime; + } + + // Applique la friction + velocity.x *= (1 - drag); + velocity.y *= (1 - drag); + + // Met à jour la position + transform.position.x += velocity.x * deltaTime; + transform.position.y += velocity.y * deltaTime; +} +``` + +--- + +### 6.4 Collider (détection de collision) + +**Rôle** : Définit la forme de collision d'un objet. + +**Types de Colliders** : +- **BoxCollider** : Rectangle (le plus courant en 2D) +- **CircleCollider** : Cercle +- **PolygonCollider** : Polygone custom +- **EdgeCollider** : Ligne (pour les sols) + +**Responsabilités** : +- Définir la forme de collision +- Définir si c'est un trigger (traverse les objets) ou solid +- Calculer les bounds (rectangle englobant) +- Tester la collision avec d'autres colliders +- Déclencher des événements de collision + +**Propriétés importantes (BoxCollider exemple)** : +- `size: Vector2` - Taille du rectangle +- `offset: Vector2` - Décalage par rapport au Transform +- `isTrigger: boolean` - Trigger ou solid +- `layer: number` - Layer de collision (pour filtrer) + +**Méthodes principales** : +- `getBounds(): Rect` - Rectangle englobant +- `contains(point: Vector2): boolean` - Teste si un point est dedans +- `overlaps(other: Collider): boolean` - Teste la collision avec un autre +- `onCollisionEnter(other: Collider)` - Callback de collision +- `onCollisionExit(other: Collider)` - Callback de fin de collision +- `onTriggerEnter(other: Collider)` - Callback de trigger + +**Différence Trigger vs Solid** : +- **Solid** : Empêche le passage (murs, sol) +- **Trigger** : Détecte l'entrée mais laisse passer (zone de collecte, checkpoint) + +--- + +### 6.5 ParticleEmitter (système de particules) + +**Rôle** : Émetteur de particules (feu, fumée, étincelles, pluie...). + +**Responsabilités** : +- Créer des particules +- Mettre à jour leur vie +- Recycler les particules mortes (object pooling) +- Appliquer des forces (gravité, vent) +- Rendre toutes les particules (bénéficie énormément de WebGL batch) + +**Propriétés importantes** : +- `maxParticles: number` - Nombre max de particules (ex: 5000) +- `emissionRate: number` - Particules par seconde +- `lifetime: number` - Durée de vie d'une particule (secondes) +- `startColor: Color` - Couleur initiale +- `endColor: Color` - Couleur finale (interpolation) +- `startSize: number` - Taille initiale +- `endSize: number` - Taille finale +- `startVelocity: Vector2` - Vitesse initiale (avec randomness) +- `gravity: Vector2` - Gravité des particules +- `texture: Texture` - Texture de la particule + +**Structure d'une Particule** : +``` +class Particle { + position: Vector2; + velocity: Vector2; + color: Color; + size: number; + rotation: number; + lifetime: number; + age: number; + active: boolean; +} +``` + +**Méthodes principales** : +- `emit(count: number)` - Émet X particules +- `update(deltaTime: number)` - Met à jour toutes les particules +- `render(spriteBatch: SpriteBatch)` - Dessine toutes les particules + +**Avantage WebGL** : +Avec Canvas 2D, 500 particules = lag. +Avec WebGL + batch, 5000 particules = fluide ! + +--- + +### 6.6 TilemapRenderer (affichage de tilemap) + +**Rôle** : Affiche efficacement une grille de tiles (tuiles). + +**Responsabilités** : +- Stocker une grille de tiles +- Référencer un tileset (spritesheet de tiles) +- Rendre uniquement les tiles visibles (culling) +- Optimiser avec un seul draw call pour toute la tilemap +- Supporter plusieurs layers (fond, milieu, avant-plan) + +**Propriétés importantes** : +- `tiles: number[][]` - Grille 2D d'IDs de tiles +- `tileset: Texture` - Spritesheet contenant tous les tiles +- `tileSize: number` - Taille d'un tile (ex: 16×16 pixels) +- `mapWidth: number` - Largeur en tiles +- `mapHeight: number` - Hauteur en tiles +- `layer: number` - Layer de rendu + +**Méthodes principales** : +- `setTile(x: number, y: number, tileId: number)` - Change un tile +- `getTile(x: number, y: number): number` - Récupère un tile +- `render(spriteBatch: SpriteBatch, camera: Camera)` - Rend la tilemap +- `worldToTile(worldPos: Vector2): Vector2` - Convertit monde → tile +- `tileToWorld(tilePos: Vector2): Vector2` - Convertit tile → monde + +**Optimisation** : +``` +// Au lieu de dessiner TOUS les tiles : +for (let y = 0; y < mapHeight; y++) { + for (let x = 0; x < mapWidth; x++) { + drawTile(x, y); // 1000× 1000 = 1 million de tiles ! + } +} + +// On dessine seulement les tiles visibles : +const visibleArea = camera.getVisibleTileArea(); +for (let y = visibleArea.top; y < visibleArea.bottom; y++) { + for (let x = visibleArea.left; x < visibleArea.right; x++) { + drawTile(x, y); // Seulement ~400 tiles ! + } +} +``` + +--- + +## 7. Systèmes auxiliaires + +### 7.1 InputManager (clavier, souris, tactile) + +**Rôle** : Centralise toutes les entrées utilisateur. + +**Responsabilités** : +- Écouter les événements du navigateur (keydown, keyup, mousedown...) +- Stocker l'état actuel de toutes les touches/boutons +- Différencier les états : + - **Down** : Pressé cette frame + - **Held** : Maintenu + - **Up** : Relâché cette frame +- Gérer la souris (position, clics, molette) +- Gérer le tactile (touches, gestures) +- Réinitialiser les états "down" et "up" chaque frame + +**Propriétés importantes** : +- `keys: Map` - État de toutes les touches +- `mousePosition: Vector2` - Position de la souris +- `mouseButtons: Map` - État des boutons souris +- `touches: Touch[]` - Points de touche actifs +- `wheelDelta: number` - Déplacement de la molette + +**États possibles** : +``` +enum KeyState { + Up, // Pas pressé + Down, // Pressé cette frame + Held, // Maintenu + Released // Relâché cette frame +} +``` + +**Méthodes principales** : +- `isKeyDown(key: string): boolean` - Touche pressée cette frame +- `isKeyHeld(key: string): boolean` - Touche maintenue +- `isKeyUp(key: string): boolean` - Touche relâchée cette frame +- `isMouseButtonDown(button: number): boolean` - Bouton souris pressé +- `getMousePosition(): Vector2` - Position souris +- `getMouseWorldPosition(camera: Camera): Vector2` - Position monde +- `update()` - Reset des états down/up (appelé chaque frame) + +**Exemple d'utilisation** : +``` +// Dans le script du joueur : +update(deltaTime: number) { + // Déplacement + if (input.isKeyHeld("ArrowRight")) { + this.moveRight(); + } + + // Saut (une seule fois par pression) + if (input.isKeyDown("Space")) { + this.jump(); + } + + // Tir (en maintenant) + if (input.isMouseButtonHeld(0)) { + this.shoot(); + } +} +``` + +--- + +### 7.2 AudioManager (sons et musique) + +**Rôle** : Gère tous les sons du jeu. + +**Responsabilités** : +- Charger les fichiers audio +- Jouer des sons (sound effects) +- Jouer de la musique en boucle +- Gérer le volume global et par catégorie +- Mettre en pause/reprendre +- Gérer les priorités (si trop de sons jouent) +- Créer un pool de sources audio (pour jouer le même son plusieurs fois) + +**Propriétés importantes** : +- `audioContext: AudioContext` - Context Web Audio API +- `sounds: Map` - Sons chargés +- `musicSource: AudioBufferSourceNode` - Source de la musique +- `masterVolume: number` - Volume global (0-1) +- `musicVolume: number` - Volume musique (0-1) +- `sfxVolume: number` - Volume effets sonores (0-1) +- `currentMusic: string` - Musique en cours + +**Méthodes principales** : +- `loadSound(name: string, url: string)` - Charge un son +- `playSound(name: string, volume?: number, loop?: boolean)` - Joue un son +- `playMusic(name: string, volume?: number)` - Joue une musique +- `stopMusic()` - Arrête la musique +- `pauseMusic()` - Met la musique en pause +- `resumeMusic()` - Reprend la musique +- `setMasterVolume(volume: number)` - Volume global +- `stopAllSounds()` - Arrête tous les sons + +**Catégories de sons** : +- **SFX** : Sons courts et répétitifs (saut, tir, collecte) +- **Music** : Musique de fond en boucle +- **Ambiance** : Sons d'ambiance (vent, pluie) + +**Optimisation** : Pool de sources audio pour éviter de créer trop d'instances. + +--- + +### 7.3 AssetLoader (chargement des ressources) + +**Rôle** : Charge tous les fichiers externes (images, sons, JSON...). + +**Responsabilités** : +- Télécharger les assets depuis des URLs +- Les mettre en cache +- Fournir un système de préloading avec progression +- Gérer les erreurs de chargement +- Créer les objets Texture, AudioBuffer... +- Libérer la mémoire des assets non utilisés + +**Propriétés importantes** : +- `textures: Map` - Textures chargées +- `sounds: Map` - Sons chargés +- `data: Map` - Données JSON chargées +- `loadingProgress: number` - Progression (0-1) +- `totalAssets: number` - Nombre total d'assets +- `loadedAssets: number` - Nombre d'assets chargés + +**Méthodes principales** : +- `loadTexture(name: string, url: string): Promise` - Charge une texture +- `loadSound(name: string, url: string): Promise` - Charge un son +- `loadJSON(name: string, url: string): Promise` - Charge du JSON +- `loadAll(manifest: AssetManifest): Promise` - Charge plusieurs assets +- `getTexture(name: string): Texture` - Récupère une texture +- `getSound(name: string): AudioBuffer` - Récupère un son +- `unload(name: string)` - Libère un asset + +**Manifest d'assets** : +``` +const manifest = { + textures: [ + { name: "player", url: "assets/player.png" }, + { name: "enemy", url: "assets/enemy.png" }, + { name: "tileset", url: "assets/tileset.png" } + ], + sounds: [ + { name: "jump", url: "assets/jump.mp3" }, + { name: "shoot", url: "assets/shoot.mp3" } + ], + data: [ + { name: "level1", url: "assets/level1.json" } + ] +}; + +await assetLoader.loadAll(manifest); +// Tous les assets sont prêts ! +``` + +**Événements** : +- `onProgress(progress: number)` - Progression du chargement +- `onComplete()` - Tous les assets chargés +- `onError(error: Error)` - Erreur de chargement + +--- + +### 7.4 PhysicsSystem (système de physique) + +**Rôle** : Met à jour tous les RigidBody et gère la gravité. + +**Responsabilités** : +- Appliquer la gravité globale +- Mettre à jour les vélocités +- Intégrer le mouvement (position += velocity × deltaTime) +- Gérer les contraintes (limites de vitesse) +- Optimiser avec spatial partitioning + +**Propriétés importantes** : +- `gravity: Vector2` - Gravité globale (ex: (0, 980) pixels/s²) +- `rigidBodies: RigidBody[]` - Tous les corps physiques +- `maxVelocity: number` - Vitesse max (pour éviter les tunneling) +- `iterations: number` - Nombre d'itérations (précision) + +**Méthodes principales** : +- `register(rigidBody: RigidBody)` - Enregistre un corps +- `unregister(rigidBody: RigidBody)` - Désenregistre +- `update(deltaTime: number)` - Met à jour tous les corps +- `setGravity(gravity: Vector2)` - Change la gravité + +**Fixed Timestep** : +``` +// La physique doit être mise à jour avec un timestep fixe +// pour être déterministe + +private accumulator = 0; +private fixedDeltaTime = 1/60; // 60 FPS + +update(deltaTime: number) { + this.accumulator += deltaTime; + + // Plusieurs updates si nécessaire + while (this.accumulator >= this.fixedDeltaTime) { + this.fixedUpdate(this.fixedDeltaTime); + this.accumulator -= this.fixedDeltaTime; + } +} +``` + +--- + +### 7.5 CollisionSystem (détection et résolution) + +**Rôle** : Détecte et résout toutes les collisions. + +**Responsabilités** : +- Détecter les collisions entre tous les Colliders +- Résoudre les collisions (empêcher la traversée) +- Calculer les normales de collision +- Déclencher les callbacks de collision +- Optimiser avec spatial partitioning (Quadtree) +- Gérer les layers de collision (filtres) + +**Propriétés importantes** : +- `colliders: Collider[]` - Tous les colliders +- `quadtree: Quadtree` - Structure spatiale pour optimisation +- `collisionMatrix: boolean[][]` - Quels layers collident entre eux + +**Méthodes principales** : +- `register(collider: Collider)` - Enregistre un collider +- `unregister(collider: Collider)` - Désenregistre +- `update()` - Détecte toutes les collisions +- `checkCollision(a: Collider, b: Collider): CollisionInfo | null` - Teste 2 colliders +- `resolveCollision(info: CollisionInfo)` - Résout une collision + +**Algorithmes de détection** : + +**AABB vs AABB (Box vs Box)** : +``` +function aabbVsAabb(a: BoxCollider, b: BoxCollider): boolean { + return a.left < b.right && + a.right > b.left && + a.top < b.bottom && + a.bottom > b.top; +} +``` + +**Circle vs Circle** : +``` +function circleVsCircle(a: CircleCollider, b: CircleCollider): boolean { + const distance = Vector2.distance(a.center, b.center); + return distance < (a.radius + b.radius); +} +``` + +**Résolution de collision** : +``` +// Sépare les deux objets +const overlap = calculateOverlap(collisionInfo); +const normal = collisionInfo.normal; + +// Déplace les objets pour qu'ils ne se chevauchent plus +objA.position -= normal * overlap * 0.5; +objB.position += normal * overlap * 0.5; +``` + +**Optimisation avec Quadtree** : + +Sans Quadtree : O(n²) = 1000 objets = 499,500 tests +Avec Quadtree : O(n log n) = 1000 objets = ~10,000 tests + +--- + +## 8. Mathématiques + +### 8.1 Vector2 (vecteur 2D) + +**Rôle** : Représente un point ou une direction en 2D. + +**Propriétés** : +- `x: number` +- `y: number` + +**Méthodes principales** : +- `add(other: Vector2): Vector2` - Addition +- `subtract(other: Vector2): Vector2` - Soustraction +- `multiply(scalar: number): Vector2` - Multiplication par un scalaire +- `divide(scalar: number): Vector2` - Division +- `length(): number` - Longueur du vecteur (magnitude) +- `normalize(): Vector2` - Vecteur de longueur 1 +- `dot(other: Vector2): number` - Produit scalaire +- `distance(other: Vector2): number` - Distance entre deux points +- `lerp(other: Vector2, t: number): Vector2` - Interpolation linéaire +- `rotate(angle: number): Vector2` - Rotation +- `angle(): number` - Angle du vecteur + +**Méthodes statiques** : +- `Vector2.zero` - (0, 0) +- `Vector2.one` - (1, 1) +- `Vector2.up` - (0, -1) +- `Vector2.down` - (0, 1) +- `Vector2.left` - (-1, 0) +- `Vector2.right` - (1, 0) + +**Exemples d'utilisation** : +``` +// Déplacement +position = position.add(velocity.multiply(deltaTime)); + +// Direction vers un point +const direction = target.subtract(position).normalize(); + +// Interpolation (smooth movement) +position = position.lerp(target, 0.1); + +8.2 Matrix3 (matrice 3×3) +Rôle : Représente une transformation 2D (translation, rotation, scale). +Structure : +RéessayerSContinuer[a, c, tx] +[b, d, ty] +[0, 0, 1 ] + +a, b, c, d : rotation et scale +tx, ty : translation +``` + +**Responsabilités** : +- Combiner plusieurs transformations en une seule +- Appliquer une transformation à un point +- Calculer l'inverse d'une matrice +- Créer des matrices de projection (orthographique) + +**Méthodes principales** : +- `identity(): Matrix3` - Matrice identité +- `translate(x: number, y: number): Matrix3` - Translation +- `rotate(angle: number): Matrix3` - Rotation +- `scale(x: number, y: number): Matrix3` - Scale +- `multiply(other: Matrix3): Matrix3` - Multiplication de matrices +- `transformPoint(point: Vector2): Vector2` - Applique la transformation +- `inverse(): Matrix3` - Matrice inverse +- `orthographic(left, right, bottom, top): Matrix3` - Projection orthographique + +**Méthodes statiques** : +- `Matrix3.identity()` - Matrice identité +- `Matrix3.translation(x, y)` - Matrice de translation +- `Matrix3.rotation(angle)` - Matrice de rotation +- `Matrix3.scaling(x, y)` - Matrice de scale + +**Composition de transformations** : +``` +// Pour transformer un objet avec translation, rotation, scale : +const matrix = Matrix3.identity() + .translate(position.x, position.y) + .rotate(rotation) + .scale(scale.x, scale.y); + +// Applique la transformation à un point +const transformedPoint = matrix.transformPoint(localPoint); +``` + +**Important** : L'ordre des opérations compte ! +``` +translate → rotate → scale ≠ rotate → translate → scale +``` + +**Projection orthographique** : +``` +// Convertit coordonnées pixel (0, 0) → (800, 600) +// En coordonnées NDC (-1, -1) → (1, 1) +const projection = Matrix3.orthographic(0, 800, 600, 0); +``` + +--- + +### 8.3 Rect (rectangle) + +**Rôle** : Représente un rectangle (pour bounds, collisions, UVs...). + +**Propriétés** : +- `x: number` - Position X +- `y: number` - Position Y +- `width: number` - Largeur +- `height: number` - Hauteur + +**Propriétés calculées** : +- `left: number` - Bord gauche (x) +- `right: number` - Bord droit (x + width) +- `top: number` - Bord haut (y) +- `bottom: number` - Bord bas (y + height) +- `center: Vector2` - Centre du rectangle +- `size: Vector2` - (width, height) + +**Méthodes principales** : +- `contains(point: Vector2): boolean` - Teste si un point est dedans +- `overlaps(other: Rect): boolean` - Teste si deux rectangles se chevauchent +- `intersection(other: Rect): Rect | null` - Calcule l'intersection +- `union(other: Rect): Rect` - Calcule l'union (plus petit rectangle contenant les deux) +- `expand(amount: number): Rect` - Agrandi le rectangle + +**Utilisation** : +``` +// Bounds d'un sprite +const bounds = new Rect( + transform.position.x - sprite.width / 2, + transform.position.y - sprite.height / 2, + sprite.width, + sprite.height +); + +// Test de collision simple +if (playerBounds.overlaps(enemyBounds)) { + // Collision ! +} + +// Source rect pour spritesheet (UV) +const sourceRect = new Rect(0, 0, 32, 32); // Sprite 32×32 en haut à gauche +``` + +--- + +### 8.4 Color (couleur RGBA) + +**Rôle** : Représente une couleur avec alpha (transparence). + +**Propriétés** : +- `r: number` - Rouge (0-1) +- `g: number` - Vert (0-1) +- `b: number` - Bleu (0-1) +- `a: number` - Alpha (0-1, 0 = transparent, 1 = opaque) + +**Méthodes principales** : +- `lerp(other: Color, t: number): Color` - Interpolation de couleurs +- `multiply(other: Color): Color` - Multiplication de couleurs +- `toHex(): string` - Convertit en hex (#RRGGBBAA) +- `toRGBA(): string` - Convertit en rgba(r, g, b, a) + +**Couleurs prédéfinies** : +- `Color.white` - (1, 1, 1, 1) +- `Color.black` - (0, 0, 0, 1) +- `Color.red` - (1, 0, 0, 1) +- `Color.green` - (0, 1, 0, 1) +- `Color.blue` - (0, 0, 1, 0) +- `Color.transparent` - (0, 0, 0, 0) + +**Utilisation** : +``` +// Tint rouge sur un sprite +spriteRenderer.tint = new Color(1, 0, 0, 1); + +// Fade in progressif +spriteRenderer.tint.a = 0; +// Chaque frame : +spriteRenderer.tint.a += deltaTime; // Fade in sur 1 seconde + +// Interpolation de couleur (ex: dégâts) +const damageColor = Color.red; +const normalColor = Color.white; +spriteRenderer.tint = normalColor.lerp(damageColor, damageFlash); +``` + +--- + +## 9. Optimisations avancées + +### 9.1 ObjectPool (réutilisation d'objets) + +**Rôle** : Évite de créer/détruire des objets en les réutilisant. + +**Problème** : +``` +// MAUVAIS : Crée un objet à chaque tir +function shoot() { + const bullet = new Bullet(); // Allocation mémoire + bullets.push(bullet); +} + +// Destruction +bullet.destroy(); // Garbage collection = pause du jeu ! +``` + +**Solution avec pool** : +``` +// BON : Réutilise les objets +function shoot() { + const bullet = bulletPool.get(); // Récupère un objet existant + bullet.reset(); // Réinitialise + bullet.active = true; +} + +// "Destruction" +bullet.active = false; +bulletPool.release(bullet); // Remet dans le pool +``` + +**Responsabilités** : +- Créer une réserve d'objets à l'avance +- Fournir des objets disponibles +- Reprendre les objets libérés +- Agrandir le pool si nécessaire + +**Propriétés importantes** : +- `pool: T[]` - Objets disponibles +- `factory: () => T` - Fonction pour créer de nouveaux objets +- `initialSize: number` - Taille initiale du pool +- `maxSize: number` - Taille max (optionnel) + +**Méthodes principales** : +- `get(): T` - Récupère un objet du pool +- `release(obj: T)` - Remet un objet dans le pool +- `clear()` - Vide le pool +- `prewarm(count: number)` - Pré-crée des objets + +**Gains de performance** : +- Jusqu'à 10× plus rapide pour les jeux avec beaucoup d'objets +- Élimine les pauses du garbage collector +- Réduit la fragmentation mémoire + +**Quand l'utiliser** : +- Projectiles (bullets, flèches...) +- Particules +- Ennemis (spawn/despawn fréquent) +- Effets visuels temporaires + +--- + +### 9.2 Quadtree (spatial partitioning) + +**Rôle** : Structure de données qui divise l'espace en zones pour optimiser les recherches. + +**Problème** : +``` +// Test de collision naïf : O(n²) +for (let i = 0; i < objects.length; i++) { + for (let j = i + 1; j < objects.length; j++) { + if (checkCollision(objects[i], objects[j])) { + // Collision + } + } +} +// 1000 objets = 499,500 tests ! +``` + +**Solution avec Quadtree** : +``` +// O(n log n) +quadtree.clear(); +for (const obj of objects) { + quadtree.insert(obj); +} + +for (const obj of objects) { + const nearby = quadtree.query(obj.bounds); + for (const other of nearby) { + if (checkCollision(obj, other)) { + // Collision + } + } +} +// 1000 objets = ~10,000 tests +``` + +**Structure** : +``` +Quadtree Node +├── bounds: Rect (zone couverte) +├── capacity: number (objets max avant subdivision) +├── objects: Object[] (objets dans ce nœud) +└── subdivisions: [NW, NE, SW, SE] (4 enfants) + +Division de l'espace : +┌─────────┬─────────┐ +│ NW │ NE │ +│ │ │ +├─────────┼─────────┤ +│ SW │ SE │ +│ │ │ +└─────────┴─────────┘ +``` + +**Responsabilités** : +- Insérer des objets +- Subdiviser quand un nœud est plein +- Requêter les objets dans une zone +- Se vider et se reconstruire chaque frame (pour objets dynamiques) + +**Propriétés importantes** : +- `bounds: Rect` - Zone couverte par ce nœud +- `capacity: number` - Nombre max d'objets avant subdivision +- `maxDepth: number` - Profondeur max de l'arbre +- `objects: Object[]` - Objets dans ce nœud +- `divided: boolean` - Si subdivisé ou non +- `children: QuadtreeNode[]` - 4 enfants (NW, NE, SW, SE) + +**Méthodes principales** : +- `insert(obj: Object): boolean` - Insère un objet +- `subdivide()` - Divise le nœud en 4 +- `query(range: Rect): Object[]` - Récupère tous les objets dans une zone +- `clear()` - Vide l'arbre + +**Algorithme d'insertion** : +``` +insert(obj): + 1. Si l'objet n'est pas dans les bounds → return false + 2. Si le nœud n'est pas plein et pas subdivisé → ajouter l'objet + 3. Si le nœud est plein → subdiviser + 4. Insérer l'objet dans les enfants appropriés +``` + +**Gains** : +- Collision : O(n²) → O(n log n) +- Recherche de voisins : O(n) → O(log n) +- Culling (objets hors caméra) : très rapide + +--- + +### 9.3 Spatial Hashing (alternative au Quadtree) + +**Rôle** : Division de l'espace en grille uniforme. + +**Différence avec Quadtree** : +- **Quadtree** : Adaptatif (plus de détails où il y a plus d'objets) +- **Spatial Hash** : Grille fixe (plus simple, parfois plus rapide) + +**Responsabilités** : +- Diviser l'espace en cellules de taille fixe +- Hacher la position d'un objet pour trouver sa cellule +- Récupérer rapidement tous les objets d'une cellule + +**Structure** : +``` +Grid de cellules : +┌───┬───┬───┬───┐ +│ 0 │ 1 │ 2 │ 3 │ +├───┼───┼───┼───┤ +│ 4 │ 5 │ 6 │ 7 │ +├───┼───┼───┼───┤ +│ 8 │ 9 │10 │11 │ +└───┴───┴───┴───┘ + +Chaque cellule contient une liste d'objets +``` + +**Propriétés importantes** : +- `cellSize: number` - Taille d'une cellule +- `cells: Map` - Grille (hash → objets) + +**Méthodes principales** : +- `insert(obj: Object)` - Insère un objet +- `remove(obj: Object)` - Retire un objet +- `query(bounds: Rect): Object[]` - Récupère objets dans une zone +- `hash(x: number, y: number): number` - Calcule l'index de cellule +- `clear()` - Vide la grille + +**Fonction de hachage** : +``` +hash(x: number, y: number): number { + const cellX = Math.floor(x / this.cellSize); + const cellY = Math.floor(y / this.cellSize); + return cellX + cellY * 10000; // Combinaison unique +} +``` + +**Quand utiliser Spatial Hash vs Quadtree** : +- **Spatial Hash** : Objets uniformément répartis, taille similaire +- **Quadtree** : Objets concentrés en zones, tailles variées + +--- + +### 9.4 Dirty Flags (recalcul conditionnel) + +**Rôle** : Ne recalculer que ce qui a changé. + +**Principe** : +``` +class Transform { + private _position = new Vector2(0, 0); + private _dirty = false; + private _cachedWorldMatrix: Matrix3; + + set position(value: Vector2) { + this._position = value; + this._dirty = true; // Marque "besoin de recalculer" + this.propagateDirtyToChildren(); // Propage aux enfants + } + + getWorldMatrix(): Matrix3 { + if (this._dirty) { + // Recalcule seulement si nécessaire + this._cachedWorldMatrix = this.calculateWorldMatrix(); + this._dirty = false; + } + return this._cachedWorldMatrix; + } +} +``` + +**Où utiliser** : +- **Transform** : Matrice de transformation +- **Bounds** : Rectangle englobant +- **Mesh** : Données géométriques +- **Render** : Batch de sprites + +**Gains** : +- Économise 80-90% des calculs pour objets statiques +- Réduit la charge CPU significativement + +--- + +### 9.5 Canvas Layering (multi-canvas) + +**Rôle** : Séparer les éléments statiques et dynamiques sur plusieurs canvas. + +**Principe** : +``` + + + +``` + +**Avantages** : +- Arrière-plan statique : dessiné 1 fois au lieu de 60 fois/seconde +- UI : dessinée seulement quand elle change +- Performance : jusqu'à 3× plus rapide + +**Inconvénients** : +- Plus complexe à gérer +- Moins utile avec WebGL (qui est déjà très rapide) + +**Quand utiliser** : +- Jeux avec beaucoup de décors statiques +- UI complexe qui change rarement +- Performance critique sur appareils faibles + +--- + +### 9.6 Frustum Culling (ne rendre que le visible) + +**Rôle** : Ne dessiner que les objets visibles par la caméra. + +**Principe** : +``` +render(scene: Scene, camera: Camera) { + const frustum = camera.getViewFrustum(); // Zone visible + + for (const obj of scene.objects) { + // Test si l'objet est visible + if (!frustum.contains(obj.bounds)) { + continue; // Skip les objets hors écran + } + + // Rend seulement les objets visibles + obj.render(spriteBatch); + } +} +``` + +**Gains** : +- Monde 10000×10000 pixels, caméra 800×600 +- Sans culling : dessine 1000 objets +- Avec culling : dessine ~50 objets +- 20× plus rapide ! + +**Méthodes** : +- **AABB Test** : Teste si le bounds de l'objet intersecte le frustum +- **Sphere Test** : Teste avec un cercle englobant (plus rapide, moins précis) + +--- + +### 9.7 Level of Detail (LOD) + +**Rôle** : Utiliser des versions simplifiées pour les objets lointains. + +**Principe** : +``` +const distanceToCamera = Vector2.distance(obj.position, camera.position); + +if (distanceToCamera < 200) { + obj.render(highDetailSprite); // Proche : haute qualité +} else if (distanceToCamera < 500) { + obj.render(mediumDetailSprite); // Moyen : qualité moyenne +} else { + obj.render(lowDetailSprite); // Loin : basse qualité +} +``` + +**Applications 2D** : +- Animations : moins de frames pour objets lointains +- Particules : moins de particules pour effets lointains +- Détails : masquer petits détails sur objets lointains + +--- + +## 10. Structure de fichiers complète +``` +project-root/ +│ +├── src/ +│ │ +│ ├── core/ +│ │ ├── Engine.ts // Point d'entrée, orchestrateur +│ │ ├── GameLoop.ts // Boucle principale +│ │ ├── Scene.ts // Scène de jeu +│ │ ├── SceneManager.ts // Gestion des scènes +│ │ └── Time.ts // Gestion du temps +│ │ +│ ├── rendering/ +│ │ ├── WebGLRenderer.ts // Coordonnateur du rendu +│ │ ├── SpriteBatch.ts // Batch rendering (★ crucial) +│ │ ├── Shader.ts // Wrapper de shader +│ │ ├── Material.ts // Shader + uniforms +│ │ ├── Texture.ts // Gestion des textures +│ │ ├── Camera.ts // Caméra 2D +│ │ ├── RenderQueue.ts // File de rendu triée +│ │ └── Layer.ts // Système de layers +│ │ +│ ├── webgl/ +│ │ ├── GLContext.ts // Abstraction WebGL +│ │ ├── Buffer.ts // Vertex/Index buffers +│ │ ├── VertexArray.ts // VAO +│ │ ├── Framebuffer.ts // Render to texture +│ │ └── GLUtils.ts // Utilitaires WebGL +│ │ +│ ├── entities/ +│ │ ├── GameObject.ts // Entité de base +│ │ ├── Transform.ts // Position, rotation, scale +│ │ └── Component.ts // Classe de base des composants +│ │ +│ ├── components/ +│ │ ├── SpriteRenderer.ts // Affiche un sprite +│ │ ├── Animator.ts // Gère les animations +│ │ ├── RigidBody.ts // Physique +│ │ ├── BoxCollider.ts // Collision rectangle +│ │ ├── CircleCollider.ts // Collision cercle +│ │ ├── PolygonCollider.ts // Collision polygone +│ │ ├── ParticleEmitter.ts // Système de particules +│ │ ├── TilemapRenderer.ts // Affichage tilemap +│ │ ├── AudioSource.ts // Source audio +│ │ └── Camera2D.ts // Component caméra +│ │ +│ ├── systems/ +│ │ ├── PhysicsSystem.ts // Mise à jour physique +│ │ ├── CollisionSystem.ts // Détection/résolution +│ │ ├── AnimationSystem.ts // Mise à jour animations +│ │ └── ParticleSystem.ts // Mise à jour particules +│ │ +│ ├── input/ +│ │ ├── InputManager.ts // Gestion entrées +│ │ ├── Keyboard.ts // Clavier +│ │ ├── Mouse.ts // Souris +│ │ └── Touch.ts // Tactile +│ │ +│ ├── audio/ +│ │ ├── AudioManager.ts // Gestion audio +│ │ ├── AudioSource.ts // Source audio +│ │ └── AudioListener.ts // Écouteur audio +│ │ +│ ├── assets/ +│ │ ├── AssetLoader.ts // Chargement ressources +│ │ ├── TextureAtlas.ts // Atlas de textures +│ │ └── SpriteSheet.ts // Spritesheet +│ │ +│ ├── math/ +│ │ ├── Vector2.ts // Vecteur 2D +│ │ ├── Vector3.ts // Vecteur 3D (pour couleurs RGB) +│ │ ├── Matrix3.ts // Matrice 3×3 +│ │ ├── Rect.ts // Rectangle +│ │ ├── Circle.ts // Cercle +│ │ ├── Color.ts // Couleur RGBA +│ │ ├── MathUtils.ts // Utilitaires mathématiques +│ │ └── Easing.ts // Fonctions d'easing +│ │ +│ ├── utils/ +│ │ ├── ObjectPool.ts // Pool d'objets +│ │ ├── EventBus.ts // Système d'événements +│ │ ├── Quadtree.ts // Spatial partitioning +│ │ ├── SpatialHash.ts // Grille spatiale +│ │ ├── Debug.ts // Outils de debug +│ │ └── Logger.ts // Logging +│ │ +│ ├── physics/ +│ │ ├── Physics.ts // Constantes physiques +│ │ ├── Collision.ts // Détection de collision +│ │ ├── CollisionInfo.ts // Info de collision +│ │ └── Manifold.ts // Manifold de collision +│ │ +│ ├── animation/ +│ │ ├── Animation.ts // Données d'animation +│ │ ├── AnimationClip.ts // Clip d'animation +│ │ └── Keyframe.ts // Keyframe +│ │ +│ ├── shaders/ +│ │ ├── sprite.vert.glsl // Vertex shader sprite +│ │ ├── sprite.frag.glsl // Fragment shader sprite +│ │ ├── particle.vert.glsl // Vertex shader particule +│ │ ├── particle.frag.glsl // Fragment shader particule +│ │ └── postprocess.frag.glsl // Post-processing +│ │ +│ └── index.ts // Point d'entrée TypeScript +│ +├── assets/ +│ ├── images/ // Images du jeu +│ ├── sounds/ // Sons et musique +│ ├── data/ // JSON, XML, etc. +│ └── fonts/ // Polices (si besoin) +│ +├── examples/ // Exemples d'utilisation +│ ├── basic-sprite/ +│ ├── platformer/ +│ └── particle-effects/ +│ +├── dist/ // Build de production +│ +├── tsconfig.json // Configuration TypeScript +├── package.json // Dépendances npm +└── README.md // Documentation +``` + +--- + +## 11. Flow d'exécution complet + +### 11.1 Démarrage du moteur +``` +1. index.html charge le script + ↓ +2. Création de l'Engine + const engine = new Engine(800, 600); + ↓ +3. Engine.initialize() + - Crée le canvas WebGL + - Initialise WebGLRenderer + - Initialise InputManager + - Initialise AudioManager + - Initialise AssetLoader + - Crée SceneManager + - Crée GameLoop + ↓ +4. Chargement des assets + await assetLoader.loadAll(manifest); + ↓ +5. Création de la première scène + const mainScene = new MainScene(); + sceneManager.registerScene("main", mainScene); + ↓ +6. Chargement de la scène + sceneManager.loadScene("main"); + ↓ +7. Démarrage de la GameLoop + engine.start(); + ↓ +8. La boucle tourne ! +``` + +--- + +### 11.2 Une frame complète (60 FPS) +``` +requestAnimationFrame(timestamp) + ↓ +1. Calcul du deltaTime + deltaTime = (timestamp - lastTimestamp) / 1000 + Time.deltaTime = deltaTime + ↓ +2. Input Update + inputManager.update() + - Reset des états "down" et "up" + ↓ +3. Fixed Update (physique) + accumulator += deltaTime + while (accumulator >= fixedDeltaTime) { + physicsSystem.fixedUpdate(fixedDeltaTime) + - Applique gravité + - Intègre vélocités + accumulator -= fixedDeltaTime + } + ↓ +4. Update (logique) + scene.update(deltaTime) + - Pour chaque GameObject actif: + - Pour chaque Component actif: + - component.update(deltaTime) + ↓ +5. Collision Detection + collisionSystem.update() + - Reconstruit le Quadtree + - Détecte les collisions + - Résout les collisions + - Déclenche les callbacks + ↓ +6. Animation Update + animationSystem.update(deltaTime) + - Avance les animations + - Change les sprites + ↓ +7. Camera Update + camera.follow(player) // Si suivi activé + camera.update(deltaTime) + ↓ +8. Render + renderer.render(scene, camera) + + 8.1. Clear + gl.clear(COLOR_BUFFER_BIT) + + 8.2. Collecte des renderables + renderables = scene.getAllRenderables() + + 8.3. Frustum culling + visibleObjects = cullObjects(renderables, camera.frustum) + + 8.4. Soumission à la RenderQueue + for (obj of visibleObjects) { + renderQueue.submit(obj) + } + + 8.5. Tri de la RenderQueue + renderQueue.sort() + - Par layer + - Par shader + - Par texture + + 8.6. Begin SpriteBatch + spriteBatch.begin(camera.projection, camera.view) + + 8.7. Rendu + currentShader = null + currentTexture = null + + for (renderable of renderQueue) { + // Change shader si nécessaire + if (renderable.shader !== currentShader) { + spriteBatch.flush() + renderable.shader.use() + currentShader = renderable.shader + } + + // Ajoute au batch + renderable.render(spriteBatch) + } + + 8.8. End SpriteBatch + spriteBatch.end() // Flush final + + 8.9. Post-processing (optionnel) + postProcessing.render(frameTexture) + + 8.10. Debug Draw (si activé) + debugRenderer.drawColliders() + debugRenderer.drawFPS() + ↓ +9. Audio Update + audioManager.update(deltaTime) + - Update 3D audio positions + ↓ +10. Cleanup + scene.cleanupDestroyedObjects() + renderQueue.clear() + ↓ +11. Prochaine frame + requestAnimationFrame(nextFrame) +``` + +--- + +### 11.3 Ajout d'un GameObject à la scène +``` +const player = new GameObject("Player"); + ↓ +1. Ajout de composants + const transform = player.transform; // Existe déjà + transform.position = new Vector2(400, 300); + + const sprite = player.addComponent(new SpriteRenderer()); + sprite.texture = assetLoader.getTexture("player"); + + const rigidBody = player.addComponent(new RigidBody()); + rigidBody.mass = 1; + + const collider = player.addComponent(new BoxCollider()); + collider.size = new Vector2(32, 48); + ↓ +2. Ajout à la scène + scene.addGameObject(player); + ↓ +3. Cycle de vie + player.awake() + - Appelle awake() sur tous les composants + ↓ +4. Premier update + player.start() + - Appelle start() sur tous les composants + ↓ +5. Enregistrement aux systèmes + - physicsSystem.register(rigidBody) + - collisionSystem.register(collider) + - renderQueue submit automatique chaque frame + ↓ +6. Updates réguliers + Chaque frame: + - rigidBody.update(deltaTime) + - sprite.render(spriteBatch) + +11.4 Gestion d'une collision +1. CollisionSystem détecte un overlap + collider1.overlaps(collider2) = true + ↓ +2. Calcul de l'info de collision + info = calculateCollisionInfo(collider1, collider2) + - normal : direction de séparation + - penetration : profondeur de pénétration + - contactPoint : point de contact + ↓ +3. Résolution physique (si solid) + if (!collider1.isTrigger && !collider2.isTrigger) { + resolveCollision(info) + - Sépare les objets + - Applique impulse (rebond) + } + ↓ +4. Callbacks + collider1.onCollisionEnter(collider2) + collider2.onCollisionEnter(collider1) + + Ou si trigger: + collider1.onTriggerEnter(collider2) + ↓ +5. Dans le script du joueur + onCollisionEnter(other: Collider) { + if (other.gameObject.tag === "Enemy") { + this.takeDamage(10); + }RéessayerSContinuer if (other.gameObject.tag === "Coin") { + this.collectCoin(); + other.gameObject.destroy(); + } + } +``` + +--- + +### 11.5 Changement de scène +``` +1. Appel du changement + sceneManager.loadScene("Level2"); + ↓ +2. Déchargement de la scène actuelle + if (currentScene) { + currentScene.onUnload() + - Appelle onDestroy() sur tous les GameObjects + - Désenregistre tous les systèmes + - Libère les ressources temporaires + } + ↓ +3. Chargement de la nouvelle scène + const newScene = scenes.get("Level2"); + ↓ +4. Initialisation de la scène + newScene.onLoad() + - Crée les GameObjects + - Configure la caméra + - Charge les assets spécifiques + ↓ +5. Activation + currentScene = newScene; + ↓ +6. Premier update de la nouvelle scène + - Tous les GameObjects awake() + - Tous les GameObjects start() + ↓ +7. La scène est active + Updates et rendu normaux +``` + +--- + +## 12. Extensions possibles + +### 12.1 Post-Processing + +**Rôle** : Appliquer des effets visuels sur l'image finale. + +**Exemples d'effets** : +- **Bloom** : Glow lumineux +- **Blur** : Flou +- **Vignette** : Assombrissement des bords +- **Color Grading** : Correction de couleur +- **Chromatic Aberration** : Distorsion des couleurs +- **CRT Effect** : Effet écran cathodique +- **Pixelate** : Pixelisation + +**Architecture** : +``` +PostProcessingStack +├── effects: PostEffect[] +└── framebuffers: Framebuffer[] + +PostEffect (classe de base) +├── shader: Shader +└── render(source: Texture, target: Framebuffer) +``` + +**Pipeline** : +``` +1. Rend la scène dans un Framebuffer (pas l'écran) + ↓ +2. Pour chaque effet : + - Applique l'effet + - Source : texture précédente + - Target : nouveau framebuffer + ↓ +3. Rend le résultat final à l'écran +``` + +**Exemple - Effet Bloom** : +``` +1. Rend la scène +2. Extract bright pixels (threshold) +3. Blur horizontal +4. Blur vertical +5. Combine avec l'image originale +``` + +--- + +### 12.2 Lighting 2D + +**Rôle** : Système d'éclairage dynamique 2D. + +**Types de lumières** : +- **Point Light** : Lumière omnidirectionnelle (torche, lampe) +- **Spot Light** : Lumière conique (lampe de poche) +- **Directional Light** : Lumière directionnelle (soleil) +- **Ambient Light** : Lumière ambiante globale + +**Architecture** : +``` +LightingSystem +├── lights: Light[] +├── lightBuffer: Framebuffer (texture des lumières) +└── normalMap: Texture (pour normal mapping) + +Light (classe de base) +├── position: Vector2 +├── color: Color +├── intensity: number +└── radius: number +``` + +**Pipeline** : +``` +1. Rend la scène normalement + ↓ +2. Rend les lumières dans un framebuffer séparé + - Clear en noir (pas de lumière) + - Additive blending + - Chaque lumière dessine son influence + ↓ +3. Combine scène + lumières + - Multiplie les couleurs + - fragment = sceneColor * lightColor +Fragment Shader pour lumière : +glsl// Calcul de l'atténuation +float distance = length(lightPosition - fragPosition); +float attenuation = 1.0 / (1.0 + distance * distance); + +// Couleur finale +vec3 lightContribution = lightColor * intensity * attenuation; +gl_FragColor = vec4(lightContribution, 1.0); + +12.3 Normal Mapping +Rôle : Ajouter des détails d'éclairage sans géométrie supplémentaire. +Principe : + +Une normal map stocke des normales de surface dans une texture +Permet de simuler du relief +La lumière réagit comme si la surface avait du relief + +Texture normale : + +R = Normal X (-1 à 1 encodé en 0-1) +G = Normal Y +B = Normal Z (toujours positif en 2D) + +Fragment Shader : +glsl// Échantillonne la normal map +vec3 normal = texture2D(normalMap, vUV).rgb; +normal = normalize(normal * 2.0 - 1.0); // Décode 0-1 → -1 à 1 + +// Calcul de la lumière +vec3 lightDir = normalize(lightPosition - fragPosition); +float diffuse = max(dot(normal, lightDir), 0.0); + +// Couleur finale +vec3 color = baseColor * diffuse * lightColor; +``` + +--- + +### 12.4 Particle System GPU + +**Rôle** : Système de particules calculé entièrement sur le GPU. + +**Avantages** : +- 100 000+ particules sans problème +- Pas de transfert CPU → GPU chaque frame +- Mise à jour parallèle massive + +**Architecture** : +``` +GPUParticleSystem +├── particleBuffer: Buffer (positions, vélocités, etc.) +├── computeShader: Shader (calcul sur GPU, WebGL 2) +└── renderShader: Shader (rendu instanced) +``` + +**WebGL 2 - Transform Feedback** : +``` +// Les particules se mettent à jour sur le GPU +1. Vertex shader lit les données de particule +2. Applique la physique (gravité, forces) +3. Transform feedback écrit les nouvelles données +4. Pas besoin de les renvoyer au CPU ! +``` + +**Rendu instanced** : +``` +// Dessine toutes les particules en 1 draw call +gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, particleCount); +// 6 vertices pour un quad, répété particleCount fois +``` + +--- + +### 12.5 Sprite Batching avancé + +**Optimisations supplémentaires** : + +#### A. Multi-Texture Batching +``` +// Au lieu de flusher à chaque changement de texture, +// utiliser un texture array et passer l'index + +Fragment Shader: +uniform sampler2D textures[8]; // 8 textures en même temps +varying float textureIndex; + +void main() { + // Sélectionne la bonne texture + vec4 color; + if (textureIndex < 0.5) color = texture2D(textures[0], vUV); + else if (textureIndex < 1.5) color = texture2D(textures[1], vUV); + // etc... +} +``` + +#### B. Texture Atlas +``` +// Combine plusieurs textures en une seule grande texture +// Avantage : 1 seule texture pour tout le jeu ! + +Atlas 2048×2048: +┌─────┬─────┬─────┬─────┐ +│ Hero│Enemy│Coin │ ... │ +├─────┼─────┼─────┼─────┤ +│Tile1│Tile2│ ... │ ... │ +└─────┴─────┴─────┴─────┘ + +// Les UVs sont ajustés pour pointer vers la bonne région +``` + +#### C. Instanced Rendering +``` +// Dessine beaucoup d'objets identiques très rapidement +gl.drawElementsInstanced( + gl.TRIANGLES, + 6, // 6 indices par sprite + gl.UNSIGNED_SHORT, + 0, + spriteCount // Nombre d'instances +); + +// Vertex shader reçoit un attribut "instanceID" +// qui permet de différencier chaque instance +``` + +--- + +### 12.6 Tilemap Chunking + +**Rôle** : Optimiser les très grandes tilemaps. + +**Problème** : +``` +Tilemap 1000×1000 = 1 million de tiles +Même avec culling, trop de draw calls +``` + +**Solution - Chunking** : +``` +Diviser la tilemap en chunks 16×16 + +Chunk: +┌────────────────┐ +│ 16×16 tiles │ +│ = 1 draw call │ +└────────────────┘ + +1000×1000 tiles = 62×62 chunks +Seuls les chunks visibles sont rendus (~9 chunks) +``` + +**Architecture** : +``` +TilemapChunk +├── tiles: number[] (16×16 = 256 tiles) +├── vertexBuffer: Buffer (pré-calculé) +├── dirty: boolean +└── bounds: Rect + +Tilemap +├── chunks: TilemapChunk[][] +└── chunkSize: number +``` + +**Optimisation** : +``` +// Les chunks sont pré-bakés en buffers GPU +// Quand un tile change : +chunk.dirty = true; + +// Au rendu, si dirty : +chunk.rebuildMesh(); +chunk.uploadToGPU(); +chunk.dirty = false; +``` + +--- + +### 12.7 Tweening / Animation System + +**Rôle** : Animer des propriétés facilement (position, scale, couleur...). + +**Exemples** : +``` +// Déplace l'objet vers (100, 100) en 2 secondes +Tween.to(obj.transform.position, { x: 100, y: 100 }, 2.0) + .easing(Easing.easeOutQuad) + .onComplete(() => console.log("Arrived!")); + +// Scale pulse +Tween.to(obj.transform.scale, { x: 1.5, y: 1.5 }, 0.5) + .yoyo(true) + .repeat(Infinity); + +// Fade out +Tween.to(sprite.tint, { a: 0 }, 1.0); +``` + +**Architecture** : +``` +TweenManager +├── tweens: Tween[] +└── update(deltaTime) + +Tween +├── target: any (objet à animer) +├── properties: string[] (propriétés à animer) +├── from: number[] (valeurs de départ) +├── to: number[] (valeurs d'arrivée) +├── duration: number +├── elapsed: number +├── easing: EasingFunction +├── yoyo: boolean +├── repeat: number +└── callbacks: { onUpdate, onComplete } +``` + +**Fonctions d'easing** : +``` +Linear, EaseInQuad, EaseOutQuad, EaseInOutQuad, +EaseInCubic, EaseOutCubic, EaseInOutCubic, +EaseInElastic, EaseOutElastic, EaseInBounce, EaseOutBounce +``` + +--- + +### 12.8 State Machine + +**Rôle** : Gérer les états d'un personnage/ennemi (idle, walk, jump, attack...). + +**Architecture** : +``` +StateMachine +├── currentState: State +├── states: Map +└── transition(stateName: string) + +State (classe de base) +├── onEnter() +├── onExit() +├── update(deltaTime) +└── handleInput(input) +``` + +**Exemple - État du joueur** : +``` +class IdleState extends State { + update(deltaTime) { + if (input.isKeyDown("Space")) { + stateMachine.transition("jump"); + } + if (input.isKeyHeld("ArrowRight")) { + stateMachine.transition("walk"); + } + } +} + +class WalkState extends State { + update(deltaTime) { + player.moveRight(); + + if (!input.isKeyHeld("ArrowRight")) { + stateMachine.transition("idle"); + } + if (input.isKeyDown("Space")) { + stateMachine.transition("jump"); + } + } +} + +class JumpState extends State { + onEnter() { + player.rigidBody.addImpulse(new Vector2(0, -500)); + } + + update(deltaTime) { + if (player.isGrounded()) { + stateMachine.transition("idle"); + } + } +} +``` + +--- + +### 12.9 Pathfinding (A*) + +**Rôle** : Trouver le chemin le plus court entre deux points. + +**Algorithme A*** : +``` +1. Open list : nœuds à explorer +2. Closed list : nœuds déjà explorés +3. Pour chaque nœud : + - f = g + h + - g : coût depuis le départ + - h : heuristique vers l'arrivée +4. Explore le nœud avec le plus petit f +5. Reconstruit le chemin à la fin +``` + +**Architecture** : +``` +Pathfinder +├── grid: Node[][] (grille de nœuds) +├── findPath(start: Vector2, goal: Vector2): Vector2[] +└── heuristic(a: Node, b: Node): number + +Node +├── position: Vector2 +├── walkable: boolean +├── g: number (cost from start) +├── h: number (heuristic to goal) +├── f: number (g + h) +└── parent: Node +``` + +**Utilisation** : +``` +// Trouve le chemin +const path = pathfinder.findPath( + enemy.position, + player.position +); + +// Suit le chemin +if (path.length > 0) { + const nextPoint = path[0]; + enemy.moveTowards(nextPoint); + + if (Vector2.distance(enemy.position, nextPoint) < 5) { + path.shift(); // Passe au point suivant + } +} +``` + +--- + +### 12.10 Save System + +**Rôle** : Sauvegarder et charger la progression du joueur. + +**Architecture** : +``` +SaveSystem +├── save(key: string, data: any) +├── load(key: string): any +├── delete(key: string) +└── exists(key: string): boolean +``` + +**Implémentation avec localStorage** : +``` +class SaveSystem { + save(key: string, data: any): void { + const json = JSON.stringify(data); + localStorage.setItem(key, json); + } + + load(key: string): any { + const json = localStorage.getItem(key); + return json ? JSON.parse(json) : null; + } +} + +// Utilisation +const saveData = { + level: 5, + health: 80, + coins: 150, + position: { x: 400, y: 300 }, + inventory: ["sword", "shield", "potion"] +}; + +saveSystem.save("player_progress", saveData); + +// Chargement +const loaded = saveSystem.load("player_progress"); +if (loaded) { + player.level = loaded.level; + player.health = loaded.health; + player.position = new Vector2(loaded.position.x, loaded.position.y); +} +``` + +--- + +### 12.11 UI System + +**Rôle** : Système d'interface utilisateur (menus, HUD, dialogues...). + +**Architecture** : +``` +UIManager +├── canvas: UICanvas +├── elements: UIElement[] +└── render() + +UIElement (classe de base) +├── position: Vector2 +├── size: Vector2 +├── visible: boolean +├── render(context) +└── handleInput(input) + +Types d'éléments : +├── UIButton +├── UIText +├── UIImage +├── UIPanel +├── UISlider +├── UIProgressBar +└── UIDialog +``` + +**Système d'événements UI** : +``` +button.onClick = () => { + console.log("Button clicked!"); +}; + +button.onHover = () => { + button.color = Color.yellow; +}; +``` + +**Layouts** : +``` +UIPanel (container) +├── layout: Layout (Horizontal, Vertical, Grid) +└── children: UIElement[] + +// Auto-arrange les éléments enfants +panel.layout = new VerticalLayout({ spacing: 10 }); +``` + +--- + +### 12.12 Dialogue System + +**Rôle** : Gérer les dialogues avec les NPCs. + +**Architecture** : +``` +DialogueManager +├── currentDialogue: Dialogue +├── showDialogue(dialogue: Dialogue) +└── advance() + +Dialogue +├── lines: DialogueLine[] +├── currentLine: number +└── speaker: string + +DialogueLine +├── text: string +├── speaker: string +├── choices: Choice[] (pour branching) +└── onComplete: () => void +Format de dialogue : +json{ + "id": "quest_start", + "lines": [ + { + "speaker": "Old Man", + "text": "Greetings, traveler!" + }, + { + "speaker": "Old Man", + "text": "I need your help to find my lost cat.", + "choices": [ + { "text": "Sure, I'll help!", "next": "quest_accept" }, + { "text": "Not interested.", "next": "quest_decline" } + ] + } + ] +} +``` + +--- + +### 12.13 Cutscene System + +**Rôle** : Séquences scriptées (cinématiques). + +**Architecture** : +``` +CutsceneManager +├── currentCutscene: Cutscene +├── play(cutscene: Cutscene) +└── update(deltaTime) + +Cutscene +├── timeline: CutsceneAction[] +├── currentTime: number +└── duration: number + +CutsceneAction +├── startTime: number +├── duration: number +└── execute() + +Types d'actions : +├── MoveAction (déplace un objet) +├── DialogueAction (affiche un dialogue) +├── CameraAction (déplace la caméra) +├── AnimationAction (joue une animation) +└── SoundAction (joue un son) +``` + +**Exemple** : +``` +const cutscene = new Cutscene([ + new CameraAction(0, 2, camera, new Vector2(500, 300)), // 0-2s + new DialogueAction(2, 3, "Hello!"), // 2-5s + new MoveAction(3, 2, player, new Vector2(600, 300)), // 3-5s + new SoundAction(5, 0.1, "explosion") // 5s +]); + +cutsceneManager.play(cutscene); +``` + +--- + +### 12.14 Multiplayer (WebSockets) + +**Rôle** : Synchronisation réseau pour le multijoueur. + +**Architecture** : +``` +NetworkManager +├── socket: WebSocket +├── localPlayer: GameObject +├── remotePlayers: Map +├── send(message: any) +└── onMessage(callback) + +NetworkedObject +├── networkId: string +├── owner: string +└── sync(data: any) +``` + +**Messages réseau** : +``` +// Connexion +{ type: "connect", playerId: "abc123" } + +// Position update +{ type: "position", playerId: "abc123", x: 100, y: 200 } + +// Action +{ type: "shoot", playerId: "abc123", direction: { x: 1, y: 0 } } +``` + +**Interpolation** : +``` +// Smooth les mouvements des autres joueurs +remotePlayer.targetPosition = receivedPosition; + +update(deltaTime) { + // Interpole vers la position reçue + remotePlayer.position = remotePlayer.position.lerp( + remotePlayer.targetPosition, + 0.1 + ); +} + +13. Patterns de code recommandés +13.1 Singleton (pour managers) +typescriptclass InputManager { + private static instance: InputManager; + + private constructor() {} + + static getInstance(): InputManager { + if (!this.instance) { + this.instance = new InputManager(); + } + return this.instance; + } +} + +// Utilisation +const input = InputManager.getInstance(); + +13.2 Factory (pour création d'objets) +typescriptclass GameObjectFactory { + static createPlayer(position: Vector2): GameObject { + const player = new GameObject("Player"); + player.transform.position = position; + + const sprite = player.addComponent(new SpriteRenderer()); + sprite.texture = AssetLoader.getTexture("player"); + + const rb = player.addComponent(new RigidBody()); + rb.mass = 1; + + const collider = player.addComponent(new BoxCollider()); + collider.size = new Vector2(32, 48); + + return player; + } + + static createEnemy(position: Vector2, type: string): GameObject { + // ... + } +} + +// Utilisation +const player = GameObjectFactory.createPlayer(new Vector2(100, 100)); +scene.addGameObject(player); + +13.3 Observer (pour événements) +typescriptclass EventBus { + private listeners = new Map>(); + + on(event: string, callback: Function): void { + if (!this.listeners.has(event)) { + this.listeners.set(event, new Set()); + } + this.listeners.get(event)!.add(callback); + } + + off(event: string, callback: Function): void { + this.listeners.get(event)?.delete(callback); + } + + emit(event: string, data?: any): void { + this.listeners.get(event)?.forEach(cb => cb(data)); + } +} + +// Utilisation +eventBus.on("player:died", (data) => { + console.log("Game Over!"); + sceneManager.loadScene("GameOver"); +}); + +eventBus.emit("player:died", { score: 1500 }); + +13.4 Command (pour actions annulables) +typescriptinterface Command { + execute(): void; + undo(): void; +} + +class MoveCommand implements Command { + constructor( + private obj: GameObject, + private delta: Vector2 + ) {} + + execute(): void { + this.obj.transform.position.add(this.delta); + } + + undo(): void { + this.obj.transform.position.subtract(this.delta); + } +} + +class CommandManager { + private history: Command[] = []; + private current = -1; + + execute(command: Command): void { + command.execute(); + this.history = this.history.slice(0, this.current + 1); + this.history.push(command); + this.current++; + } + + undo(): void { + if (this.current >= 0) { + this.history[this.current].undo(); + this.current--; + } + } + + redo(): void { + if (this.current < this.history.length - 1) { + this.current++; + this.history[this.current].execute(); + } + } +} + +14. Checklist de développement +Phase 1 : Core (2-3 semaines) + + Structure de fichiers + TypeScript setup (tsconfig.json) + Classe Engine de base + GameLoop avec requestAnimationFrame + Time management (deltaTime) + Scene et SceneManager + Math (Vector2, Matrix3, Rect, Color) + +Phase 2 : WebGL Foundation (2-3 semaines) + + GLContext wrapper + Buffer (Vertex et Index) + Shader compilation et linking + Texture loading + Shader sprite de base (vertex + fragment) + Tests de rendu simple (triangle, quad) + +Phase 3 : Rendering System (3-4 semaines) + + WebGLRenderer + SpriteBatch (★ crucial) + Camera 2D avec matrices + Material system + RenderQueue avec tri + Frustum culling + Tests de performance (10000 sprites) + +Phase 4 : Entity Component System (2 semaines) + + GameObject + Transform avec hiérarchie + Component de base + SpriteRenderer component + Animator component + Tests d'intégration + +Phase 5 : Input et Audio (1 semaine) + + InputManager (clavier, souris) + Touch support + AudioManager + Tests sur différents navigateurs + +Phase 6 : Physics (2-3 semaines) + + RigidBody component + Collider (Box, Circle) + PhysicsSystem + CollisionSystem + Quadtree pour optimisation + Tests de performance + +Phase 7 : Assets et Utilities (1-2 semaines) + + AssetLoader + ObjectPool + EventBus + Debug utilities + Logger + +Phase 8 : Optimisations (1-2 semaines) + + Profiling tools + Dirty flags sur Transform + Batch optimization avancée + Memory profiling + +Phase 9 : Extensions (optionnel) + + Post-processing + Lighting 2D + Particle system GPU + UI System + Dialogue system + Save system + +Phase 10 : Documentation et Tests (1-2 semaines) + + Documentation API complète + Exemples d'utilisation + Tests unitaires + Tutoriels + + +15. Conseils finaux +Performance + +Profilez tôt et souvent avec performance.mark() et performance.measure() +Mesurez avant d'optimiser - Ne devinez pas où sont les bottlenecks +Le SpriteBatch est crucial - C'est là que 90% des gains se font +Object pooling pour tout ce qui spawn/despawn fréquemment +Quadtree seulement si vous avez beaucoup d'objets (>500) + +Code Quality + +TypeScript strict mode activé dès le début +Interfaces pour tout (IRenderable, IUpdatable, ICollidable...) +Composition > Héritage - ECS pattern +Single Responsibility Principle - Une classe = une responsabilité +Don't Repeat Yourself - Factorisez le code répété + +Debugging + +Console.log n'est pas suffisant - Utilisez le debugger +Visual debugging : Dessinez les colliders, le Quadtree, les forces... +Stats en temps réel : FPS, draw calls, objets actifs +Assertions : Vérifiez vos hypothèses +Hot reload : Configurez un bundler avec watch mode + +Architecture + +Commencez simple, ajoutez de la complexité seulement si nécessaire +Testez chaque système indépendamment avant de les intégrer +Gardez les systèmes découplés via interfaces et événements +Documentez les décisions importantes dans le code +Faites des exemples pour chaque feature + +WebGL Spécifique + +Minimisez les state changes (shader, texture, blending) +Batch, batch, batch - C'est la clé +N'oubliez pas de bind/unbind les buffers et textures +Gérez les erreurs WebGL avec gl.getError() +Testez sur différents GPU - Les performances varient énormément + + +16. Ressources utiles +Documentation + +MDN WebGL : Documentation complète +WebGL Fundamentals : Tutoriels excellents +Learn WebGL : Autre ressource de qualité + +Inspiration + +PixiJS : Moteur 2D WebGL (code source à étudier) +Phaser : Framework de jeu 2D +Three.js : Pour comprendre l'architecture + +Outils + +Spector.js : Debugger WebGL +Chrome DevTools : Performance profiling +Texture Packer : Créer des texture atlases +Tiled : Éditeur de tilemap + + +Voilà ! Vous avez maintenant une documentation complète de l'architecture d'un moteur de jeu 2D avec WebGL en TypeScript natif, sans librairie externe. +Résumé des points clés : + +WebGL pour exploiter le GPU et avoir des performances 10-100× meilleures +SpriteBatch pour dessiner 10000+ sprites en 1 draw call +ECS (Entity Component System) pour une architecture évolutive +Quadtree pour optimiser les collisions +Object Pooling pour éviter le garbage collector +Architecture modulaire pour faciliter la maintenance + +Bonne chance pour votre moteur de jeu ! 🎮 \ No newline at end of file diff --git a/docs/PROMPT.md b/docs/PROMPT.md new file mode 100644 index 0000000..1ae3f39 --- /dev/null +++ b/docs/PROMPT.md @@ -0,0 +1,19 @@ +Je développe un moteur de jeu 2D WebGL en TypeScript selon la documentation CORE.MD. +État actuel : +Setup complet (pnpm, Vite, TypeScript strict) +Math : Vector2, Color, Rect, Matrix3 +WebGL : GLContext, Shader, Buffer, Texture +Core : Time, GameLoop, Scene, SceneManager, Engine +Rendering : Camera, WebGLRenderer, SpriteBatch +index.ts utilise Engine comme point d'entrée +À faire (selon CORE.MD, phase 4) : +GameObject : entité de base avec Transform, hiérarchie parent-enfant, cycle de vie (awake, start, update, destroy) +Transform : position/rotation/scale locales et monde, matrice worldMatrix avec dirty flags, hiérarchie +Component : classe abstraite de base, référence au GameObject, cycle de vie +Principes : +TypeScript strict mode +ECS (composition, pas héritage) +Utiliser deltaTime pour tous les mouvements +SpriteBatch pour performance +Dirty flags pour optimiser +Continue le développement étape par étape, code les implémentations une par une, teste la compilation à chaque étape, et mets à jour DEV_LOG.md après chaque ajout. \ No newline at end of file diff --git a/index.html b/index.html index ec43d89..e8bb59b 100644 --- a/index.html +++ b/index.html @@ -1,82 +1,66 @@ - - - - - - - - - Document + + + CastleStorm Engine + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +

+ + + diff --git a/js/Core/loader.js b/js/Core/loader.js deleted file mode 100644 index 645120f..0000000 --- a/js/Core/loader.js +++ /dev/null @@ -1,23 +0,0 @@ -export async function loadImages(paths) { - const images = []; - - for (let i = 0, l = paths.length; i < l; i++) { - images[i] = await loadImage(paths[i]); - console.log(paths[i] + " chargé"); - } - return images; -} - -async function loadImage(path) { - const image = new Image(); - image.src = path; - - try { - await image.decode(); - } catch (error) { - console.log(error); - return null; - } - - return image; -} \ No newline at end of file diff --git a/js/Entities/Character.js b/js/Entities/Character.js deleted file mode 100644 index 5de0e38..0000000 --- a/js/Entities/Character.js +++ /dev/null @@ -1,123 +0,0 @@ - -import Entity from "./Entity.js"; -export default class Character extends Entity { - constructor(x, y, radius) { - super(x, y, radius); - this.speed = 5; - this.targetX = this.x; - this.targetY = this.y; - this.frame = 0; - this.sprite = 0; - } - - draw(context, game) { - context.beginPath(); - context.drawImage(this.sprite, this.frame * 32, 0, 32, 32, this.x - 32, this.y - 32, 32 * 2, 32 * 2); - context.closePath(); - - ////////////////////////// DEBUG //////////////////////////// - // dessin de la cible de destination du personnage (pour le debug) - context.beginPath(); - context.arc(this.targetX, this.targetY, 5, 0, Math.PI * 2, false); - context.fillStyle = 'red'; - context.fill(); - context.closePath(); - // dessin de la direction du personnage en fonction de la cible (pour le debug) - context.beginPath(); - context.moveTo(this.x, this.y); - context.lineTo(this.targetX, this.targetY); - context.strokeStyle = 'blue'; - context.stroke(); - context.closePath(); - // dessin de la ligne de tir du personnage en fonction du curseur (pour le debug) et un arc de cercle pour le curseur - context.beginPath(); - context.moveTo(this.x, this.y); - context.lineTo(game.mousePos.x, game.mousePos.y); - context.strokeStyle = 'red'; - context.stroke(); - context.closePath(); - // dessin de l'arc de cercle pour le curseur de 360° (pour le debug) - context.beginPath(); - context.arc(game.mousePos.x, game.mousePos.y, 5, 0, Math.PI * 2, false); - context.fillStyle = 'red'; - context.fill(); - context.closePath(); - - - - } - - update(context, game) { - this.updateSprite(context, game); - this.draw(context, game); - const dx = this.targetX - this.x; - const dy = this.targetY - this.y; - if (dx !== 0 || dy !== 0) { - const angle = Math.atan2(dy, dx); - const velocity = { - x: Math.cos(angle) * this.speed, - y: Math.sin(angle) * this.speed - } - if (Math.abs(dx) < Math.abs(velocity.x)) { - this.x = this.targetX; - } else { - this.x += velocity.x; - } - if (Math.abs(dy) < Math.abs(velocity.y)) { - this.y = this.targetY; - } else { - this.y += velocity.y; - } - } - } - - updateSprite(context, game) { - // on doit vérifier la position du curseur par rapport au personnage pour savoir si on doit changer le sprite - // on doit avoir 8 sprites différents pour les 8 directions possibles - // on doit donc vérifier si le curseur est dans un des 8 angles de 45° autour du personnage - let angle = Math.atan2(game.mousePos.y - this.y, game.mousePos.x - this.x); - if (angle < 0) { - angle += Math.PI * 2; - } - // on a l'angle entre le personnage et le curseur - // on doit maintenant vérifier dans quel angle de 45° on se trouve - // on va donc vérifier si l'angle est compris entre 0 et 45°, 45° et 90°, etc... - - if (angle >= 0 && angle < Math.PI / 8) { - this.sprite = game.character.sprites.right; - } else if (angle >= Math.PI / 8 && angle < Math.PI * 3 / 8) { - this.sprite = game.character.sprites.downRight; - } else if (angle >= Math.PI * 3 / 8 && angle < Math.PI * 5 / 8) { - this.sprite = game.character.sprites.down; - } else if (angle >= Math.PI * 5 / 8 && angle < Math.PI * 7 / 8) { - this.sprite = game.character.sprites.downLeft; - } else if (angle >= Math.PI * 7 / 8 && angle < Math.PI * 9 / 8) { - this.sprite = game.character.sprites.left; - } else if (angle >= Math.PI * 9 / 8 && angle < Math.PI * 11 / 8) { - this.sprite = game.character.sprites.upLeft; - } else if (angle >= Math.PI * 11 / 8 && angle < Math.PI * 13 / 8) { - this.sprite = game.character.sprites.up; - } else if (angle >= Math.PI * 13 / 8 && angle < Math.PI * 15 / 8) { - this.sprite = game.character.sprites.upRight; - } else if (angle >= Math.PI * 15 / 8 && angle < Math.PI * 2) { - this.sprite = game.character.sprites.right; - } - - // on doit mtn vérifier si on est en train de bouger ou pas - // si on est en train de bouger, on doit afficher l'animation de marche sinon on doit afficher l'animation d'arrêt - - if (this.targetX !== this.x || this.targetY !== this.y) { - this.frame += 1; - if (this.frame >= 4) { - this.frame = 0; - } - } else { - this.frame = 0; - } - - } - - - - -} diff --git a/js/Entities/Enemy.js b/js/Entities/Enemy.js deleted file mode 100644 index 07b4d7b..0000000 --- a/js/Entities/Enemy.js +++ /dev/null @@ -1,84 +0,0 @@ -import Entity from "./Entity.js"; -import Projectile from "./Projectile.js"; -export default class Enemy extends Entity { - constructor(x, y, radius, color, health, velocity, speed, behavior) { - super(x, y, radius); - this.color = color; // Couleur de l'ennemi - this.health = health; // Points de vie de l'ennemi - this.velocity = velocity; - this.speed = speed; // Vitesse de l'ennemi - this.dx = 0; // Différence de position entre l'ennemi et le joueur - this.dy = 0; // Différence de position entre l'ennemi et le joueur - this.angle = 0; // Angle entre l'ennemi et le joueur - this.behavior = behavior // Comportement de l'ennemi - } - - draw(context) { - context.beginPath(); - context.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false) - context.fillStyle = this.color; - context.fill(); - context.closePath(); - context.beginPath(); - context.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false) - context.strokeStyle = "black"; - context.stroke(); - context.closePath(); - - } - - update(context, game) { - this.draw(context); - const distance = Math.hypot(this.dx, this.dy) - switch (this.behavior) { - case 'follow': - this.updateAngle(game); - this.updateSpeed(); - this.x += this.velocity.x; - this.y += this.velocity.y; - // Si l'ennemi est trop proche du joueur, il recule - // Cette condition est nécessaire pour éviter que l'ennemi ne se colle au joueur - if (distance < this.radius + game.character.model.radius - 3) { - this.x -= this.velocity.x; - this.y -= this.velocity.y; - } - break; - case 'ranged': - this.updateAngle(game); - this.updateSpeed(); - setTimeout(() => { - this.shoot(game, Projectile); - }, 1000); - this.x += this.velocity.x; - this.y += this.velocity.y; - if (distance < this.radius + game.character.model.radius + 300) { - this.x -= this.velocity.x; - this.y -= this.velocity.y; - } - break; - } - } - - shoot(game, Projectile) { - const velocity = { - x: Math.cos(this.angle) * 20, - y: Math.sin(this.angle) * 20, - } - const projectile = new Projectile(this.x , this.y , 5, velocity, 'black'); - game.projectiles.push(projectile); - } - - updateAngle(game) { - this.dx = game.character.model.x - this.x; - this.dy = game.character.model.y - this.y; - this.angle = Math.atan2(this.dy, this.dx); - } - - updateSpeed() { - this.velocity.x = Math.cos(this.angle) * this.speed; - this.velocity.y = Math.sin(this.angle) * this.speed; - } - - - -} diff --git a/js/Entities/Entity.js b/js/Entities/Entity.js deleted file mode 100644 index 495fe07..0000000 --- a/js/Entities/Entity.js +++ /dev/null @@ -1,9 +0,0 @@ -export default class Entity { - constructor(x, y, radius) { - this.x = x; - this.y = y; - this.radius = radius; - } - - -} \ No newline at end of file diff --git a/js/Entities/Loot.js b/js/Entities/Loot.js deleted file mode 100644 index c783b16..0000000 --- a/js/Entities/Loot.js +++ /dev/null @@ -1,27 +0,0 @@ - -import Entity from "./Entity.js"; -export default class Loot extends Entity { - constructor(x, y, type, radius) { - super(x, y, radius); - this.type = type; - } - - draw(context, game) { - if (this.type === 'health') { - // Dessine une potion de vie ayant sa hitbox au centre de l'image - context.beginPath(); - context.drawImage(game.loots.sprites.potion, 0, 0, 32, 32, this.x - 16, this.y - 16, 32 * 1.2, 32 * 1.2); - context.closePath(); - } else - if (this.type === 'money') { - // Dessine des pièces d'or - context.beginPath(); - context.drawImage(game.loots.sprites.money.coin, 0, 0, 32, 32, this.x - 16, this.y - 16, 32 * 1.5, 32 * 1.5); - } - } - - update(context, game) { - this.draw(context, game); - } - -} \ No newline at end of file diff --git a/js/Entities/Projectile.js b/js/Entities/Projectile.js deleted file mode 100644 index 90a11d0..0000000 --- a/js/Entities/Projectile.js +++ /dev/null @@ -1,26 +0,0 @@ - -import Entity from "./Entity.js"; -export default class Projectile extends Entity { - constructor(x, y, radius, velocity, color) { - super(x, y, radius); - this.velocity = velocity; - this.color = color; - } - - draw(context) { - context.beginPath(); - context.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false) - context.fillStyle = this.color; - context.fill(); - context.closePath(); - } - - update(context) { - this.draw(context); - this.x = this.x + this.velocity.x; - this.y = this.y + this.velocity.y; - } - - - -} \ No newline at end of file diff --git a/js/main.js b/js/main.js deleted file mode 100644 index 0a62b66..0000000 --- a/js/main.js +++ /dev/null @@ -1,224 +0,0 @@ -import Character from "./Entities/Character.js"; -import Enemy from "./Entities/Enemy.js"; -import Loot from "./Entities/Loot.js"; -import Projectile from "./Entities/Projectile.js"; -import {game, characterSprites, lootSprites} from "./Core/vars/game.js"; -import {resetGame} from "./Core/functions/reset.js"; -import {drawCharacterHpBar, drawHealthBar, drawPlayerStats, drawXpBar} from "./Core/ui/drawUI.js"; -import {spawnEnemies} from "./Core/physics/spawn.js"; -import {shoot} from "./Core/physics/shoot.js"; -import {move, dash, keyDownListener, keyUpListener} from "./Core/physics/movement.js"; -import {loadImages} from "./Core/loader.js"; - - -function drawGameIU(context, game) { - drawXpBar(context, game); - drawHealthBar(context, game); - drawCharacterHpBar(context, game); - drawPlayerStats(context, game); -} - -function clearCanvas(context, canvas) { - context.clearRect(0, 0, canvas.width, canvas.height); -} - - -function updateMousePos(e, game) { - game.mousePos = { - x: e.clientX, - y: e.clientY, - } -} - - -// Initialisation du canvas html et du contexte 2d -const canvas = document.getElementById("myCanvas"); -const context = canvas.getContext("2d"); -// On définit la taille du canvas en fonction de la taille de la fenêtre -canvas.width = window.innerWidth; -canvas.height = window.innerHeight -// On désactive l'anti-aliasing pour avoir des sprites pixelisés -context.imageSmoothingEnabled = false; -// On cache le canvas tant que le joueur n'a pas cliqué sur le bouton "Start" -canvas.style.display = "none" - - -// Initialisation du menu de démarrage du jeu et du bouton "Commencez" -const startMenu = document.getElementById("startMenu"); -const startButton = document.getElementById("startButton"); - - - -let up, upLeft, upRight, down, downLeft, downRight, left, right; -[up, upLeft, upRight, down, downLeft, downRight, left, right] = await loadImages(characterSprites); -let potion, coin, pile, bag; -[potion, coin, pile, bag] = await loadImages(lootSprites); -let quiPlayerStats; -[quiPlayerStats] = await loadImages(["assets/img/gui/gui.png"]); -game.gui.playerStats = quiPlayerStats; -console.log("Sprites chargés"); -game.character.sprites = { - up: up, - upLeft: upLeft, - upRight: upRight, - down: down, - downLeft: downLeft, - downRight: downRight, - left: left, - right: right -} -game.loots.sprites = { - potion: potion, - money: { - coin: coin, - pile: pile, - bag: bag - } -} -// On ajoute un évènement sur le bouton "Commencez" pour lancer le jeu -startButton.addEventListener("click", () => { - // On cache le menu de démarrage et on affiche le canvas - startMenu.style.display = "none"; - canvas.style.display = "block"; - // Apparition du personnage - game.character.model = new Character(canvas.width / 2, canvas.height / 2, 10); - // On lance le jeu - gameLoop(); - game.isLooping = true; - // Apparition des ennemis - game.intervalInstances.push(spawnEnemies(canvas, game, Enemy)); - // On ajoute des évènements sur les touches du clavier - window.onmousemove = (e) => game.isLooping ? updateMousePos(e, game) : null; - window.addEventListener('keydown', (e) => game.isLooping ? keyDownListener(e, game) : null); - window.addEventListener('keyup', (e) => game.isLooping ? keyUpListener(e, game) : null); - document.addEventListener("mousedown", (e) => - { - if (game.isLooping) { - game.character.inputs.click = true; - shoot(e, game, Projectile) - } - }); - document.addEventListener("mouseup", (e) => - { - game.character.inputs.click = false; - if (game.isLooping) { - game.character.inputs.click = false; - shoot(e, game, Projectile) - } - }); -}) - - - - - - - -let requestId = 0; - -function gameLoop() { - try { - requestId = requestAnimationFrame(gameLoop); - clearCanvas(context, canvas); - // On ajoute l'événement de déplacement du personnage - move(game); - // On ajoute la mécanique de dash - dash(game); - // On actualise le personnage - game.character.model.update(context, game); - // On actualise les projectiles - game.projectiles.forEach((projectile) => { - projectile.update(context); - }); - // Pour chaque loot - game.loots.instances.forEach((loot, index) => { - // On actualise le loot - loot.update(context, game); - // On vérifie si le personnage est en collision avec le loot - const distance = Math.hypot(game.character.model.x - loot.x, game.character.model.y - loot.y); - // Si le personnage est en collision avec le loot - if (distance - loot.radius - game.character.model.radius < 1) { - console.log("collision") - game.loots.instances.splice(index, 1); - // On vérifie le type de loot - if (loot.type === "health") { - // On ajoute de la vie au joueur - game.playerHealth.currentHealth += 20; - if (game.playerHealth.currentHealth > game.playerHealth.maxHealth) { - game.playerHealth.currentHealth = game.playerHealth.maxHealth; - } - } else if (loot.type === "money") { - game.character.money += Math.floor(Math.random() * 10) + 1; - } - } - }); - // Pour chaque ennemi - game.enemies.forEach((enemy, index) => { - // On actualise l'ennemi - enemy.update(context, game); - // On vérifie si le personnage est en collision avec l'ennemi - const distance = Math.hypot( - game.character.model.x - enemy.x, - game.character.model.y - enemy.y - ); - if (enemy.behavior === "ranged") { - - } - // Si le personnage est en collision avec l'ennemi - if (distance - enemy.radius - game.character.model.radius < 1) { - // On vérifie si le personnage n'est pas déjà en train de se faire attaquer - if (!game.character.model.isHit) { - // Sinon, on retire des points de vie au personnage - game.playerHealth.currentHealth -= 20 - game.character.armor; - game.character.model.isHit = true; - setTimeout(() => { - if (game.character.model) game.character.model.isHit = false; - }, 500); - } - // On vérifie si le personnage n'a plus de points de vie - if (game.playerHealth.currentHealth <= 0) { - // Si oui, on arrête la boucle de jeu et on affiche le menu de départ - cancelAnimationFrame(requestId); - canvas.style.display = "none"; - startMenu.style.display = "flex"; - resetGame(game); - } - } - // Pour chaque projectile - game.projectiles.forEach((projectile, projectileIndex) => { - // On vérifie si le projectile est en collision avec l'ennemi - const distance = Math.hypot(projectile.x - enemy.x, projectile.y - enemy.y); - if (distance - enemy.radius - projectile.radius < 1) { - // Si oui, on retire les points de vie de l'ennemi - enemy.health -= game.character.attack; - // game.projectiles.splice(projectileIndex, 1); - enemy.color = "red"; - setTimeout(() => { - enemy.color = "green" - }, 100); - // Si les points de vie de l'ennemi sont inférieurs ou égaux à 0 on le supprime - if (enemy.health <= 0) { - const type = Math.random() > 0.5 ? "health" : "money"; - game.enemies.splice(index, 1); - game.playerLevel.currentXp += enemy.radius; - game.loots.instances.push(new Loot(enemy.x, enemy.y, type, 5)); - // On vérifie si le joueur a assez d'expérience pour passer au niveau supérieur - if (game.playerLevel.currentXp >= game.playerLevel.cap) { - game.playerLevel.currentXp = 0; - game.playerLevel.cap *= 1.5; - game.playerLevel.currentLevel += 1; - game.playerHealth.maxHealth += 5; - game.character.attack += 1; - game.character.armor += 1; - } - } - } - }); - }); - drawGameIU(context, game); - } catch (e) { - cancelAnimationFrame(requestId); - } -} - - diff --git a/package-lock.json b/package-lock.json index dc97453..5f7cc5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,502 +1,450 @@ { - "name": "castlestorm", - "version": "0.0.0", - "lockfileVersion": 2, + "name": "castlestorm-engine", + "version": "0.1.0", + "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "castlestorm", - "version": "0.0.0", + "name": "castlestorm-engine", + "version": "0.1.0", + "license": "MIT", "devDependencies": { - "typescript": "^5.0.2", - "vite": "^4.4.5" + "@types/node": "^20.10.0", + "rimraf": "^5.0.5", + "typescript": "^5.3.3" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", - "cpu": [ - "arm" - ], + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, - "optional": true, - "os": [ - "android" - ], + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, "engines": { "node": ">=12" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", - "cpu": [ - "arm64" - ], + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, + "license": "MIT", "optional": true, - "os": [ - "android" - ], "engines": { - "node": ">=12" + "node": ">=14" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", - "cpu": [ - "x64" - ], + "node_modules/@types/node": { + "version": "20.19.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.24.tgz", + "integrity": "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==", "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", - "cpu": [ - "arm64" - ], + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, - "optional": true, - "os": [ - "darwin" - ], + "license": "MIT", "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", - "cpu": [ - "x64" - ], + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, - "optional": true, - "os": [ - "darwin" - ], + "license": "MIT", "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", - "cpu": [ - "arm64" - ], + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } + "license": "MIT" }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", - "cpu": [ - "x64" - ], + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", - "cpu": [ - "arm" - ], + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, "engines": { - "node": ">=12" + "node": ">=7.0.0" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", - "cpu": [ - "arm64" - ], + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, "engines": { - "node": ">=12" + "node": ">= 8" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", - "cpu": [ - "ia32" - ], + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, "engines": { - "node": ">=12" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", - "cpu": [ - "loong64" - ], + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", - "cpu": [ - "mips64el" - ], + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=8" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", - "cpu": [ - "ppc64" - ], + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", - "cpu": [ - "riscv64" - ], + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, "engines": { - "node": ">=12" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", - "cpu": [ - "s390x" - ], + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "license": "ISC", "engines": { - "node": ">=12" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", - "cpu": [ - "x64" - ], + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=8" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", - "cpu": [ - "x64" - ], + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, - "optional": true, - "os": [ - "netbsd" - ], + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, "engines": { - "node": ">=12" + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", - "cpu": [ - "x64" - ], + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", - "cpu": [ - "x64" - ], + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, - "optional": true, - "os": [ - "sunos" - ], + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, "engines": { - "node": ">=12" + "node": ">=8" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", - "cpu": [ - "arm64" - ], + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, - "optional": true, - "os": [ - "win32" - ], + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=8" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", - "cpu": [ - "ia32" - ], + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, - "optional": true, - "os": [ - "win32" - ], + "license": "ISC", "engines": { - "node": ">=12" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", - "cpu": [ - "x64" - ], + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, - "optional": true, - "os": [ - "win32" - ], + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=8" } }, - "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "ansi-regex": "^6.0.1" }, "engines": { - "node": "^10 || ^12 || >=14" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/rollup": { - "version": "3.29.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", - "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, - "bin": { - "rollup": "dist/bin/rollup" + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=14.18.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "node": ">=8" } }, - "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -505,308 +453,125 @@ "node": ">=14.17" } }, - "node_modules/vite": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", - "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==", + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", "dependencies": { - "esbuild": "^0.18.10", - "postcss": "^8.4.27", - "rollup": "^3.27.1" + "isexe": "^2.0.0" }, "bin": { - "vite": "bin/vite.js" + "node-which": "bin/node-which" }, "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - }, - "peerDependencies": { - "@types/node": ">= 14", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } + "node": ">= 8" } - } - }, - "dependencies": { - "@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", - "dev": true, - "optional": true - }, - "@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", - "dev": true, - "optional": true - }, - "@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", - "dev": true, - "optional": true }, - "@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, - "optional": true - }, - "@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", - "dev": true, - "optional": true - }, - "@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", - "dev": true, - "optional": true - }, - "@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", - "dev": true, - "optional": true - }, - "@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", - "dev": true, - "optional": true + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } }, - "@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, - "optional": true + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } }, - "@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, - "optional": true + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "optional": true + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } }, - "@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, - "optional": true + "license": "MIT" }, - "@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, - "optional": true + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } }, - "esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, - "requires": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" - } - }, - "fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "optional": true - }, - "nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", - "dev": true - }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "dev": true, - "requires": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - } - }, - "rollup": { - "version": "3.29.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", - "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", - "dev": true, - "requires": { - "fsevents": "~2.3.2" - } - }, - "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true - }, - "typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", - "dev": true - }, - "vite": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", - "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==", - "dev": true, - "requires": { - "esbuild": "^0.18.10", - "fsevents": "~2.3.2", - "postcss": "^8.4.27", - "rollup": "^3.27.1" + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } } } diff --git a/package.json b/package.json index 59551a0..9130f8b 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,31 @@ { - "name": "castlestorm", - "private": true, - "version": "0.0.0", + "name": "castlestorm-engine", + "version": "0.1.0", + "description": "Moteur de jeu 2D avec WebGL en TypeScript", + "main": "dist/index.js", "type": "module", "scripts": { - "dev": "vite", "build": "tsc && vite build", - "preview": "vite preview" + "watch": "tsc --watch", + "dev": "vite", + "preview": "vite preview", + "clean": "rimraf dist" }, + "keywords": [ + "game-engine", + "webgl", + "typescript", + "2d", + "ecs" + ], + "author": "", + "license": "MIT", "devDependencies": { - "typescript": "^5.0.2", - "vite": "^4.4.5" - } + "@types/node": "^20.10.0", + "rimraf": "^5.0.5", + "typescript": "^5.3.3", + "vite": "^5.0.0" + }, + "dependencies": {} } + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..7b010c2 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,843 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@types/node': + specifier: ^20.10.0 + version: 20.19.24 + rimraf: + specifier: ^5.0.5 + version: 5.0.10 + typescript: + specifier: ^5.3.3 + version: 5.9.3 + vite: + specifier: ^5.0.0 + version: 5.4.21(@types/node@20.19.24) + +packages: + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@rollup/rollup-android-arm-eabi@4.52.5': + resolution: {integrity: sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.52.5': + resolution: {integrity: sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.52.5': + resolution: {integrity: sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.52.5': + resolution: {integrity: sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.52.5': + resolution: {integrity: sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.52.5': + resolution: {integrity: sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.52.5': + resolution: {integrity: sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.52.5': + resolution: {integrity: sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.52.5': + resolution: {integrity: sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.52.5': + resolution: {integrity: sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.52.5': + resolution: {integrity: sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.52.5': + resolution: {integrity: sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.52.5': + resolution: {integrity: sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.52.5': + resolution: {integrity: sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.52.5': + resolution: {integrity: sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.52.5': + resolution: {integrity: sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.52.5': + resolution: {integrity: sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.52.5': + resolution: {integrity: sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.52.5': + resolution: {integrity: sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.52.5': + resolution: {integrity: sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.52.5': + resolution: {integrity: sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.52.5': + resolution: {integrity: sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==} + cpu: [x64] + os: [win32] + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/node@20.19.24': + resolution: {integrity: sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + rimraf@5.0.10: + resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} + hasBin: true + + rollup@4.52.5: + resolution: {integrity: sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + +snapshots: + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@rollup/rollup-android-arm-eabi@4.52.5': + optional: true + + '@rollup/rollup-android-arm64@4.52.5': + optional: true + + '@rollup/rollup-darwin-arm64@4.52.5': + optional: true + + '@rollup/rollup-darwin-x64@4.52.5': + optional: true + + '@rollup/rollup-freebsd-arm64@4.52.5': + optional: true + + '@rollup/rollup-freebsd-x64@4.52.5': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.52.5': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.52.5': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.52.5': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.52.5': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.52.5': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.52.5': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.52.5': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.52.5': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.52.5': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.52.5': + optional: true + + '@rollup/rollup-linux-x64-musl@4.52.5': + optional: true + + '@rollup/rollup-openharmony-arm64@4.52.5': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.52.5': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.52.5': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.52.5': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.52.5': + optional: true + + '@types/estree@1.0.8': {} + + '@types/node@20.19.24': + dependencies: + undici-types: 6.21.0 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + balanced-match@1.0.2: {} + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + eastasianwidth@0.2.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fsevents@2.3.3: + optional: true + + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + is-fullwidth-code-point@3.0.0: {} + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + lru-cache@10.4.3: {} + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minipass@7.1.2: {} + + nanoid@3.3.11: {} + + package-json-from-dist@1.0.1: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + picocolors@1.1.1: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + rimraf@5.0.10: + dependencies: + glob: 10.4.5 + + rollup@4.52.5: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.52.5 + '@rollup/rollup-android-arm64': 4.52.5 + '@rollup/rollup-darwin-arm64': 4.52.5 + '@rollup/rollup-darwin-x64': 4.52.5 + '@rollup/rollup-freebsd-arm64': 4.52.5 + '@rollup/rollup-freebsd-x64': 4.52.5 + '@rollup/rollup-linux-arm-gnueabihf': 4.52.5 + '@rollup/rollup-linux-arm-musleabihf': 4.52.5 + '@rollup/rollup-linux-arm64-gnu': 4.52.5 + '@rollup/rollup-linux-arm64-musl': 4.52.5 + '@rollup/rollup-linux-loong64-gnu': 4.52.5 + '@rollup/rollup-linux-ppc64-gnu': 4.52.5 + '@rollup/rollup-linux-riscv64-gnu': 4.52.5 + '@rollup/rollup-linux-riscv64-musl': 4.52.5 + '@rollup/rollup-linux-s390x-gnu': 4.52.5 + '@rollup/rollup-linux-x64-gnu': 4.52.5 + '@rollup/rollup-linux-x64-musl': 4.52.5 + '@rollup/rollup-openharmony-arm64': 4.52.5 + '@rollup/rollup-win32-arm64-msvc': 4.52.5 + '@rollup/rollup-win32-ia32-msvc': 4.52.5 + '@rollup/rollup-win32-x64-gnu': 4.52.5 + '@rollup/rollup-win32-x64-msvc': 4.52.5 + fsevents: 2.3.3 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + source-map-js@1.2.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.2 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + vite@5.4.21(@types/node@20.19.24): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.52.5 + optionalDependencies: + '@types/node': 20.19.24 + fsevents: 2.3.3 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.1.2 diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_01.png b/public/Retro Inventory/Retro Inventory/Original/Health_01.png new file mode 100644 index 0000000000000000000000000000000000000000..62e9c949d7cefb856584fa3a9eb7034624e1cbde GIT binary patch literal 314 zcmeAS@N?(olHy`uVBq!ia0vp^4nQox!3-oPT6RhUDaPU;cPEB*=VV?2IV|apzK#qG z8~eHcB(ehe5&=FTu0ZE}k>Pfnp41Yj>mrDUOmLzu^B6z;Lg5 zZzNERv%n*=n1O-sFbFdq&tH)O6rAqq;uvD#KX>wNzQYC_&Wy{S{QqC^>{6zG@QIZU zD^}@VUbd-oLRPVpve&AsXE~>INHL_&bn6i`;rJV+eLAE!e9QkY&SzxUBF=wKNt9CL z<+&nekT{|Eh?e8&%L|qppL$?^@GjSVjtA4{>_4J=Pcq@&vFBGDKb>Lty1V&^ShT}i yr+BuVte>CTEOC8Qx4fSBQT&_C6HDKuzUOD>5cvFQicKTXi42~uelF{r5}E*$%6NGI literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_01_Bar01.png b/public/Retro Inventory/Retro Inventory/Original/Health_01_Bar01.png new file mode 100644 index 0000000000000000000000000000000000000000..7b12b563a3b7eb4a73a5e9dc605b0429995e64e1 GIT binary patch literal 174 zcmeAS@N?(olHy`uVBq!ia0vp^4nQox!3HFkJ+IURQjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`ISV`@iy0XB4ude`@%$AjKtX3u7srqY_hbpy#R(!kO%r(}7Mk3Auz%~5 zdX3@}496CqfB0{i1w&ewG0#p0k9yCY``r&}K-C5saJ5+^Brq_veP(AipD(utXdHv5 LtDnm{r-UW|SYtD< literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_01_Bar02.png b/public/Retro Inventory/Retro Inventory/Original/Health_01_Bar02.png new file mode 100644 index 0000000000000000000000000000000000000000..9d96b8df25c075ce81c700c30479291e901822b1 GIT binary patch literal 164 zcmeAS@N?(olHy`uVBq!ia0vp^4nQox!3HFkJ+IURQjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`ISV`@iy0XB4ude`@%$AjKtW4S7srqY_qP`e`4|*#oxsX?;QXRbk$S12(uVKf{bM}*q2ca*UIvB)KP$xsHsNn{1`ISV`@iy0XB4ude`@%$AjKtWqi7srqY_hbpy#R(!kO%r(}7MlEgaI5`) zz_kmEQA`E(!Ey|_&ISyt1Q-pt+zx6$)ddNn{1`B?5dxT!Hkz{XVDK1dW9mEc8u%Ts&un1H~B3*6v6LQXC~ge!>4CfZ<;A z-bkPrXMsm#F#`kNVGw3Kp1&dmC^*H_#WBRf|LqiSzQYPUtbZA6{!6FlXn4(D(Ve#| z!#C%n8h_(b0Zylm$QkKchTe>U#pSBJha9$F(^BNz^Yxzg-UiFhl6jXmsyw)tc>B>T zg%q9s+Yf5xa;6*?jZIkR_20^hVNn@VM~xpt=GEKGDetfF`egL(YUH|YXDzg+xMstX tM?ae5+2Vh_(A)m->%J@Nn>soy7&^oSG`l99EC9NX!PC{xWt~$(698&YaKHcn literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_02_Bar01.png b/public/Retro Inventory/Retro Inventory/Original/Health_02_Bar01.png new file mode 100644 index 0000000000000000000000000000000000000000..50a62b7cfd152408743dd40bb13567f805d12222 GIT binary patch literal 172 zcmeAS@N?(olHy`uVBq!ia0vp^0zmA*!3HFSYrjteQjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`ISV`@iy0XB4ude`@%$AjKtV@O7srqY_qS&@axy3gIA44q^hn3=)s}LW zb(;mPpYB(i@Q9niZ_T<6%R|xEmtBohb~wNxV8F!E!@vk++Hu(Ud)>Ul##t+(KOblq NgQu&X%Q~loCIHlXG93T_ literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_02_Bar02.png b/public/Retro Inventory/Retro Inventory/Original/Health_02_Bar02.png new file mode 100644 index 0000000000000000000000000000000000000000..7dd79cbca6505dbc98408fbf51532fa250382661 GIT binary patch literal 171 zcmeAS@N?(olHy`uVBq!ia0vp^0zmA*!3HFSYrjteQjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`ISV`@iy0XB4ude`@%$AjKtTsj7srqY_qS&Zxfm39SPmYR|8-%H;WLG~ zy*D(i1^9y2Fg$SaOLdFQ-EX$|yA&gnTSJ4OLIMki1S8Wo#|M>{nB=_K7kz*HaVF3# N22WQ%mvv4FO#n*oG3EdO literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_02_Bar03.png b/public/Retro Inventory/Retro Inventory/Original/Health_02_Bar03.png new file mode 100644 index 0000000000000000000000000000000000000000..ca721741a8bb3f60995fd757c290f009e71e2edb GIT binary patch literal 169 zcmeAS@N?(olHy`uVBq!ia0vp^0zmA*!3HFSYrjteQjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`ISV`@iy0XB4ude`@%$AjKtVfC7srqY_qS&baxy3~Fdt0#I4wiAtYFuJ z@Cm;)tpz^a_P8|h*8VM)b&J>hKij}4VZgx5mT-W9N8$i;<7-vsXMP+U*R!i015IM^ MboFyt=akR{0GD_)1ONa4 literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_03.png b/public/Retro Inventory/Retro Inventory/Original/Health_03.png new file mode 100644 index 0000000000000000000000000000000000000000..96e21d73442fe6511d86409fefdc4ed2f3e23b57 GIT binary patch literal 311 zcmeAS@N?(olHy`uVBq!ia0vp^4nQox!3-oPT6RhUDaPU;cPEB*=VV?2IV|apzK#qG z8~eHcB(eheq5(c3u0Z*p-9T)!}USNn{1`ISV`@iy0XB4ude`@%$AjKtX3u7srqY_hbpy#R(!kO@RhnZh8qVms}eE zUz_xQ;TcC>iGDlvTk$Ha2Yd?K6a^Vx>U(~^t^rrA#K_FdprXLe?7@6D7-$@Wr>mdK II;Vst0H!!EbpQYW literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_03_Bar02.png b/public/Retro Inventory/Retro Inventory/Original/Health_03_Bar02.png new file mode 100644 index 0000000000000000000000000000000000000000..c15e9309651dd65db9b3410d0ea611f525dc3fec GIT binary patch literal 165 zcmeAS@N?(olHy`uVBq!ia0vp^4nQox!3HFkJ+IURQjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`ISV`@iy0XB4ude`@%$AjKtU@{7srqY_qP`|3Nk2gIA1ha^nA`dWv9ao z7hZmhpS-1$fnmmJBkw;aHrt&os||nO@bx6)_U+sZ4HK6zOch{bmWi{N3p9nn)78&q Iol`;+0QqS%H2?qr literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_03_Bar03.png b/public/Retro Inventory/Retro Inventory/Original/Health_03_Bar03.png new file mode 100644 index 0000000000000000000000000000000000000000..084c518d37e9fb9a7dc32b8d9377a311d02dc27b GIT binary patch literal 169 zcmeAS@N?(olHy`uVBq!ia0vp^4nQox!3HFkJ+IURQjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`ISV`@iy0XB4ude`@%$AjKtVfC7srqY_hbpy#R(!kO@RhnZh8qVmyR@M z%l~tZY?jX86nLwDiD^M@2aAK~M@FFZK@Eu7hyw>07Zvh literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_04.png b/public/Retro Inventory/Retro Inventory/Original/Health_04.png new file mode 100644 index 0000000000000000000000000000000000000000..a469c07e8d3aba5b954c7dea0df9e14667253af0 GIT binary patch literal 381 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!7>k44ofy`glX(f`u%tWsIx;Y9 z?C1WI$O`031o(uw0_lJIeNMFr8VfU6=$rbuc+Lz5iZPh2-H{HYI7)*2g8xGR!@cIc zkw7ud0*}aI1_r*vAk26?e?8rNzt$WJellU&HYdG18Vy?bwJHXPvj z_CMXHJa&)JU!51nx6ffpVDYX!EYD#4crT;F<-EJz`3`4V+KY-B?)zEWelUEe@}Ts~ zVHSRi*0(II8Vp{HTn#}DObbLF$TNJM@O6S(p55B_@5Fa}Jt{an^LB{Ts5Yj%`W literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_04_Bar01.png b/public/Retro Inventory/Retro Inventory/Original/Health_04_Bar01.png new file mode 100644 index 0000000000000000000000000000000000000000..aba997656af3e07ee5cd6ee2743a919803c28d2a GIT binary patch literal 136 zcmeAS@N?(olHy`uVBq!ia0vp^xanMprEX$i(`mJaPl0@a|RL&kL$BNHHsY=g{R0x c95}$h&^(=qL$36RB2XcNr>mdKI;Vst0M~#acmMzZ literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_04_Bar02.png b/public/Retro Inventory/Retro Inventory/Original/Health_04_Bar02.png new file mode 100644 index 0000000000000000000000000000000000000000..9fc53899542edf3aae4900e3a7a0915bdcd6c7da GIT binary patch literal 135 zcmeAS@N?(olHy`uVBq!ia0vp^xanMprDMWi(`mJaPlAd69y6tkNanMprDMWi(`mJaB@n literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_04_Bar04.png b/public/Retro Inventory/Retro Inventory/Original/Health_04_Bar04.png new file mode 100644 index 0000000000000000000000000000000000000000..b94dd144547a57f81740cf40db1cf807ce636a77 GIT binary patch literal 136 zcmeAS@N?(olHy`uVBq!ia0vp^%s{Nm!2~3qym4Cxq!^2X+?^QKos)S9a~60+7BevL9R^{>a~60+7BevL9R^{>a~60+7BevL9R^{>C!}Y8XhQ^2&-a aGc(NUV7d_7QF95XjKR~@&t;ucLK6VUs38Xc literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_04_Heart_Blue.png b/public/Retro Inventory/Retro Inventory/Original/Health_04_Heart_Blue.png new file mode 100644 index 0000000000000000000000000000000000000000..0b1087b8782bdc78c0f5d44875059a0cadd3ae3b GIT binary patch literal 405 zcmeAS@N?(olHy`uVBq!ia0vp^0YEIk!3-p~P3QOwq!^2X+?^QKos)S9s|WamxB}^a`+ZKe2^tGCFtmNJ&^KjR_rb@-li}KjP2c|i-@9pMI8X^g zxK-5xAjMk}pC^`YZWNSm&6;2AvHjWEO{h z*tIT7?9frorHRY#&ft7JtN5DJ+{wY3s=S-t9P{XC+IekHu15w(!Ld5cW}Vqg+xfEB zhC4ksPH0IDo_9gpDfdY_d$P{#EalBlp8Ygo`uX*w-2I50u+*x%We`%ahq2uTW>Z~M>u^wtHT8@B${oW?)>?vEHoVIBE@H~!el mGydOPvt#xZ3kCRuxH7bTU|9Eo;o65y-~RvKy9p?q@Fk!UNU@a!`33(60fx={m+S|M za29w(7BevL9R^{>Pl;@R8ehboXnY`N?vuQ|wj@+xnkC+ZjAv{an^L HB{Ts5Ihkp< literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_04_Heart_Red.png b/public/Retro Inventory/Retro Inventory/Original/Health_04_Heart_Red.png new file mode 100644 index 0000000000000000000000000000000000000000..307bf04dedf38b177c52bc10be55671ee9d21278 GIT binary patch literal 398 zcmeAS@N?(olHy`uVBq!ia0vp^0YEIk!3-p~P3QOwq!^2X+?^QKos)S9s|5IjxB}^a`+ZKe2^tGC%oJy^&^J9J$Kd1Q`QKXd|K3eA!-4V)$v4`U z11X-8Aiv=M2*4n8|J*sCIA?)JWHAE+-(e7DJf6QI1t@si)5S5w!hh-HNWQ}gJTA?V zlkfcBH&=4<)8h)kGh>bCm9lu6TC z3yRNG&dMxLJ2b;fGipU~;kvEQJg422^9T5ZxXu)3I3vgK-&*qj-c3L;$3Bk7K#H{_$S?Rm5HS4S#up3};4JWn zEM{QfI}E~%$MaXD00j#?T^vI!{Gav(avf0MalZZP?|GZIi&8FKH2fo3t)Ois|5IjxB}^a`+ZKe2^tGC{10TX&^P^G&EVtW`F}0L|9dlMh6CjpS{RZy z0x6!7Aiv=M2*4n8|J*sCIA?)JWHAE+-(e7DJf6QI1t@si)5S5w!hh-HNWQ}gJTA?V zlkfcBH&=4<)8h)kGh>bCm9lu6TC z3yRNG&dMxLJ2b;fGipU~;kvEQJg422^9T5Zxc(1h_+QQNe=WoRdozJz8N9nb0V&p!Aiv=MK)~>O8(%O`fV03O zvY3H^?=T269?xHq0u(Ipba4!^@PFDD$aO%0$NBcFzvpePx#1ZP1_K>z@;j|==^1poj5Ay74N&h<; z|GlyR000Az1KD1eZxfK~#8N?V62}s~`-8)wb*2eg6l(=Oh8F0!oJ|>Tt%@6h3|v(7GM|x-O-Z z!%7|5`Mj#}iHu<^YoYUb)#Kyo!!#AO(D}S7^6}&w4>+m|@$uvv4>+m|@zLZ#3LI~t z^Lf?ex2eN0j1z?-N80FoUX}TElG2X{4=H2mDRn-t>ii-B4i)_Ics`#K{#XV@AS2!- zIi7#LUY&oFMuqXB87^QO;u@fMWigNY{-gaa!7{ryD)Pr0HgIC+XT@Xl)kXrl9~ zk9c#Jci5H>COnMYmi7Nb10x*v4=urAk7PtFd8>b(JFohPH)fubd@$i*Y=E+`(115a z?uyX{-g&EjF6O!FZ^|R9ls77@ur=>ZI2hp|D@z&(SDYdtvF5G*Vji2o>!v)jih0Mm z>zeo0pc}4w$~q>WkoJg##G1GIOL_DMtNxxmvviG>@H}H->1E41O9O~ivjulnx8c(! zBsRR%U(8E?Vj*_}-sP3@a!K;ekO0E%5p`qMrCZuJN!o_QhG)gay!Iy+ayQ^DkGoL1)fV%vKgkKYG;evb#>#k{lDrvi#cPl7q$h_yQr`DxyGPuBU*s0^fKxoLd|Sl;@bf(I31G|%&S>;P|hjQ@tbGu$2_Q}cF??O4rt8^8gc zn2*sc&-sxF+TmSZTRs>JInJHTp$5NvLfSSUn(@;ABfNk%;w&%o!z~EP^ODz^_m+m7 zPK?gvVt?5LL_3}XIKs3JLQ%wPAVc{e|{1wm)$g9#7owk`vfP{sW4 z29u14GxIiH-T*SZF(IN)&j%A8hWzj7?27$L-(hWZa^8lE{Upm96(;EHd@u*Tb7YWm z%rZf+;9kCkj5lytSMW0Y(xg~~V@9a*ZUN^~M!ZXMPRnz&M|rT9;n!uQ2+Hsf z+|pC(d|uW0Z3XIPrT40*I-gf%K3atoIaIr<^Lf?e6R9j<_7QbHuZny;`Nji|>Oy=x z`Nji|>Oy=fIr>#0t8_S^xk507*qo IM6N<$f`OWm>Hq)$ literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_05_02.png b/public/Retro Inventory/Retro Inventory/Original/Health_05_02.png new file mode 100644 index 0000000000000000000000000000000000000000..ba9dd25b279ea635bea9e92191b7d1a7ae42d50f GIT binary patch literal 1350 zcmV-M1-bf(P)Px#1ZP1_K>z@;j|==^1poj5Ay7D1eZxfK~#8N?V62}s~`-8)wb*2eg6l(=Oh8F0!oJ|>Tt%@6h3|v(7GM|x-O-Z z!%7|5`Mj#}iHu<^YoYUb)#Kyo!!#AO(D}S7^6}&w4>+m|@$uvv4>+m|@zLZ#3LI~t z^Lf?ex2eN0j1z?-N80FoUX}TElG2X{4=H2mDRn-t>ii-B4i)_Ics`#K{#XV@AS2!- zIi7#LUY&oFMuqXB87^QO;u@fMWigNY{-gaa!7{ryD)Pr0HgIC+XT@Xl)kXrl9~ zk9c#Jci5H>COnMYmi7Nb10x*v4=urAk7PtFd8>b(JFohPH)fubd@$i*Y=E+`(115a z?uyX{-g&EjF6O!FZ^|R9ls77@ur=>ZI2hp|D@z&(SDYdtvF5G*Vji2o>!v)jih0Mm z>zeo0pc}4w$~q>WkoJg##G1GIOL_DMtNxxmvviG>@H}H->1E41O9O~ivjulnx8c(! zBsRR%U(8E?Vj*_}-sP3@a!K;ekO0E%5p`qMrCZuJN!o_QhG)gay!Iy+ayQ^DkGoL1)fV%vKgkKYG;evb#>#k{lDrvi#cPl7q$h_yQr`DxyGPuBU*s0^fKxoLd|Sl;@bf(I31G|%&S>;P|hjQ@tbGu$2_Q}cF??O4rt8^8gc zn2*sc&-sxF+TmSZTRs>JInJHTp$5NvLfSSUn(@;ABfNk%;w&%o!z~EP^ODz^_m+m7 zPK?gvVt?5LL_3}XIKs3JLQ%wPAVc{e|{1wm)$g9#7owk`vfP{sW4 z29u14GxIiH-T*SZF(IN)&j%A8hWzj7?27$L-(hWZa^8lE{Upm96(;EHd@u*Tb7YWm z%rZf+;9kCkj5lytSMW0Y(xg~~V@9a*ZUN^~M!ZXMPRnz&M|rT9;n!uQ2+Hsf z+|pC(d|uW0Z3XIPrT40*I-gf%K3atoIaIr<^Lf?e6R9j<_7QbHuZny;`Nji|>Oy=x z`Nji|>Oy=fIr>#0t8_S^xk507*qo IM6N<$f>!>NQvd(} literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_05_03.png b/public/Retro Inventory/Retro Inventory/Original/Health_05_03.png new file mode 100644 index 0000000000000000000000000000000000000000..f5ed7eeb7d4186e4686fa2a3f15eb76d10a322f6 GIT binary patch literal 1350 zcmV-M1-bf(P)Px#1ZP1_K>z@;j|==^1poj5Ay7D1eZxfK~#8N?V62}s~`-8)wb*2eg6l(=Oh8F0!oJ|>Tt%@6h3|v(7GM|x-O-Z z!%7|5`Mj#}iHu<^YoYUb)#Kyo!!#AO(D}S7^6}&w4>+m|@$uvv4>+m|@zLZ#3LI~t z^Lf?ex2eN0j1z?-N80FoUX}TElG2X{4=H2mDRn-t>ii-B4i)_Ics`#K{#XV@AS2!- zIi7#LUY&oFMuqXB87^QO;u@fMWigNY{-gaa!7{ryD)Pr0HgIC+XT@Xl)kXrl9~ zk9c#Jci5H>COnMYmi7Nb10x*v4=urAk7PtFd8>b(JFohPH)fubd@$i*Y=E+`(115a z?uyX{-g&EjF6O!FZ^|R9ls77@ur=>ZI2hp|D@z&(SDYdtvF5G*Vji2o>!v)jih0Mm z>zeo0pc}4w$~q>WkoJg##G1GIOL_DMtNxxmvviG>@H}H->1E41O9O~ivjulnx8c(! zBsRR%U(8E?Vj*_}-sP3@a!K;ekO0E%5p`qMrCZuJN!o_QhG)gay!Iy+ayQ^DkGoL1)fV%vKgkKYG;evb#>#k{lDrvi#cPl7q$h_yQr`DxyGPuBU*s0^fKxoLd|Sl;@bf(I31G|%&S>;P|hjQ@tbGu$2_Q}cF??O4rt8^8gc zn2*sc&-sxF+TmSZTRs>JInJHTp$5NvLfSSUn(@;ABfNk%;w&%o!z~EP^ODz^_m+m7 zPK?gvVt?5LL_3}XIKs3JLQ%wPAVc{e|{1wm)$g9#7owk`vfP{sW4 z29u14GxIiH-T*SZF(IN)&j%A8hWzj7?27$L-(hWZa^8lE{Upm96(;EHd@u*Tb7YWm z%rZf+;9kCkj5lytSMW0Y(xg~~V@9a*ZUN^~M!ZXMPRnz&M|rT9;n!uQ2+Hsf z+|pC(d|uW0Z3XIPrT40*I-gf%K3atoIaIr<^Lf?e6R9j<_7QbHuZny;`Nji|>Oy=x z`Nji|>Oy=fIr>#0t8_S^xk507*qo IM6N<$g7ut~r~m)} literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Heart_Blue.png b/public/Retro Inventory/Retro Inventory/Original/Heart_Blue.png new file mode 100644 index 0000000000000000000000000000000000000000..004b317887ebe646a0c9284d600b29068f134f23 GIT binary patch literal 258 zcmeAS@N?(olHy`uVBq!ia0vp^4nQox!3-oPT6RhUDaPU;cPEB*=VV?2IV|apzK#qG z8~eHcB(ehe`~f~8t_*D-7}kAYxb|VwxBoz~a>?MeK#H{_$S?Rm5HS4S#up3};4JWn zEM{QfI}E~%$MaXD00px>T^vI!{IB-i=4(*kaW4P;KVK|eaWbRfr$y?K84I%&Za>jx zb$Jrp`77mNtbp?_@O1TaS?83{1OWJtTzvol literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Heart_Blue_1.png b/public/Retro Inventory/Retro Inventory/Original/Heart_Blue_1.png new file mode 100644 index 0000000000000000000000000000000000000000..d529fd39a2cd0ab22b18bf1542bddc210d33ba69 GIT binary patch literal 208 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`(>+}rLo9le|NQ@N&#c> zK?Cb-h2-DvJNEwz|75)AbKsRl?TpF4kN@Y9@?bKTkht*kL|Z~BBQx`=WxjHMctD`e z+Ac{$S%yvO$I`;x44gk~4{VWK&JEITbSEJpVaLG(8$2836&Xd9B{~W?BNDP6T%b%NV~@i|Mh0nx6v>;+S?oY3GI+ZBxvX5!wVvw?BF+UFX$e5^188jAnG4?@+S^s`(NkdKsWA!4VPLSm5~(<8 T{i=gN2QYZL`njxgN@xNAWg0T^vI!{IB-i=4(*kaW4P;KVK|eaWbRfr$y?K84I%&Za>jx zb$Jrp`77mNtbp?_@O1TaS?83{1OWAATzLQh literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Heart_Orange_1.png b/public/Retro Inventory/Retro Inventory/Original/Heart_Orange_1.png new file mode 100644 index 0000000000000000000000000000000000000000..32e89989ba54032e36308b6fe8da552fcfbbbe71 GIT binary patch literal 201 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`6FglULo9laPCm$a*nr1%zSpvs zYwswO(AOa2Dt`22WQ%mvv4FO#o?i BRBQkM literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Heart_Orange_2.png b/public/Retro Inventory/Retro Inventory/Original/Heart_Orange_2.png new file mode 100644 index 0000000000000000000000000000000000000000..46aa9eaa9de97632c208ed6de6ecad68dfd3d258 GIT binary patch literal 170 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`g`O^sAr`%7C!OYOFc5IL-*vUZ zM|~OV>P?+f9$hRu)@^@5XClY04Fa3aB$o+IcJVb0ePzz@_HFK#5@(_sfrnbvbUKnoc>UHx3vIVCg! E0A4&gsQ>@~ literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Heart_Orange_4.png b/public/Retro Inventory/Retro Inventory/Original/Heart_Orange_4.png new file mode 100644 index 0000000000000000000000000000000000000000..db0f7bef9906f259d6be878de2390f2d868901e8 GIT binary patch literal 118 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`ww^AIAr`&KfByfsXI5?KRJwb` zpn-LDhJf;+_}Bl8C#5+EJg#Tc^@=?J1PLy47@6fedYtWLd!!s#nHm0`=Q{g3Pu?A9 O5QC?ypUXO@geCy8Yb90y literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Heart_Red.png b/public/Retro Inventory/Retro Inventory/Original/Heart_Red.png new file mode 100644 index 0000000000000000000000000000000000000000..e335e05aeb1f0ba14c1e1afd4a2611d66dcf0644 GIT binary patch literal 258 zcmeAS@N?(olHy`uVBq!ia0vp^4nQox!3-oPT6RhUDaPU;cPEB*=VV?2IV|apzK#qG z8~eHcB(ehe`~f~8t~13M&d4$Rx0d|BcN0*|v5(_1kYX(f@(cbC1Ps5o@dX0~I14-? ziy0XB4ude`@%$AjK*4NJ7sn6_|Eqns`5F{>oXdaz&lgKqoXlwWX_0zl#=>lc+fTGv zU7iGY{z`cmD`5Ql$JM8bbEmpDa;$D+67)!a8r_&9KE;2Jo`$*9w{|(L#(> zK?CdT2RfW?JN93S`;+eSTzPe>3x9K@P|yF~n}UBf+e=DZ_<5o&p_GxCnYnr68kLrh zKP;O+{_zz!+gT^^ildG3j)Dxsa_#_!SgX99|Ns9_elYV9&;Ro$4FCM!Uw--j|7(-} z|Bv?mXRmzxL;W&~!}TRwBX}JQS^A%W)E;9pYB;$@+!PC{xWt~$(69E5lVz>YR literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Heart_Red_2.png b/public/Retro Inventory/Retro Inventory/Original/Heart_Red_2.png new file mode 100644 index 0000000000000000000000000000000000000000..d9241d8a445aabd5913fb3f7b107b045208a2941 GIT binary patch literal 190 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`t)4E9Ar`$yC!OYPG2n4I&e18P zz@E{h{5QvcAmdKI;Vst084d7O#lD@ literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Heart_Red_3.png b/public/Retro Inventory/Retro Inventory/Original/Heart_Red_3.png new file mode 100644 index 0000000000000000000000000000000000000000..bb67295f64f2c0671a0c39293c602a4069cce92c GIT binary patch literal 156 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`$(}BbAr`&KfByfsXI5?KRJwb` zpn-Mv107Dc9s4iE{Yh6j*$^qz^S}3|W(gxRGjsFCH7bE@JT(eNKppbT=S9+g)VuXP z{QrON?0?CV-H%zQK4_be&2UD*iOqoT?-H92K)dwYrrA6dK6o5x9fPNwFYP$gQu&X%Q~loCIFV)CS?Es literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Hearts.png b/public/Retro Inventory/Retro Inventory/Original/Hearts.png new file mode 100644 index 0000000000000000000000000000000000000000..28354f2fce6a744ca5f30fbcec33a7c72ab03572 GIT binary patch literal 713 zcmeAS@N?(olHy`uVBq!ia0vp^0YGfP!3-ofTJs(SQjEnx?oJHr&dIz4a#+$GeI0>P z{oH>NS%G|;0G|+7ApLK@*+V_}Aw;tUr0rf1|Bd|W*LTTA}myJ==P!~Z~r|J4lt z*E0OSHEaj?;r@2Y%Sp`&0xhP;l;r{+|K9()?7PPywH0q3?Z3Oy;34y~1zuLs z(^Q*h8ha=0&}pB!`LBxGt3N>^u6>0Yr<$BQas72@vv%LexI#d=3af+s+xal z`fG;?rS~>2+Fo%o>S{{;TDxY2&wsjg?N0pJ`iAjuMtyVS{p4fqk5 z`1bW($9*9CcU*EP|GC<|){ppY-~38^@xEN;$nkwf-+o#xjwqhBl2@DW@Pd|g%;Eya zXI=iomzZAKG5zk>@~4k~-%FTVzUGDQG3yZDZ_B?+q|K|}c$9sn{fo%sbzAlyT=U-W z@%OJ6-ldBr?e=Gx?R@>({z$I%|Kj4uOU8{6M nX4kfVXM-xf*1Pjhf69HuwuhvS9FZ^>bP0l+XkKL!M;b literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Hearts_Blue_1.png b/public/Retro Inventory/Retro Inventory/Original/Hearts_Blue_1.png new file mode 100644 index 0000000000000000000000000000000000000000..9ffdd01f37f920db0c498fd153c51b884cdb2394 GIT binary patch literal 335 zcmV-V0kHmwP)0W7qERce5s}T&>*+HEK&-3Jge28@Tiakr&{*ut+}5 zPml*5!ku`Be;!E83|~YP0RUzujg$CZik&s`->+O^GEczH8XlUGt0h7^YZ5#(B|B^Q zDr2ur#b~C`8oo6ND{R{ze7#Pl*|4jCjqTR~^0I>cz2Tq>91Axb; zXQ;K(X?b3sK-Btl&bVv>02q6Nsx5#cfFrPbSVYClRc(Q>Ph$-+b~Vx@-Y3TQ{1pjw zHPRSGp4`jFVYB`^1164U?y002ovPDHLkV1k5hjaUEx literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Hearts_Blue_2.png b/public/Retro Inventory/Retro Inventory/Original/Hearts_Blue_2.png new file mode 100644 index 0000000000000000000000000000000000000000..e476446b7b8b93ecb74c31e3f325cc24cbd24530 GIT binary patch literal 367 zcmV-#0g(QQP)TW26fR(PL574nt?*(56 zOJ@nIe+P&BN%wuaAB&h7)`(~X0GOFVdoDh6$$g{Tvv2o25t9M;jpDXQIK6;)-zX2a zMZ$ffc%U(u8Dz*f9S_)+A9&t%BA%8pW{XVl=bOgeF2QQOL3H&9W+o{?$jdR^*&@UI zaxVBC8MdW{uE&@N08Yn)S0|Q(yJTR*ZhQm)tMvx|F*iP(!7t{(|8Do{h8zbbyh}hYSh#~ilnvgvKD`I~Z zD`LrQkxa-FflxW2v|T6nFBBHA9Tzut{^F+JvF5Y<}rwSxbw4_lWVH11u zM0+@m0?89CN17las1up>YRM$t^EN$L@i1;sZ}%+z`pB3~1G@bIwYz6TMA;TBxl@!| zZ}(_jw=Djo$0W|k7DY}CV7*#;ebPA|mkb4V^J@Uz{(%34n_peQFHx!hAX{j{xB**W z3$lAH5qFyxvc=h2MFgdk%*Swh6L`bakM|yVHVxRk?|FD~3}yBS$b4Mvp6DQYI1)V^ zSx?NzcpeauLcoX^vxH4NavB9Ca!J6g#JDnzg32#jC%QkA9n5JaCcK`{bE^c5hQWXJ2L^b7XerJpGNZW5Wb2XE52a-oxV&9cv2NQXu zYdrQ{$s;X=Ob`*&SYyAb$b19oF(U~zr*zx+^QD-x z*aTF^GVCDwd1?+S_TAb68O>D!WIe-NW$e4Mp3x{4kzxmzD0fogs%^Wr$)Gf`#%vV- cXz*8j19IZ-AHkJ=g#Z8m07*qoM6N<$f~>Np>i_@% literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Hearts_Blue_5.png b/public/Retro Inventory/Retro Inventory/Original/Hearts_Blue_5.png new file mode 100644 index 0000000000000000000000000000000000000000..f6d7db6c8c64968544b65c9c37c60eeb71c3314f GIT binary patch literal 355 zcmV-p0i6DcP)a;-pV;3@R223_1r1Fz&m z-zp6}T`(aDbA>q`_RN;-PsrfjOV0tn)ct%J5pDA) zbpVcsJwWMjo6pX>1jEsoM^rpts^DJLDgcPK9_M)13ZCN~(YEqgC1H%wdJ6q_%Y2#e z+MhJStA!;#CCoPYKy~0^W*dl4Nn<71%~hBy>=xw1GUy`jYvKkf^lfQ?98(K`)>GV6 zhQ8H$N+T@k(u1n9o@A<(U2S6HM&U2K1-B-+SN~`H>-mH{eBb~8002ovPDHLkV1l(N Bn)d(z literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Hearts_Red_1.png b/public/Retro Inventory/Retro Inventory/Original/Hearts_Red_1.png new file mode 100644 index 0000000000000000000000000000000000000000..635491abac5968e1095a07c3021050f9852fbe6b GIT binary patch literal 367 zcmV-#0g(QQP)WXbp5yUVjLRwN;zlo|m5lFFAe@zf-Dd1Tgp%~>MD1nlyNMG`Q)hhAMCS-407 zc6r2}Mv#OW$f!<7Y2qxlX4~ifHI4)zXJ6IsGAme zPjyq>w5TurC6<|OtF|$Xp>0ebz-kV1HDfjJvDlc#uxy0h3?kyahzPmMBZq7Wh=}|w zA|m7>2^_K`;5&|QY;!5|F-a(;n8qXCe7cWF_xc1x#JFfL`A>oW#t#!K5kp8ma-{$O N002ovPDHLkV1k0Ep^g9m literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Hearts_Red_2.png b/public/Retro Inventory/Retro Inventory/Original/Hearts_Red_2.png new file mode 100644 index 0000000000000000000000000000000000000000..5fac4a9c52bcd1bedc2d47dcc2df07e89a817c58 GIT binary patch literal 360 zcmV-u0hj)XP)9Z)V@yVHHW}P)ZE|07>P`nRsfFyDBr!Mst=(-vPTSV_gJ{=aAJ^nT6{j zU{_^4(g>1JJsItOht2jK*VEy)`%hqYkqG`cG`PDBuv({>Ts?!Nl5$OsPX>Y6d4g~e z3VuU^&9*`F=)6I@-&u3Umk|IUJPoaUxCrrjujw#}u?&!j^s#WEl8&BOOkB(**PEh09Jkbe~TYkUF!>L2xLP$gpk0000DwAKc-0?)}a^_vA#02#J(ZsR2YpmBq5YnuX)Q&F-Cy8A*;6NF2BldX_kK z^oj%5@S$f(9Jq481Q9_UVHWclPs1IL)%3RU9%@%D?Y}k=qk%=U-Jx>-iijwEJb|6F zG(qj6h25~V|DeUwFd~}fda#(!jCVTMmjKx3wh^}*HrtJPz5>u}cleL8=PP9u{H8($ z0MXQCyoco{EZ?{A;upZ}>Wyfc`>Y~@QcC{l>CjbBd literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Hearts_Red_4.png b/public/Retro Inventory/Retro Inventory/Original/Hearts_Red_4.png new file mode 100644 index 0000000000000000000000000000000000000000..43c7b9f6348bad6c9e33259b38565e9ae022ec9a GIT binary patch literal 392 zcmV;30eAk1P)2Wbah4v9jk(h&cD)b`kvEUn2Jo^s0c^3NyLXLvRoJ2^3;5!Jp6!HKq znI1HoU5))2*u#7~GyCnwCrF}%5TYUgNQ%0e$s4A*h?IGEwPuB!I$#kg#(|{Qf<8s0 zEFK4vMWlGN5hNi_X6&{bOlCRW8r5s>UqJKT*ZlL4U_O=T^#^dCS0E|0yfk|^RY3F3 zhud~F|A3FlEI|^U&0x3PSpW1MIsgFo*0t(x+eLP;^$q~^`UCt2rrxRR;5V3hJ%c1v z@LyNxJPxlKvviFlR91i>2_Xa*BcS^vF`4D)bT31)bdC8`BHia0FRO)efFl_%fpniE z2^F$*UHkI687H$oK(#Dh3sf&dy-;x=tp(DaivqyK2zo2yKyooct(oL;2InYeQsSa* mQ(G5Mh8W+hHUAj!S9}8+XYJv~&k^7N0000a;-pV;3@R223_1r1Fz&m z-zp6}T`(aDbA>q`_RN;-PsrfjOV0tn)ct%J5pDA) zbpVcsJwWMjo6pX>1jEsoM^rpts^DJLDgcPK9_M)13ZCN~(YEqgC1H%wdJ6q_%Y2#e z+MhJStA!;#CCoPYKy~0^W*dl4Nn<71%~hBy>=xw1GUy`jYvKkf^lfQ?98(K`)>GV6 zhQ8H$N+T@k(u1n9o@A<(U2S6HM&U2K1-B-+SN~`H>-mH{eBb~8002ovPDHLkV1l(N Bn)d(z literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Hearts_Yellow_1.png b/public/Retro Inventory/Retro Inventory/Original/Hearts_Yellow_1.png new file mode 100644 index 0000000000000000000000000000000000000000..b9b962afffa4d1965745ce77d6cead76853da76d GIT binary patch literal 329 zcmV-P0k-~$P)d4PxD8@%V{1Lm*~SkN~FghhzKz4;A)koFKe)01fz z8=9HwbQL{Q8DeHg5K#aCn3=4a##2r1YNx-wtVJTv3An2rH&w=;GYr|)F2YTfaaTL; zX)N5B%{@7aL7*pZUC+TK73$Y`+9!r`=SsVeSz4C9hi^LRXN}b#5O8F z*%!h4L1Z2Ni7`165E1cNM1m>b{B#4N85pemFKBA%55)ct}(I)v{ bfp_Bp;j|tPURoy-00000NkvXXu0mjfo1%(E literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Hearts_Yellow_2.png b/public/Retro Inventory/Retro Inventory/Original/Hearts_Yellow_2.png new file mode 100644 index 0000000000000000000000000000000000000000..75168e3981ca03fc310cb6f1a0691a73d53a06a5 GIT binary patch literal 348 zcmV-i0i*tjP)DFgX{fF^~c zy*v4o$HIbUwKKPS!wE?YAtDw4ASt1mi!WSqTc+;6$(kq9Pr$ZJS>=YyIW*fc^>CFN zwq?qWMv#Q`WHiSEHoH$WxAwI74VYaeiof1#JUtr3t2Ls#SCAB4LCM!vV0KEdxLhdy zK!VM#M!mN^5dbvDgSRJcx^&5a#cqED0P$*#|AgBgp5T`l_v`I0=OFJvZb6<3@&#RP zyZYcvl^bDmq7|%k4Kp>59seJ^o_8qRXmU{%jGLZ5>5XqfHY*MBQqzU){ zftwVkd%Jh1Nd~6)XXl%potsl4!qJpcg#biE)x~UmVZ&J**k>nWPLV?glEr}}p_Wb) z<5?UyItjI8aUeM+hzRNcvstegPxox@irf4pG_JZve}AORCK|uj$Gd+*M3i}gfv<}K zjrT6C%a+k!bQw=m(nXQu0c_SQ=bhZ(Gc1<+8O5K%fNwW6U}zFXEZMs%7a{t=CIxE-A+n3mthXD zaVVCh(?o)Yfmx^|)XoAKrV;>&hM28PLM_pdT3AH#8IIu@im=5{al76F2zq_~1H0bc!7s4u%?^@K6FjYG zeGK5mEj*-al2F$Sl!%~|lEo0(w?5-pMyow=isuubbe}O=RtM!lK}JhR_Zdm3Iizdn z&!=LJViQmuOXm{Nj{|d1@zk#!kkOnaKo&#HRmM|a7DJq35h-?XigG6<&f2zXT?VCz gHKtSiqrqSC1%a6E|MnFR3;+NC07*qoM6N<$f~I(a;-pV;3@R223_1r1Fz&m z-zp6}T`(aDbA>q`_RN;-PsrfjOV0tn)ct%J5pDA) zbpVcsJwWMjo6pX>1jEsoM^rpts^DJLDgcPK9_M)13ZCN~(YEqgC1H%wdJ6q_%Y2#e z+MhJStA!;#CCoPYKy~0^W*dl4Nn<71%~hBy>=xw1GUy`jYvKkf^lfQ?98(K`)>GV6 zhQ8H$N+T@k(u1n9o@A<(U2S6HM&U2K1-B-+SN~`H>-mH{eBb~8002ovPDHLkV1l(N Bn)d(z literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Inventory.png b/public/Retro Inventory/Retro Inventory/Original/Inventory.png new file mode 100644 index 0000000000000000000000000000000000000000..afb0139f7310b88b884c64f039d0c1fe8e7f1085 GIT binary patch literal 1168 zcmeAS@N?(olHy`uVBq!ia0vp^4M3d0!3-oHE$ONTQjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`B?5dxT!Hkz{XVDK1TFMUeOx>%ippk&1H~AYZgF)2QXC~ge!>4CfZ<;A z-bkPrXMsm#F#`kNVGw3Kp1&f6fr0t0r;B4qhV$F$Z_5@N@VHEVx2^WSd0cHz%E=QV zO?Th$=Se?Yyz%KlHAcDn`OnM>qzjn)`Ig8c!Mg_@|8%TB&)=l}zRjVsps^}=hZ|CY=*JMfvGC!_XEz)Qaig8aFR{9Ar{+e)rDChcgS>Y#h#kG^C8 z|D}qP4s-syGF#caTF&#qL+(t-E~^WAt$M}Y99EAub_bmI*Zi8XU>C>j<@-7KB_CYU zh?frVwdOhY`(Vit=V*m20_H3q!?8ggSDrqk}N_Mv}Y}+2p zkua+-?vC=rW9zT1mHxUye&zwAyT1?4Y~F$dON>A0ynP_|!GFVscn|hWWN7~3 z-E6^^1#G*xUYFQz=o2rWpsjO7@c8@pz@Yh9F6X7ZO!=1b?RUwgNz?i?_)d$nGo~N- zCXp9zW#v)4rPixHs&LU+_~O>8 zX$C>B(h`=i`?w$BVp(O}apsaB|GdP6X?-5sy*U!9ty>wEy?k-hRw(n2*WF!#{l6x1 z>RjF{KgZI;8VQ;+zkdWwMLTj7;x5`&&nmoo;Fng3@G<3WaF7GR@JU-O1^iy|*Zh^+%O& z3E#Ibj|$x_H!{V@zh>Li99!GfsQv!`6{k(gQzaOg+2ysKE{u|&J=^Z+fo*P5ys^*U z%kS-D6!V->|CYY`;N%LGBYMy^7rDkknikc}QmB6}p*1IHsEyG7k8r?20&uP5Yz-+nXVlLlr zwKMxT3>|;Z;^&#b#(wZXkLZo)q(i_c$vt>PltbmWZ~RtP_wWT5zc_^ibqX3dY>)0? zx_#S_#o%X|j*%Gz|n%kfqe~OZ~?(*AO!X`gdtiG{93*Q6HP3t(q}M9vz+MZ^J*@(DWtCw zsY}0q4@g~3F4(3JKw?#>acPTmTr*`+aI&Fd>v;YF0WlSBEIF{<00000NkvXXu0mjf D7ZQ`v literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Inventory_Example_01.png b/public/Retro Inventory/Retro Inventory/Original/Inventory_Example_01.png new file mode 100644 index 0000000000000000000000000000000000000000..b7846d01718910b448bc4468a4676379b395611a GIT binary patch literal 965 zcmV;$13LVPP)Px#1ZP1_K>z@;j|==^1poj57*I@9MF0Q*%7zjCzf74|05~r-Ohid|a&`a!01>bd ztN;K22y{|TQ~&?}|NsC00N#VWSpWb432;bRa{vGi!vFvd!vV){sAK>D10qR8K~z{r z)tQZM+b|4-?PTlT|Bc7HqxfUB-jpf>c4#B(6M3Q}LG!^dfcPjlR) z$N5C_JkQ7Zc${qf^ zMa7BR9u7m#Yo&ZJ*P4b$BwONIp)!%Hyq-DKF`}S?vqE2l3>li(-AoND%p1oSLu8EK zl1W0|u=;N6Sca%}WJ)~~JT$+#*!|IorNb$_SG>i}gaTp3b z5|vt_HJ}8?X~=C>CLT;IykOH6S3}^*5Q84jGTo7n4abM@d?*+kC2}5OQ~2HDfA-ar zIL{hx6<%ZFp7Ory7MDX|OcNp&aTt!6`BGoQb;;;n1h$5Ig&Xv~3Te1w58rvpi10U} zi!rOX7_Pl9zrb(%E^AI73B81;3UMFJ7oVtj!QgC7#O+XSG9wEQmuR$*4xEmrxNXc z9Cs|k*yhNjVd9W=5+_fb6)Lk;IA`LyVT}Zy4uzKdK}fEx>JI;BU%hKg+*96n-Qu!` zjA=r|A~vHmM9h4tui-FM{XSO+Yz?<1?@+05lUO1CK0fvXyP=O)`gn$||Gj;O^j`q; n^ZyNeTjnTd?|)mzaU8z^rw~zp_#5`g00000NkvXXu0mjfXhyO` literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Inventory_Example_02.png b/public/Retro Inventory/Retro Inventory/Original/Inventory_Example_02.png new file mode 100644 index 0000000000000000000000000000000000000000..c2439a4ad9631b03d4f3769fee6fe38d70a357a7 GIT binary patch literal 870 zcmV-s1DX7ZP)<<00001b5ch_0Itp) z=>Px#1ZP1_K>z@;j|==^1poj57*I@9MF0Q*%7zjCzf74|05~r-Ohid|a&`a!01>bd ztN;K22y{|TQ~&?}|NsC00N#VWSpWb432;bRa{vGi!vFvd!vV){sAK>D0>eo}K~z{r z#h8I|;~)$~ZRgti|35zMYLNsuy-W)2P8!2wVG;HuogC2Ly<*03UfXl7({dgbv{%>p zz~ynC*LhthFM4~uKXv5@ZWqK@S*U8KPK4n2jz2qP>wC_5e3%p-0$YMhJ?Qatai)H# zgiCPLgPu9G1*v(ykL$U?q2rY3S%cfTA;%oHp(69F!=)b7-3e$7F3$n7pihcTZG~-e zZ*@5J74Gg2EuX$1mt;J#fL{{#yXrBoCgNHTEj|xw7GQJ@a1Y&rbehl}Fs_hr*29uH zi-M(L_W1%&ciOAFm++X=HZ$dD#4g3%R2D=N1H77qJqNY&yF7fkF_pSs0eB%C>AD>jcgxkFFzOJW84wYW`f z$nkxSS%#VV-qP1n!BUSMXpp$xAC`>u@uG?Gj=P+|ZLPr#yWWJ`{mC3p^7%RQS&lC? zxwQnBA)cRu7&n!f#%^-}ZroCFk2z&xnQ16uDy~ZWJST6VZ`)OI%}H#!cT?4!_0WQ) zv|PU+k@Jdy?%Wic#JA5|Y*8h9CngSh+@0G-m3sDy_26g4cBnW~e|x<@b>#?- wHq!L#BJE?f?J) literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Inventory_Example_03.png b/public/Retro Inventory/Retro Inventory/Original/Inventory_Example_03.png new file mode 100644 index 0000000000000000000000000000000000000000..5e1af9ed0cf4688bb0f75dadbeadaeef3375f75b GIT binary patch literal 664 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r53?z4+XPOVB7>k44ofy`glX(f`u%tWsIx;Y9 z?C1WI$O`0(2Ka=y0_jt2g8%mW%nWC+&^PsQ@dSz_-t;R3QtTx`e!>62fI-Te*$pVf zS>O>_%)r2R7=#&*=dVZsTJqD=#W5tp{c7aJJZ43n=GWW)|9|rE0rv$imHU(0e=cYx{t9(CP6=^QGDlWG^YJFAgRk=SELfhHD%nJO$u z``sWWARg#~2g}Y%%~iW`d`{sFJE_0t_f@^Otn)Wi>-$ooF%4wZ+otk!&zG*dSbUP> zK7-%l@OH)%#TiY4wr6ZoQ$F;1MD%(2H{JSO<<00001b5ch_0Itp) z=>Px#1ZP1_K>z@;j|==^1poj56;Mo6MF0Q*%7zjCzf74|05~r-Ohicl003dxPH_ML z00(qZPE-H?|NsC002)gJMgRZ+32;bRa{vGi!vFvd!vV){sAK>D0i;PpK~z{r#h8I^ z!ypVqA=&o-pFE$r7_)4sWHvpiP5f|r*r23JfxdF39;F`XK1UUwQ;c$T)DwrZ)}tOr z)dRm?k8dF}jLQKwQWmPpRD}uJS3WW&zu$an`4rLI1Qvo_J&3fs=xJ-J!VqlrAd(DD zfx2@3kA2^e!FE!b6N9h5!ACM=LQawshh06Wdk|0zcKZNcKg6jpGCjb<`uskX5bWz& zyK)uxdj2Rm2=DblZL3!@GR^SrzD<{qk?9fc^{igW$TY*d_W{n=X1IpmvESsD&LPU~ z^o*$|*!4|w1|cH`4?f7bKXOB``LrZX!2}k9{rY6;J@sr@pI!S+0%Y*N5T!XmxYr|} z{Qp(WlS8IR?Car(DH3;|)fqA}&G2rYcFqx#+K1q4eRlb*U%86+`b?HE2={tcuViGJ z;obYKopZ#b_91xpKES!$(m6!got`oE1iSUooI%Kl!Segv2RZjgZU{D?RxPJs0t>-I z-xB5O$(7cF*NRtD(bD$ydVC9+VI1dh*@di002ovPDHLkV1ioW2m$~A literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_1.png b/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_1.png new file mode 100644 index 0000000000000000000000000000000000000000..f9ff3dfbfd421eb28d1fa26b872c5545a27bee17 GIT binary patch literal 312 zcmV-80muG{P)yI}Vcc;SE)ABnS^ku12E#<|c4FhUpuEV2!yR)BxY-w)9Nu$5jucKC(4_@CSW0000< KMNUMnLSTaNDuibM literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_10.png b/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_10.png new file mode 100644 index 0000000000000000000000000000000000000000..d4d9bae1ca41c0a6bd43a65b6465324d97512059 GIT binary patch literal 409 zcmV;K0cQS*P)B2KqPt#KAVC-U3H%mae$mC5*wtfA|#g;aF5;~(*-gz>3qVJ%3bgzD<4n% zp5IIo8;J;VVw9ErDd0Z{c(DQy@mEz!MCcZPh}1PD9QG}|t>3Rl0D!~3y@^flr?H4g zst6)t8vsz(loYCh0KnBkRZ!QIbfMvSSO?OXFE?MXnlCh?L@YO-5ULisrWBnJQ;?F6 zU&k&1tNG$8LV_sZlGB!>Z3mHXsCQckRiV3?dqINQ z#^e!8ve8Gat_xKGQG^;`z*>}=0{$A=_XSS@*#&$9XN56pXEuY600000NkvXXu0mjf D-^8fA literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_2.png b/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_2.png new file mode 100644 index 0000000000000000000000000000000000000000..d8ed9beae6a0a84a67261897d5d0913489e8127d GIT binary patch literal 407 zcmV;I0cie-P)abk)62lmqBQhHL^}nSh#=?R)eFo-Trn>_{VP;-W6{Kv)QQ z-qV|rkdTNVB^F89UjqJvfVa>DM0~1Bi3r645RtkvhugJ+kKB_dkb9WS zaV1-P73{r`6<|cL69NcbN`nIa8p+=UF9FE~`~ur5H}s>J$E*MV002ovPDHLkV1m#J BvtIxJ literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_3.png b/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_3.png new file mode 100644 index 0000000000000000000000000000000000000000..864a347bcb2c917dd0a0d5e1d14c95b25767e46c GIT binary patch literal 377 zcmV-<0fzpGP)+b;>vwaOf&e+*pJCQj%0&PLC=L0pn;$rTGVzZZ3TF=Uh7-%26%+d Xye25kq)^9|00000NkvXXu0mjf>4m1O literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_4.png b/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_4.png new file mode 100644 index 0000000000000000000000000000000000000000..4de41caf3c51213850225f838838dcb55047a986 GIT binary patch literal 387 zcmV-}0et?6P)Nkl{`SpFh0>JrH-K-7g!?i@j zst9I|4glPhIeV8e0Mqa;ZqtMyfc1ThQxm|DrhXZyWkP#uu)_VFH_Z|i38UP%XHx1~m&*ltzh7O-GppUy6WY z{2z{!rbtAPQnRG)Z-D;*@F4{t;!9mhM5qQpMCzIfPRAC$Hs6m|0D#l69mKlV-B?5< z7J`U^0|2ONDzYXc08kBUGU}R&?lxhcUjR71e6@1J<0@i4_Q$=4Aob^JOsH@1Dse z0!Zerk$)`&5tC{Pd_x~-OfGRJlpy-|T8+LJxXqTA#lW0v#7O|8QoB`o8Nlz{F^T=y zNmju#Ad^l3*SR4h?M4(`1^2SmqaZZ7R23L2^;T?62K8aDHm(nQuqI=hF!_N5xd)Re zj%2fK&7KQcfF6Pk5F{N+MS#Cn^5=p#KyrW^$w@eUt0m9&00000NkvXXu0mjfU!ALJ literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_6.png b/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_6.png new file mode 100644 index 0000000000000000000000000000000000000000..1a2733d48f8e5f47199bee8d0666b8634041ac48 GIT binary patch literal 387 zcmV-}0et?6P)Nklb;^1uYl zWZvgDmMs%AqogK9-5&t=0Pr+cKtxyFrOZs(07S%nTjO}>@bdb0`2~REq5D)uVgOl(H5K=5&1ajiPcQ-h%GcX(Y?do2sHoQ44_MQoZ|i6h(GmFO z)8}axz-GC+K*SVX1#YIf=KgUeOVpshdCNXN$RL_VsyE_Ywg@nl0u* zgVY)oz%Vw~Sd+jIB5MR8X%k3M(z`$tZA$#f0Gg{yO+dX0+-x-l!xW@dP<#aHRgh~S z^#T-+;#=shAH`w<)>LTryKLiTzk@ZEoM!0_2}vI&x7dgOL>#~;io^yfs|d+u1>B=I$aH}mxoAB0n6&PKUy6v5 zVdk5MEeDARN@`Lx`V-(k0KCKu5b>cYB_hlQKt$@>8cxR!-nQ?SyNHp)^rQQ$6+hrXTIKj!DhLV3Ki6P_X*Z?(6@E462TVu z!Kbg&EP%~&H9&+ETm`PDxxxMaI$tV)`yqQolCNM3ZqyC?Znl`S&#&i;+LwAG3I&h7 z3u*Kga1fF&RRvNq47FhtfK+K(p#r#`FGGfYrrNBN0Jyd5GV>w0`xIYM07(vHwwSZ` z8mU%6YN_w-M&F3EbC9(W{nb`14xu#_G>3h5;O4LgYbtRFiyuf(`Y;*eNH+Os#B-qt r;2=bUh+>D*D8O&4_;$W~00000NkvXXu0mjf@Sm)= literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_8.png b/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_8.png new file mode 100644 index 0000000000000000000000000000000000000000..9ac30eb28f5ffb3ea0fbfcad740590316221fab7 GIT binary patch literal 373 zcmV-*0gC>KP)Kp4#0 zGoIa*SH#T7iAh%Wr+~j8;Kdg}L`hZ3%oHtvh`6guoR1COH{0O~0Ow=#E!Mnm#u5>W zBAD5_0B~2AtX07PqLx+#cXi2MG=9kAhb{x+;f5aobGJ|@H>wBdSbgGrIvdKp!&V{0YUW6Fnfo~}F3ixegKNma&WEb!WlcEu- TIh%)I00000NkvXXu0mjfMe~^D literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_9.png b/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_9.png new file mode 100644 index 0000000000000000000000000000000000000000..318c16646e14db0131a7e87579429bcab614e51a GIT binary patch literal 383 zcmV-_0f7FAP)V+45QX0$E)a>{f}af_s;;`vi8z2y6p0N|RuPiR3b;pakm;f_GEqDeJJ3*xCq?<2 z_8O z2*J#)8vw4$oUO?iK+?ca`NrA0IT_2&PqX?VppX@MYbws~~{(0T?>S3j3^ HP6Nn{1`ISV`@iy0XB4ude`@%$AjKtU%@7srqY_qP`e1sN20oG*s4wu`YWBFmd?AMQ}TJ1(|68X4*3nUmdF|8*Dy0MsCL@$|6phS!@Tb~ P&@={5S3j3^P6Nn{1`ISV`@iy0XB4ude`@%$AjKtW4S7srqY_qP`|3Nk40I0YIldOl~K@-a1G zkBLhEO)oStFwD@_+W#~#ELZUM?FaH3X7TU%2ei;_lAId@8?)N)Nl`#U7(8A5T-G@y GGywn^LN6!) literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Settings_Bar03.png b/public/Retro Inventory/Retro Inventory/Original/Settings_Bar03.png new file mode 100644 index 0000000000000000000000000000000000000000..5494241c6c0491eee9da0eee504d491e3a28703d GIT binary patch literal 165 zcmeAS@N?(olHy`uVBq!ia0vp^4nQox!3HFkJ+IURQjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`ISV`@iy0XB4ude`@%$AjKtU@{7srqY_qP`|iZUp0Fkdv7HTmb9GZ!}` zG6e?|>~AzvXJB~Nwd;RgTCHtZg!CVV-IfPFGBGe1O#8x+#K6Y9Q^HaSXbOX;tDnm{ Hr-UW|e_$`h literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Settings_Cross01.png b/public/Retro Inventory/Retro Inventory/Original/Settings_Cross01.png new file mode 100644 index 0000000000000000000000000000000000000000..70512e9698c00272a467078879935aadf98115f7 GIT binary patch literal 210 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJV{wqX6T`Z5GB1G~mUKs7M+SzC z{oH>NS%G~10G|+7ApLK@-Snc+YtLo#QfFOXs_3Gxg64+IRqxAB1t;wNS%G~10G|+7ApLK@-Snc+YtLo#QfFOXs_3Gxg64+IRqxAB1t;w6K7yX+-`Ob1h@{Qd$o On!(f6&t;ucLK6T2ib#Y2 literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Original/Settings_Cross03.png b/public/Retro Inventory/Retro Inventory/Original/Settings_Cross03.png new file mode 100644 index 0000000000000000000000000000000000000000..541ffbd0ab3be91a9f9473f95c286788dc7b69a5 GIT binary patch literal 244 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJV{wqX6T`Z5GB1G~mUKs7M+SzC z{oH>NS%G~10G|+7ApLK@-Snc+YtLo#QfFOXs_3Gxg64+IRqxAB1t;wZk%`4|jYoDKi~KYKmAc;^$Twjg2K4)dLg#l|*PtG?`* zp?Ab5F_d$nPp?bQ+14o>3^PxOXqYJ1?2@h*`sTq@BQ3x2(6;g=<|p(&TFV}>ob5X! culzf+$_*y%`U8Kmffg}%y85}Sb4q9e0REy)2><{9 literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Read Me.txt b/public/Retro Inventory/Retro Inventory/Read Me.txt new file mode 100644 index 0000000..fc21e0f --- /dev/null +++ b/public/Retro Inventory/Retro Inventory/Read Me.txt @@ -0,0 +1,31 @@ +Dear valued customer, + +Thank you for purchasing my asset pack! +It means a lot to me that you chose to include my work in your creative projects. +I put a lot of time and effort into creating high-quality assets that can help +bring your vision to life, and I'm glad that you found value in my work. + +I hope that my assets will be useful for your game development or design work, +and that they will help you achieve the desired results. +If you have any questions or feedback, please don't hesitate to reach out to me. +I am always open to hearing from my customers and improving my work. + +Thank you again for your support and for choosing to work with my asset pack. +I hope it exceeds your expectations and helps you create amazing things. + +Best regards, +ElvGames + + + +License: + You are free to use, personal or commercial projects. + You can edit/modify to fit your game. + Credits to ElvGames. + +You cannot: + Sell this asset pack, not even modified. + Claim this asset is yours. + +Follow me on Twitter! +https://twitter.com/ElvGames \ No newline at end of file diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_01.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_01.png new file mode 100644 index 0000000000000000000000000000000000000000..709447a7a15158366bcf58e356c9341bae64522e GIT binary patch literal 534 zcmV+x0_pvUP)$hQKq&Lq zw>!=bJ}1LW?QDXb{exIL--o}KpF?rlDEgjcT;tFh8wOC0*8n`Gb7nU;o6Zhq@3Tzp zY=Ysv?VtaFpj!C%g#iE&Ic5QJZ1rV!-uCIl-Tz+8T|0nsc^O*+%l8ihC?{cn-)ga% zF*;-ruvlOK*wI+~be(%=CbLQhjP`!Wwl#{3d5&=IZc)g16=SvgF%;)NJ_tF{!nO{#}0ObS? zFb@pO4t&)jZU5sl2VT~F7O8BxUj5g48$)6CFZ~CVv0@Jx`p<7;_#+k1kQRxlOC6ig4*kD31k88r SJ!l1V9D}E;pUXO@geCyV#zkfT literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_01_Bar02.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_01_Bar02.png new file mode 100644 index 0000000000000000000000000000000000000000..7b06a7a87191f07345d0c66d72cb61759a80f3f2 GIT binary patch literal 189 zcmeAS@N?(olHy`uVBq!ia0vp^4M42G!3HF6DHW&!sTNNc$B>FSZ*MLXY%maEbvzWH z*m@}`gW;_BcJ0MVdGkE3-TS@Z^_+bSn^^?Z8!Q-)a4PsQ6f$)PJIrBtwC*Lh%=u3u zKijLc)I$ztaD0e F0st0RKDz(_ literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_01_Bar03.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_01_Bar03.png new file mode 100644 index 0000000000000000000000000000000000000000..1816161e33058ab805c0f25599267deaed4cf344 GIT binary patch literal 197 zcmeAS@N?(olHy`uVBq!ia0vp^4M42G!3HF6DHW&!sUA-k$B>FSZ*MLXJnX>3;8?GA zr*y~flF3#<2bVJ@bKlVu5!xa6Bfk25^SbL_n9c|}%wc%MBB0)2!FYsI!H=PksiWFo zf5Sd||B3uJuJ3&JOwZxY4^|7i%iJz8-;>1^@_zE>6f>gf5XLkFEc8z&fLl!Fb6ghC OT@0SCelF{r5}E*P)D5T^vIy=DeM4@8#?$a4fx9 z&Y`3AfvnSkj-*u02rfCtpxc!UyRS&uG)@TGutn&oddnTf32q-4n$9Q9Ju&;up1W5c zyyL!qUDf*LpJ%sk+Vg*#xk+cG-?16D8aCBoTW1b8*A*c}r8pZrjb?_51vJ#&tnT-}n4m zo$~*urNZ%yo2T}(MI2qR>sWqif8AI1%^z!*?tdd=@X6&!^`Q?}>fV*TkvgZHyh3=6 z<$K1*oqzw{T$_77%{*oWW92dFNviy7IW3==UpI?OJLmQ8fD^9{OO5r%qR6}FmkLL1 zeIFOWKD%7&+L4(jXI*%<)BmI_$2!K!u#`gIjQgK&>fe^?C~H{S`}KTX{?xt97dJN; zUrN1Qd%o-Qza++KYZKU?%y?gV$KUDvmdKI;Vst0N~sI;{X5v literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_02_Bar01.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_02_Bar01.png new file mode 100644 index 0000000000000000000000000000000000000000..d3e7394db3a4587d04d77446a02d478004dd32d7 GIT binary patch literal 191 zcmeAS@N?(olHy`uVBq!ia0vp^3P9Yz!3HF=(z2z1RGX)ZV@SoEx3?DxHaPIG9H?$n zIuLwY+%>o2g%XDr!{?^n?h~o%c57D|ZCbYQHX~365}0%*p8fmM&`Do%YkxiVne=7# ot=V%PiX$l?$jq=?&sfL5gE8SF?{B$9-c}$Dp00i_>zopr0QkBvW&i*H literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_02_Bar02.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_02_Bar02.png new file mode 100644 index 0000000000000000000000000000000000000000..0cf6a5c5bab5a125bdcf7bd38bdc149f795c4528 GIT binary patch literal 185 zcmeAS@N?(olHy`uVBq!ia0vp^3P9Yz!3HF=(z2yM98VX=kcv5PZyWM87;rE+80_1y z`F!y^@xl!I1>DWoTMZb*-6o}lx<0p#TFwkqj|5zf{M=de#7ZZ|dVl$xsrk3Ie001eao37ik#&6 zXEG`0OhZU16&-WVY0yAm5K;D@#Y_iWD)YV_c<24TEe}425{yf<{h#erNFT#HYWDnkbU-M@wD3B4V>Bp0<0P#T z$R~NTx+PrmUjRA_5GdEKD^jnmZu zqNDG{^&)gU&=1rBfD2>+7Y4Eb5K0#V$V!pS4!k}^wnrtfB&rf`*x!#0TiHd+_!?gCmgqe$HmIlG$qi00yIvg0DOYATcx=1bKcbJ zge(9YL5%?{U;e#C)ITMb{k^dm6rgd`JAr0L{2e?g_&XScumhBc2OslQ)RzDN002ov JPDHLkV1fz2$+Z9g literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_03_Bar01.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_03_Bar01.png new file mode 100644 index 0000000000000000000000000000000000000000..7826cc4670f09f2adfe26cb7bdac7e2f046e68db GIT binary patch literal 202 zcmeAS@N?(olHy`uVBq!ia0vp^4M42G!3HF6DHW&!sfnI0jv*Cu-rjQLWin)Nb@UQ& zywEgHv{i14K#YiRuGy2b92r#=Kc|J1i*M>rMy7z&v> zgkeHEZf~1BpZ|Q1_Fnts;~T{k^4|R7|FDand&7l~bHy6IZqsG>e~<}77qUTj__O^4 V%vsv`LxGNC@O1TaS?83{1OSWeLCF9B literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_03_Bar02.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_03_Bar02.png new file mode 100644 index 0000000000000000000000000000000000000000..b7e39154912c64db5bb673a602ebb91bbf85ed08 GIT binary patch literal 191 zcmeAS@N?(olHy`uVBq!ia0vp^4M42G!3HF6DHW&!sWwj+$B>FSZ*LvsZ7~pFIrzDg zgL8_Auh1HSSF0Fw?4v$!yngSu`}O~Cxj*PJ9^q8*V<=?m5O$cu@Q6h~y}<%5WPQwU z-?a2&>T|1oWwIF`UAs}=aGA;G)g}g>8PN=O2bu6_6?VA8`_q@-{N*+G9H5IBJYD@< J);T3K0RUGQKGpyL literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_03_Bar03.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_03_Bar03.png new file mode 100644 index 0000000000000000000000000000000000000000..d5bab149cb6ee712b340ef14d8c96fee19c2dc3b GIT binary patch literal 198 zcmeAS@N?(olHy`uVBq!ia0vp^4M42G!3HF6DHW&!sa{VP$B>FSZ*OhnZFUfFb>y=w zmo4!(Uw2q^&g>6e56+5waQ(35CJ*khkxmOrqWML@m5g7FBaf*(U6Q-`p_9EL}5 zp?9XHd+g%R_tbA$|9#etQpQKe^bQpE>zcED=YU;~#G> z;8!@}^g&i-0*B(%r4xcaaJsC0Tf<C;#RsfS?*%*(53mcwbf30=sAgWh z`T4WF!{2|e^eMa3@_76Hxi@7!E^o_yb;-o{dBe8c*>;aj^rT<^W8D(R@5TP8>)-F% z{hwKCkKbf@oWy+KZdSveE<0OGKh33I{(XMGQ@x|mdOhEBx&NOYUOcnIwU~9So5@CA zhU-VP5-v{t|Ka7iJHme~=ZDDltKGPFcebURz52fS^?Ng9&Tg6?Y?t@==$4JS3_FsT zkJ#F-dvo^q>|+0dpRYH)sxUeDWI^@01-nJ(@8?}G<-*qgQe@pI1G*Jr+4 zkm$<%94N<7zM3&Ft$X!Ocl|5@US%nZbmjxPbC`K@uktR=Gh>MPn47RKu71L)>|<9 zJ(J0bXx@Tah3qEt_phF@EUo@`^L%FWp1)>C*025lw2oov6f3C*RR=yL*=2T`mQZ+r{{4uFBH8dp7>QvbX+w zGcUvTFER-)K1T6NGvvLrJaFOhRyi|yaHKFm)PxgcY} zyM@_}E8$PRuH~#nFB#l|P39iRaJbCb!)7v4RdY&$hAs(H{fAV#I&S&I2v+MJm kMiA&^WS5wV26coOq@S`TE0@pH2I^$+boFyt=akR{05?(`wg3PC literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Bar05.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Bar05.png new file mode 100644 index 0000000000000000000000000000000000000000..bdb1871a91c209ac4a0271a3587265e36ec75a65 GIT binary patch literal 98 zcmeAS@N?(olHy`uVBq!ia0vp^Y(N~v0U~ENs2>4Rnw~C>As(H{fA&v%XwPgE!L)FO iL_tGfCn`Ls#K@qc&cb{#`?ds76N9I#pUXO@geCx!`5QR^ literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Bar06.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Bar06.png new file mode 100644 index 0000000000000000000000000000000000000000..7d6becd964bd45443315b0e5036211fbf065e358 GIT binary patch literal 98 zcmeAS@N?(olHy`uVBq!ia0vp^Y(N~v0U~ENs2>4Rnw~C>As(H{DLcMBv}ZQD!LV?K iL_tGfCn`Ls#K@3u&Z60?UN#M=iNVv=&t;ucLK6U=P#cN> literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Heart_Blue.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Heart_Blue.png new file mode 100644 index 0000000000000000000000000000000000000000..6b77340bbc0f82d8cd0c2d86653f5f6fa089f159 GIT binary patch literal 686 zcmV;f0#W^mP)001Be1^@s6m49>f0007YNklkh-??OyG)=`Ik|Y_}Ax+c7rJpPCSKB}0^Q8a?#VFT6 zGy3CVb>IN5{cI}{{{P`e0)nV02`JC#o9infgb+LJ7wrJlueH(DwjbyP?1pNy%(eEqh-|F`@|KoAx79I)4+J3jqQ2km;5dvc z9Ugl!Pp?e!&>f%*tp(^VvaJ5q{VnbIRQaL(wnCLp65O4l^3+q{3WUE}ek34>it{d*djkHO;;tw(@K?)^1O!p}1iJB` U+{mE_$^ZZW07*qoM6N<$f^q##@Bjb+ literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Heart_Blue_Clear.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Heart_Blue_Clear.png new file mode 100644 index 0000000000000000000000000000000000000000..ba7bd10a618a1101f238a8e0cb4ed6e9b4f876ee GIT binary patch literal 354 zcmeAS@N?(olHy`uVBq!ia0vp^3xHUGgAGVdtuOw{z`&^K>EaktG3V{Ay@E{!62}T3 z-wfW}7#tCC^Tgc87LC6nBCT7OY8yD}wA^W|YH9iK;p2bPISQ4U_iWf%lGnyEYGr?A zO}Lri;Bm2G!XidRS7xWI$G_c+b?fZ(ZhNQI^d0xIwfk!7;oz}VZNcoy-8&vHG~Sx~ z=gPD=ds88SpnKmN&dr}$VV$=1{?EM}JLf(4)f`#%cizN?39sfg_!`fiC?UqlvGNCt z&eQcS-)m-ov>f|hec;*e6|Yu)*Zw*0Ywmk(#njr_wsya_S7-jc`q1R%R1UDgoG<(` z_RU001Be1^@s6m49>f0007rNkl% zjKk$qa(7>HNl(wj_t}!W-qRu9+xyW3VHk=ef*?rj5QbrprC(OyFSman&zAzg6@y#@ zJ<%W6tBC_-?Pptw@c$1#5)gPrOF;FBzQ4T@LI|;Q{9+t{`qkGjjqNA9Be8z!TtTe} z`2Uw52?)Fr&?-3X$=_mTi>L3xs+}rfZecP01(sT|Ra9PoWuk6ue|v7j@4%lneqKKk z5O{@M1*?@eJ7n&+CG6U1ymnJw)12YH*Pp*KCM&NxS~a%!L_Hd>4Sx9?O_c5*y^>a`rMECYd_;h0s^lD z>OZ3COt7&buWK{%43FEHr&TbQJah)ALURE+i!7@DV9J~fRQay`wnSw#6Q#|$9vn6)m&=a?1YY?Bl0Ks%ytP&m00000NkvXXu0mjfoNiL` literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Heart_Red_Clear.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Heart_Red_Clear.png new file mode 100644 index 0000000000000000000000000000000000000000..2487ce3c41b38fb680d8e65a994cf68d83c58e31 GIT binary patch literal 353 zcmV-n0iOPeP)001Be1^@s6m49>f0003fNklXh2BOfRI}F{_(OhKex-rmo+}uL&ct-`Whr4q-qn;t-NFz zcD?ONt(!l#sn?H<1ca3LGoX`f*5!f6GaJ9&O2g@4-~O(e)bH8U>vxR=gjC}MtopYV ziGGoQkP1$~!uizMRG1DD5K_kXd%)`ZsAAV!Nxn4S6{qj)ch@H&gR8&Yjrt@)jRb_0 zcLF+JNs#Y?V)*$H7OO@ALaK2BI@KhIojgOWKlZOt)JQ-`wN5}sodoLo2txuw zDmVdw5F{X^Xh2BOfRLgAAw>g1iUx!f4G5_Z_`idTafo0H00000NkvXXu0mjf=FOTQ literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Heart_Yellow.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Heart_Yellow.png new file mode 100644 index 0000000000000000000000000000000000000000..9efe57ddf57f59de5c1ce9c480f2f72103de773e GIT binary patch literal 712 zcmV;(0yq7MP)001Be1^@s6m49>f0007yNkl!7A3%sjz$dVgNFsohEE%GAG5}FY}X);y4zQh@xm>hd7QSmwv9mUvB?go-YM} zFXp)hx}(2ucP9?u+RwHU;hzmZ5)cGMOF;FG9z5I&A%tk3ei#R!evSQWWBb|BVyd4y zS5PYg{@L;)0YOkAS_Nm_`GYO<0QEaz)lQXA-`Grlf$eT;6_wY&u-q`Ve~tR^d+=wC zpVyBB1VLd}!CK|5nEMTcU3)zluYW18Y0fZspU%JVDl4zDR57-{i$*eDAO0MEO17-= z^ZJp1AShY_(r-V@8uR{PCw+b1GIq-v-_>8~nJe$NJ*mvM5&U`lT(Y^gbABWs2#S_~ z<5h}zUq^#W#GE+$Jp=#W`jLPjDDHDWj-h$Zr;=4+x&4;F|G)f5KoAsmAFzKZcf{+o zU!-=RRj-Kbx|lXZ3M!e%i0A)|zf& zOFt421SL}c5lv@;odbDYTa{;c?9M!`f|=x@Ge8xZ3(#3)QT=;8b23on`}W%=QBnLz zKoAsd4ygXeza!`jH)BqeG3OdS%W+y4^nvTdR}y~$H|O!?&zh4J&5r~GL19vji6EaktG3V{A-MohkL|PMj zB3h!Zj{r|HU*-V-J=$A&Ha8i%7L+a`}hBeZ> z9GX%BK_&`IG8{bKZdz~CdjIm%be_oPd$aD(|0SfNuw<9;1-E+#EBC$kc)P1UH+y}& zBqztpKjjX^e~SOq{J&Qpb9(-<;Ch|x_4Wor0zpqXXGGoTII_dT!Q-d!g*{q(KPO%U zvRmK$XT1DA(CoGEmkC&1SvsHE-qh{b#Fn z!+Kd-LMoYNnQ&&QpAUI`{!hM$DbV!4!WULdc8HbN1G?)ECx^v^h6#%p6d)HQ_K2@}Bu>3{wdiSS?w`_A< z67HhDzT)}Dt)*7J*%lw{(5ZW}JdmWl3eFOJBwgZ5|8SfW@mE{lP(_P(tZy)W%LM`#XvBs5jf&NGcO0 z@F7!MP!_E0?~i)Vq3c-(r4X-M{MJa9(uz30{j&&a(f2=#Qz88o4|*(Ux5l@z@+E62 zvj_Z6@)I8Wv;Sg;xTKwj*ndTMwEN~c3leG5!2f(eBHdc$GzV4gR*R)1(ut!}Vv78&)rZ!~C{zKEnIxx!lso5b~g__=NNd zhJf}xCGNjDK|Nrp?3s8?Ld#Cs<-$ASd|{#-{CXeiHDT6DoB>93=qs>kvHvN0Fbpv= zP?W*|dCn}|LL^D#IKxX1xaYXx0}nVkTzocm9Fk{%+rZEYP@h6_91reG<{L24$H|!w z5BY?~jQRP70ihT{^m<$i5f*h1zqt+V0hyx<2_+)9HWAyW%2Jn{p1_QE9kDGeE{>-^ zB4D=N)??M((T{5s)!(shA0H=vpb3VwL=knD>_1ChRY0`&?vzCpJ(p>^MwM)kvk1?s za;%kJpGc10yptTa*S`h*_JyeAMpRf?raxbFU`#8$TB zhY1b(-2(ArE=r*-4^U0RkSoD&;<>T|8Zy0<0{jV{vQ^Qj7Y;DGcFb230lsL^6?W5U z?@po;u<3mh0(EO28%Q1jiOE9cPP})*}D_^ z$WpHy|0;m*G#+Kn+0bwHwY@x;=ah?9eK}NGJ`e-fxvbhL-6tyLXo}U@*~97)Wvdcu zG|)9+0#?+O=9~Cn_{LLg`!20Nn%c_e;C2a<`VSmQ-hsf``h#kYayMrh0Tm8?07Xy8qH7+{{wBmD1)3>HZ zxn0pmkMZ~Vt79ty$gdF^D0iXsNI1J>3lyxX(>rwLlfUMvE?$s;Q)DcGd96WfE}SCT z@_}2TpPscA-0Drer70fk{qSkIK6|D2)NIntdk}4TTQ^;xrO?36h@AFo3$$B9JK=c; zXKzmP9B9;i9esD+)z+d%0VRr<7^g!v%^ZR-GhQbTk6vkmAQ#j zmXu6No{go#MMLggnzRc7<}f2A8`j>~u@|oVU9j_pYhX*p5zYDs;*S`mDH3 z_jc=Ku6mjerwKRfY@;xj8V}*^Ij*s^V%aUY_rmFn(4Vz3$~x=?(CRl{MUHn=@=E>Zr#+L=Yc_)Z!qMn&q%Yf zDcYb^DiH_^l|a?z_7NhtsC4G6lEMoK0A0wG4d)erzFHrMKnQ&HCQN$X=Nad zI$@^J3v!(0+D6Tm^y0%4mpc0jB%;)YvB2%af3*KNSB$G)gK2RSwfk^TLgdY#bSUS{ z+ky^Dag!LCJqYq8L-m-Bf%U?JWCG z!2|TZ!BDP$y1x|vC}6HJM*4$zbeR;20!fqfB^z0Q=mtMVC4!yoUjz6f$1dX1QRwej zUi|#f{2}8}Sz!G7h_49PB1#Gjbz;}`@RQK>5qG9V5Y6cpm#aFx4%4AIK_I_N3SRAz z`zgB%8d&?0^{y-^D@V`6hP^Fg0w8IaZ%Um;Encl za4T;Yh^w}ouL?VS$$?Cxmdg}{bNZx;r*kCR|1ZE2Lu(VQO0T6&V%Gc-e;_E=KfYQz z6MmPd+Bz-e>b!27n_bZ>no>9~T8JB~g>b549>eF=+cZTR$9nCGmOf~$Ba zJ9i~N4`&<$ea?xcmSRCkhy;WYp@L2G3aE%-S#{H@kbUu?F%g87ZJ@A^PC6eQR$IRi z(nn3c()nr7DF6-52R9G0bozEzz2E;5{UjLW@0kSzz?^p+C%44#*g&;IsS( zDkth+S0_U3wO5-{7+z2C*ZmETX{ae(nEne(mgfy~IUbsrW6z`B?sq%MUkn?_=l5EF W`mpbEDk0NJ`*sET)_fcH{r>~VMIhM# literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_05_02.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_05_02.png new file mode 100644 index 0000000000000000000000000000000000000000..1ff5c3e4b033b69d4893f7b0ed5363309e4eaa91 GIT binary patch literal 3634 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 literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Blue_3.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Blue_3.png new file mode 100644 index 0000000000000000000000000000000000000000..bfe8a384f95199aeb5e8d91ba3d61b4a44a952cc GIT binary patch literal 188 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJW=|K#kciggKmY&RA9i3`#<;ci z3gcqF1RzihQ%wHtzUJoBdR2R6RX&M7iVWX(vgOJc{BUMq4zN^Vvte z#nXdo{B>*~5bxKvFjh!NmZ^X@L-C`1!H(;!FBrmpKU^j2$;vFpq_UhbtC^9%F_68- eDgk5_14H*zdB&-d{TG4GVeoYIb6Mw<&;$UQh&nC+ literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Blue_4.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Blue_4.png new file mode 100644 index 0000000000000000000000000000000000000000..3e3afc6af6f780ef808fc950fab517a8b5e50fe4 GIT binary patch literal 145 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJ2u~NskciggKmY&RA9i3`#<;ci z3gcqF1RzihQ%wHtzUJoBdR2R6RX&M7iVWX(vgOJc{BUMq4zN^VvtexES$;2(9{_Ks)`~R~#Se^#m*?Xi>AUVeI`mV363v{I% zWOx<&*aX~}J8q}ce-_xMFaFLjMOOc5ZQaFRDh)gSv1~YZTU1W|_R8Al-?x1L3H?cD z*!+Lv|BAnVYOMM9&zkgWtIirbPKCS*rX^ASEL{^A9-R_CU^DmK&jMGb4(^-(**&f= z{&r=)-@nDT_w9PS|2~V~+JE0dp8GBk7IKKGW13MSlq34Uv@&>o{mL)V4M4#=A6N`t zs#N&j3weEg)$0dqS>pVyulvGvb97z&?qHbg2j9=XWyD1)KUq`?B_gYRtXCZn zJF0C_p~k-cL14*etJ0V9Dnc1D9=!RPQ`{bIb+CN?#Pj*D?;Z+59-3dD%@y9e{q($( zBH{(+4zFY!Qkw<#pIUX4Rrnp-n{tMpL%%C)?ee={7V}Cy@Nnz-bAZ7jH~n0p`GWI| z2c9$L*{=VivHH^g1&YiKvmL}k!uX_27Ef``q9#ZA`WRYM?qj}-&M Y3KQ{{vTlqXKsPXWy85}Sb4q9e07j`dfB*mh literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Orange_4.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Orange_4.png new file mode 100644 index 0000000000000000000000000000000000000000..c11a4774fc9d4ab84429cd6c5bc91c5c3a225066 GIT binary patch literal 143 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJFi#i9kcif|*9^IU60M27ewS|q zT4jW9S*a@3#MN1se(*edQ&!re$b+R;3=B#_)2+MDonJcZ9fO?5g8X?PDq&5H*n@dM gqfXfa2@t5^&yf1T_0}c7aW#nR>FVdQ&MBb@0D0RimH+?% literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Red.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Red.png new file mode 100644 index 0000000000000000000000000000000000000000..b218732891ed812f2c0fde1f3d7cbb4604c99ef2 GIT binary patch literal 345 zcmeAS@N?(olHy`uVBq!ia0vp^4M42G!3HF6DHW(PFfb~3x;TbZ%z1ljBkv&t0oH(? z2@#B3%TBO;*}%-3Gtn{QMx(6_%c2I+6+RO!f_Q>ttkczh{x7vSG5ygyZ9a|UGff4$ z#rGMW9%W-V4nx~N{e4ur_@A8kx%|a7>yA&lC%@HBm?0*e>Bc$jYlYXHt+!^^XBMwN z&%*HhFHeH#?%e8!KUZDL|9)+go$TTFtNi~Sc@{RGlVM#s(~cs4mZOPG4c&eXZk3Wb z+FtFP41PcV$se%oH}n5{?#b(4Aq(uUK3{mBrElH8e-obgE)Y>;uz1h1p-yd~9jk}$ zy%(?TFZ~fyXa$=V@lM6V|6auF<*U?z0(usVYwk>*u>0Q?L&gKH>I}Zp!VEIf=;)64 Yy6w%~QudX5fI-IK>FVdQ&MBb@0AYiO`2YX_ literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Red_1.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Red_1.png new file mode 100644 index 0000000000000000000000000000000000000000..874859cb5fd6f94a47af1d0fca44dfd89ee22381 GIT binary patch literal 242 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJot`d^ArY;~fByfsKkUG?jB#u2 z6~@JU2|%D2_E3k@ZOu*9w@2&UlAaVcGV(LJIXz`KlYZ>Yji=}TwOnX7TyXD0Uh=PhP^fo z%d1T0&u@Lk<@8gYt<8_)0E3%{Lowq@yY|L~;!Fk&azaaf$*{HM0VQ-)Jic1;0L@q+ j!c)P~W0e4MAOpjpOwV<1%__5io?!5F^>bP0l+XkK?xa`X literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Red_2.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Red_2.png new file mode 100644 index 0000000000000000000000000000000000000000..a07d4004d417eec178c395eeb2aec6222d442c1a GIT binary patch literal 216 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJ`JOJ0ArY;~fByfsKkUG?jB#u2 z6~@JU2|%D2_E3k@ZOu*9w@2&Uykrc1I5RvePAkTVzmdK II;Vst0LYe0KmY&$ literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Red_3.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Red_3.png new file mode 100644 index 0000000000000000000000000000000000000000..ec626ce5f895d09baa19827013d10275377aaa20 GIT binary patch literal 192 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJc25__kciggKmY&RA9i3`#<;ci z3gcqF1Rzihd#JXKE=Y&I-zvIegh znszdTT{`G}==ga_2_Wcsp1-IIBq`yUE4Xla&oTb?idcstjspxjDs45n9%UQ>+&mQ= hJyr=I%Opw}7*44x-nnPWv;pWO22WQ%mvv4FO#nhcKt%um literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Red_4.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Red_4.png new file mode 100644 index 0000000000000000000000000000000000000000..957c8d0e9baa509a8c59d19c3a7ceae69152a6bf GIT binary patch literal 149 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJ7*7|+kciggKmY&RA9i3`#<;ci z3gcqF1Rzihd#JXKE=Y&I-zvIegh tj$QnAUWH9LFI|!&$Jh(|R$=G@Eqvv`H>K?U9Bht|W2G%nHjkJK4Y-y{HL{BSBi) zAw9lSQdm;zus36dN|r`*i*j0&ikMWqD3Br|;DdXf)!AS5$G!L5-@U(=^Z9-6J-Ic0 z(`=tbJ_rP2HYt`!f!2%ANS);c&6UB~6$pgqB#B7);n;c8d0@XEVaY}vhfpuVw6X65 zaPrG9V+hw1V<|J+Df#(5h7M)OY3`bW?FDBx%q2t-FesAm+Py)X-@-WsrK_qs%&YrH z{(d|Huw+sTeiQA8Zr34e)2&?Q1)8+(AS2S>6}?&7XWF+e6gS!T3ft-KLUd}+I8L}X zPUqpf?d|mnu7o&c8avBg7AIi8jmXiN7GEi$e2>%A2OG6DI$??;sjS$xM3;h^>@x%- z2_;38T1y0L`Qg$pMhA<17O8N1jQ3+8m+#7|=ck!PV@@79K1IQ@(6wQ0mUHr}LQl^G z%J5%1v(NHO!J${LA6l=O$a&%IV?zu1+XnQb3B!{~c>gET(*Tbee2(rGG3R24q`6zC z`z(9MdbV2@Q|E~6So(N#OKzGnr3&Ti0D?wE$1zcyb2D7YV~4dWvtO(7pbCS+NfjzHA{5a>_Ka=f2-}bTPd>L zD;!6(qiSlG#u?vI6~^Floe^Bns63#_o-?t#WC$B|6t8dYoSOH{Ns7i?sb+otyAUPW zRp_+>`XYbuB2dA(J>Id95L=q;ie2jm#kgHQ8ezsgp)Yo#=4rdk9i^TU_0Rgwj9Mz- zon@K`u`5VD47mJ2S*i|MrA~OR-u4#FXI%s#07F~uy^c-`DlcgndnbKBp21nwhAKZl zsGo7|z?)atgRtAa_-OW5%aH^>#k7o02K<@Hu=OdLPwj#4k9!O)ut8cVkYY1+0 zDrA52e&7P#UF#a{HH)zruSMxV>di4_FS$Qqk2Z0g3z_GBMSeD+ZB&ziWw$x+Xyw)- zJ6&RPgO3L0C;oA+{arB))G%A`I1JwT+s??aPb>IyjJ;$Y^*Nle!-cf#z1xKvc%x+A zyEeH{Q%B0|42|i~t2b=_SZ$v?;oEfQ^x&$81{=jty;aJ3}v>@8fg$75i`3-vb5LZU>WhH}*Nt1Tm zV*~Dy2Ca`JE;wuwKL{I!MRFfx|0*=8j<5XR3-zB7Hj)z6|UochsjZ33aNpUpCRZ&p{R8j4#T4E)FN4U6k|)Y-9s@HYI;KTQUrN`z!r!a6ZFt&ejsx}dKRL5hhdN;af& F{snoJtbzal literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Blue_1.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Blue_1.png new file mode 100644 index 0000000000000000000000000000000000000000..d65ec2d154621e6d4d387531a6905fc606b5d2f8 GIT binary patch literal 404 zcmV;F0c-w=P) zu}%U(5QcY+z4{h*geV2^1t<&^iH|_2?>VTE_yimc9Y{0RrPjm{h6lc~gKg^b5Yq$i*s)SO@1 zg}cTbAMRznGv`n~1RzbK+mY^NBv`k{oI}Y8c=laTCPY@puHDX* yKY=n)kPrm(0000 zu}Z{15QhIfv{vEld;q)S&=g{2Zz=c)VtvnXjg21n03uvzF9>20MeKZl0~; z(2gGN?*IU&!*{cgdD+pm89yIvl=3_htrG|UDxE(LKFq?M;W_|dbt^ODpVJ(`4~gBq zeE>k`K!}k<>jdU@!JPALyO7Sin_V;hhHSQ;kjN+YI_VJLyLY8Xc+PeFby~fCIt19I zfW5C5mA8I61ZsO=L8adVR6gO1mr185VO+Zf0EDy4a^Xw)%1O!fk|hxMMa0u;bS!~> zPiD9~8F_V_^9*=;mim(`i6?g&7CoT9@kWp*uj>i#osTbMzkAN9bO=zIM87B9>yhxf zz0Nt+I)UK*7ZeFKyW_Xr?kRl&MWQ01)zmq)JOqjY=U=d?b87hr{Iyyq@D0`MyZj~R R_YD95002ovPDHLkV1oD)z$pL# literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Blue_3.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Blue_3.png new file mode 100644 index 0000000000000000000000000000000000000000..4dbc2161abef1d2dccdd73e04e6d74ab81457c08 GIT binary patch literal 425 zcmV;a0apHrP) zy-EX75QWbTTB{H{AHXgwx`kL-3b7N!ClKpAM_M)b1VT!|QV_&qmEb!B(g@lMUW>`B zWHPhYCE`v_nfW2-+qrXhLyQrcLI`?bsu*LalV_<_75opa$RF6z*L}CLux_43!VdxI z==tdp0C3)aR|`2WKfYDtm%WXdJc~r*1Ok9c=P$hvwQ#4u4ggr)%GLPixB&1&Vs~#J z0MI%hVoaiO0&}Ne&iS@p$j;lFZ8iReeA|UIz4cR&00i#!{Kf9s0yW6mw0r`$M0R8jSOt9oLzrLdLZaHW9A;6L(vJOqJB!TP@ zfIaOg=d8vF1naw?Oh}xL-%h(F{{+fJK|-pobJp?@C<}~l!Me^_%SYg^)i{A~lLx%0 TCxvoo00000NkvXXu0mjfXAr zy-EX75QWbT+AG9D@B!>3=mx~fQm_3YJzPxJvLH0%-*81+T?q zHtb|(H)|w2Ipxj|d%m6fvmwR^O(6t5Fj0&#RLQf{C<-n^Yw`g*`guGpEUcPmk?=!6 zI(mJ60svfgKh;9cOU~}qc(1dW%d<$-P9Oj%bpF=)QVaLH8vuayokWd)k5T|r5(kGz z0D#6Z5n~dy6PQ^AGtPJQLU!KVYO3)MB$IwbA|2>`{1D)8Z%UA`oXhxlRNUVW0iIGo zU)PJu>hFg@=?p9=|1-c!2fWAAPPg4U`BUTZ@FCaFErGx{B0jD9=M-q|(H_n_<0b({9N>fhs&m%r5%_P_PT&{uf3;rLl4!92 O000030L(}K_WR(~NUAzqAu)JD0NqDtjvr-U zU|?X#?0t__BTSuX$QG>f(~AXB)Zq$=0S^I21_lPaK4+CN#j5f5lhZ`W!$QKu(wc#R zfq{X^5fWmskQnd~z{myYJ{J)c!K(2M199@1y^xSNy#~7j22TjU;vNQY`HWQg=TESk z4+DcI1YoI@fdPBIrWD{ZfAE9=t~jC=82o7f22QU*E#oB(J;^fo`T6ZA<_+2qV1%`Z zV71lEaEOGVKBNqXm5juO1kC5KI^g_8i2n4I5cxq@8^Y57dK+&h1H|RTrU~>opFIPj z9+%HyV8BCwkx;AnR2yo2jvf-YQoC6dBm@V2s~Bepz!eb49mJ+~Tww5r09+wjNT35! weGUUdCj{UcaP$R9^*Ia-y%3-_81N7P0QZ}<9bT{H2mk;807*qoM6N<$g6D*&1poj5 literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Red_1.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Red_1.png new file mode 100644 index 0000000000000000000000000000000000000000..9d96a4298be11c703e28a7499696989c34a67b17 GIT binary patch literal 420 zcmV;V0bBlwP) zJxc^J5Qg6zg0-cEzd__w5G#AJ60B|Ru(q?Xw6oUJ!dkdW?5#xbT*TjCVQDRhv6y`W z$!7Kg#GQM)osY2d>|~O4-h1TaoXa~(dGB49I8B2(=YMD+HL#+ehuzx5u5lU(I|R~- zUY;HS07vJa*+j-|?B8eo7spFcoJOK|0uDfJ<)ccMh_1fT%1;vOTiXDD!Hy6kiQWlR z-aoB;xR_5Ip`D-W&8GP{I|TUcH4RsmW5fH0*l+G4b8)zr&DYkqLx5ch)RpF)B~zc$F0Kb_W(7U#xtJ#xzhDXr08(DyMoU;F@m5&u8&@9!Qkzv~J%# z5rXD##VUai(6Bq&cDp9~1VUg!B30MQsrexg5|n?zu9Z{sN8qp3JAq#Tc*5{sT_rsL O0000 zu}T9$5QhH^2C)pBprFqna-JYo_LhQgkYH_Rk+!^2Iz{?l_kX~E?0Is_or*3HC!*{yo zNy+O)OF;37h`TsocDmIXR7a9(*5XmL)!!X8Ltoe=Vd)1J@fNZ z%9ot8YA2w|PQ?$$?tX4b7_rcKo*6CB;v`CVC`%Rdvpq9RjvM l_!g|{oHcs{{#vyY_y%-x!AkB?qtXBX002ovPDHLkV1nl>(BJ?7 literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Red_3.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Red_3.png new file mode 100644 index 0000000000000000000000000000000000000000..eebcf6d4fe031ea27c1f792fd1ef8b1279883ce0 GIT binary patch literal 439 zcmV;o0Z9IdP) zze>YU6vn?>EaE6cP|(%2l$b%B90ff!K0&Y&uwRLb7I*H)uBn}aZ&tM^o-~%XL zhm&(9x921c6?6M7Avu4@_shw>iLur~k}*aO*w=yqrg*H*^!?E|qPJLf%M{7UsFw-gU z&q~8U5XQeP7V%h!U_c*1q|^xF$)n&=@C_8tz4-v%JlE-}ZVU#y=)0fFBau zyL$kD);)Eo#*k4 z%yd%TKO6!)rGWfi&nvBeI0UL@U|xkk1FUr9uJQ5a!LJ%m$M;UZUW9;{;6AsrdQA z-Oqgz+O+SS6T#q5#VUb3q2leBb=ozBPase9NcgJjoV7Rv@&e^su~;t}|3HBR6g Xhj+7kc(sAd00000NkvXXu0mjfR0PI_ literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Red_5.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Red_5.png new file mode 100644 index 0000000000000000000000000000000000000000..03987ffce338fffd308329ac71f0fa4b8560b802 GIT binary patch literal 402 zcmV;D0d4+?P)30L(}K_WR(~NUAzqAu)JD0NqDtjvr-U zU|?X#?0t__BTSuX$QG>f(~AXB)Zq$=0S^I21_lPaK4+CN#j5f5lhZ`W!$QKu(wc#R zfq{X^5fWmskQnd~z{myYJ{J)c!K(2M199@1y^xSNy#~7j22TjU;vNQY`HWQg=TESk z4+DcI1YoI@fdPBIrWD{ZfAE9=t~jC=82o7f22QU*E#oB(J;^fo`T6ZA<_+2qV1%`Z zV71lEaEOGVKBNqXm5juO1kC5KI^g_8i2n4I5cxq@8^Y57dK+&h1H|RTrU~>opFIPj z9+%HyV8BCwkx;AnR2yo2jvf-YQoC6dBm@V2s~Bepz!eb49mJ+~Tww5r09+wjNT35! weGUUdCj{UcaP$R9^*Ia-y%3-_81N7P0QZ}<9bT{H2mk;807*qoM6N<$g6D*&1poj5 literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Yellow_1.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Yellow_1.png new file mode 100644 index 0000000000000000000000000000000000000000..75e283f640c4749d2ad0ce19bd1f79fabf79865d GIT binary patch literal 407 zcmV;I0cie-P) zJxT;Y5QSeaUchUZEGuImf?#eacmRXN*mD@`ARa*2&|DD2UFkMv*^>uZ1cZe~3)xClCO1W?r58fyFBZ*-4@}J^=vC&Jr<_n4Cas7ql`z z@B=FhvlsPz>y_`94*{;d)S(!^D!2b1m#&+dKOX|DQo!HWEh!>GYW{o(^wxmV#$R)* z{A++(uTn=W>K62mwo4k;Cujfw002ovPDHLkV1iA2 Bv339e literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Yellow_2.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Yellow_2.png new file mode 100644 index 0000000000000000000000000000000000000000..3893ab6e9415caa8f57fb6bc4658f376ef28d399 GIT binary patch literal 432 zcmV;h0Z;ykP) zze)o^5XQd^$pZ*ymnX0Zk-I_!A-$#G16YLAK8Lj)K7o)G%$-NX+kDNEoj+uLH#>W0thJD2jFAma)>_lXPgAeX_#f(r8(h)X?O|z-;ud|M536>dEz0!&tBDT^=I-RSMKeO%-ga) ze>?=ZO9A;@uPZHoJOmnhU`@r}1FT|B6ESr&z6>2sasa^fY+Buabn6Q(JSjO}v;+*l z5%F!6ACqA`q8YAEMxF~w<$MP`JyZPjiuR+s4Sf%U?|3=rpEve|^2{$U$X~0R6%PSc zNMbyq?$t zPfEi;6vn?P_5eb<5IljK5=jnW@kn_@$dog}}xmn56NOVrX0H}3d?8}A86Mgo#Q!)NI&jI|9 zNJe`AfZhQSV-lScSXu>3&UfU({u?=u29X$lN9y_!iF_jW35NjRdsVyD%V;V)FXN8P zVqV`r90EM0fV{3(mDWET0*y1UqQaj6Rz9H}F>!l*;U}DB?#o?m7m$=o%0zmd!_pGTZ#vF8kRHQKjUSff8IC~PX0Z=8B@OIoD~iM zmQNxc()9WywqnZHroG8It8)TIdoL&x8dk?|r(IL{1j zze)o^5XQd^<^hDW2>1jx61fvZ5G(_hf{!59c0Pcebqqd%kW#P|1d(`w;0ss<(g+ro zf@?81E4P`s%M$T6UlT6#hnwHd?(7+BEhHIZWJ4iqtqJkd)T%Q6ht~W*xT5ceqsqWg zKaE5@1pJD|&rbk=v#Sp=ko~gbJJH@hUoZJ-BpN4R08~~!K9B>`NBV7VW}^LTngjSH zk?!pS09uDcj7c<3U~U)8RlX$$w%@4oWFryn@5qXLL?R!`dEz0!Yp-gj{Fsd7%ALI+ zGn-cDkB0zvDIo9bWu@hhhd^x)EUEZ=fRzvFL`;h_zVq25!ze8AY;s@97cBw9HzJ-^ zgA+1zx9JS$ld;q7EtHTe=NT}0rTEiZvPU-!T@UzoyaDLVc@25)Yb5+NzquxVt#Vf5 z1Wetn_~o)Z&wUbFx9?sM!RT+rDgozEvpW{;c1`gUaE=}cU!j$=R)>HKP`(92D`%}9 efxlMc1bzUYp|30L(}K_WR(~NUAzqAu)JD0NqDtjvr-U zU|?X#?0t__BTSuX$QG>f(~AXB)Zq$=0S^I21_lPaK4+CN#j5f5lhZ`W!$QKu(wc#R zfq{X^5fWmskQnd~z{myYJ{J)c!K(2M199@1y^xSNy#~7j22TjU;vNQY`HWQg=TESk z4+DcI1YoI@fdPBIrWD{ZfAE9=t~jC=82o7f22QU*E#oB(J;^fo`T6ZA<_+2qV1%`Z zV71lEaEOGVKBNqXm5juO1kC5KI^g_8i2n4I5cxq@8^Y57dK+&h1H|RTrU~>opFIPj z9+%HyV8BCwkx;AnR2yo2jvf-YQoC6dBm@V2s~Bepz!eb49mJ+~Tww5r09+wjNT35! weGUUdCj{UcaP$R9^*Ia-y%3-_81N7P0QZ}<9bT{H2mk;807*qoM6N<$g6D*&1poj5 literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory.png new file mode 100644 index 0000000000000000000000000000000000000000..61be73c831671fe32bd39b18f53786ae94e4d931 GIT binary patch literal 2480 zcmaKuYdBPE8^@m^N;Yj3p`=t>CCautpc<7!@j9f8QjXOqQ*@Boj5%~9IV6#aAretD zP2&_9r*b}SF&G+UGE-)n%wSAr%wCJ$YkzpJ{eD^3y6&}}=lS3F{k#8b-Ewnr&{(Ly z5CEWYV85+900`(7R4q`2uA_nqGyrPY1Gak(|CuHjbP6(XSXwuB$$6XK9jyN`-GGmG z604V78}Yd+$y7PL$mc3o$@v$(8}lsvWW7B5sE1{6A*5FD= zl+L~`2%xHldN7jc88@&3e@||L9jAEl?g;#9qlrKzQIhY;bXh2Lb~Oetnj~MRPK~AG z8XEfowX|Mav4v56RImE$9Am}c1yrN!e6U^zuT$kAjVG_r9xz2(P`uSJYmidOFeNcV z6S!?EQ!d@*`r7yMS#RktxZ}9Wpw&pJFx~=3umUzT&Q?-qVL!Ehcqe{_|3Rpjnv_=y z?AwJ+pFcH2yI=q?Iz>gqW--nEHlT05G$Ha@%!3G?l)qT5ciEEZLSIj7qyfl0GARdl zjIkqz9{iR-r*t(3f38NWu%Cj04>503I;>w$;ZXEGY!^qNW;#VB_JlSi5dBu)<`m_` zkh=x4PN%%ME5bAy(i^Lpxfo3tFdi3XX~T=HNKFAqMDapqxXdf#`aXR)Mp4Pz>GIXM zCLr!%W%5BW*GF9|27>^$Cp0n_Gif0_W8FGCO&*L_g*TT+8F?Zs9AKv8b|| zgoo@r{I-+hvZ4#7fOI-dCGQUx-hWVEk6;UBW<~J=hvp2{#|sXtKsQZw3vUOl0g0aJ zjnAFXUjSv=8_X(xxh#SlAMs=4T%7z;*;METBLR7dD?xPo`Tf~~mnxU`RUJ}0cE&?` zj$>TeU?u$mv6N*^dZb~H+;MpAG^V!1>kCZ9F%QCWY2)VKiF3Ws92(J}nD}pgVNw|< z;|MJY!LxC`Ca8MD`(GSm|39t&@|ecURc4_pO@hosNtN8G0c~BIR~${hd&b$R@{S)T zbs_q$8!IFBS9M)klb9hh8s+3FgQe;|%HKU0p7on=3kUYOwC%nf-Ow#1AX^CCR==%< zd7?YcYUR-Zr3pLE$r}omKDZ!oJObrcX^4%R#ktdzRa_p1dc^xe=z6JrdGgeO?FH6n?^h-Hfdyk9$7Ksm{;j zWV}XI9$sgaHcb13W>rbPVfo-DM~}%qqGnD@1Zvf|q^DpXEVZBq!D&05$@fA2T+(>1snilkM>$mxt zX}fBPf0igD@5Y9&F>^kQwiTtze{?YWs3rB-bAjj?iQyu46di!~S;|$2F0E9$5}lgD zvMg+$+6L`FaA;N|1`B+Lk=!trcfQD9AfA;ceJ=IB;%(u@Hy3Vl7pDr`nZI6q z@#^VI$9OLo-~0@f{po!U-YSa>>dDoC&6Q0w@kYy_$1wNBszX*`mkn5D=(0YhYJl~X zgCfdB{+oYRo^L(L(4$B;{&|Ms;&<gX=tbq=%gyD2KC@hc+> zF^EX}N}Yh@Zt>50_3$^@e6jx25|ly*1uLwnce8cicA>HJ@^{hW!c1*2ITFnKG#aY$ zpgLT}5^JU8ZOunXy_nT|<9^_(d*KvHP0r_DaHQUf{cqqd6SUBIE^TIZww&=+*46UQ z*s(972;@pbzGuN!Qn#4;PySb}f%r|hSQakc8G-xl>^rh_Wd&nLZ~I?`-1>*`?paI_ zY4P|$VoUzGz&v$DB>UY&1arEWEQOl*`FYLHTCvWtQY5NO3;)d(3E`g2A=qtCYfs}- z*4(#r3gGhOlHcLa9F0GXI8Dtf7%53eZ;iED-UC)bldwLeLY4Ho52HJh+DEoL)E-<~ ztd1wLr~8MeKd_&x=oW;Xe+&!ZfqQgMd25UG^QfzqR=9%J6|XJ&I5@*zRv&p$v>V1` zr``giPh@Fe#%kv%@g9N?F^aB)g(;z_W~K>~Vhcin8GUX9Ck^*2f4mvm&J2Qo$PU=K K*i!cTUHu!2^Tr4O literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_9Slices.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_9Slices.png new file mode 100644 index 0000000000000000000000000000000000000000..60b10b75ea5e204ddfc0fbfc55490e0857af5ee8 GIT binary patch literal 489 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEVBFy8;uumf=k07mzrzj!uK(S3 zIxH#ZwvR8COo_T~zFA0U>du%NJ3R)5^ziEp4L8pobHC{DXU{z*hJE+$ zGcbtrS2L72JZ*S?_g_J%n}^c1t?i2z->=zvZqi9>1;^0FudzQXcST)M^U9mxajM+0 z?)sB=mp}hLclq@y`Z#udBfQ@;;+l`-W z3U*p;zwkM~=FK*SS+6f#VBE<#*LAx3>4Qgk9M^SmlsM!q$bR3$`mBLZf+3&r-j{Vu z$*rLanAr|QH<Zm;h*dU)?LFP&>p$^Iv_@y=}fY4;wQzN(#g=!xuw6-={|3my7S cT0aoq^Z4+EyAnz-fKkZc>FVdQ&MBb@0D2SME&u=k literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Example_01.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Example_01.png new file mode 100644 index 0000000000000000000000000000000000000000..f2f6b4aa3b7f07db1f3e61fc879bbecb09dcae0e GIT binary patch literal 1876 zcmYjSc|6PRqj+?8q%5nAM zGBPqM&^wOzf%9KSOHl#X^G3prWn_+NKpnq@C)V(1TrhBF)virQk9_f4V|%0LJ=*=p z4XW>K+>}fBC`*4*)CB=uhyrlsy01Hz?=6;(Mk)n1Ird_(of;URxZsoHeBz)txC(x2d9&z z`&%obJiY2;y_~D5PkEhnyJEN?X_a?Zv%JuWkruE<;ud4+Enq@ z#+Qu4r4`$g28{hpy$Mmaf5Qy{SKm?_#DqtyRGu2?iS6~bicCD*o$gyfn3Dr4YVkcb zdKly_dihZLQ!YNyJhC!%5k(=tvq_bRiy~W5Y!)8Mz4_0@@?zOwQsU|J(D22LJ?22u zSaL~h7&t+b810;&u~5~nFi6N@B?m`t(pT|#2QC`PJ9+^l$d2t1roux?A>LI9*oV<+ z(jh8|ez3PLq|Q+a{OuJo4aBhQ8|}Yq|4A(9Ncb8mF6=Oy-AW>ER#0BN0Lx{OR%GiY z=cMk~`pK|DINJCU)Fh+Sm(^ExV9i*>q}cI9cX^Rg7pOvJ<8o8*tHlhIEOh zo`NK58ic{ncT`oNiW_KcXAQLsE{8!V7FCg`?35UBk0os&dyd7~A z=;LhvEDZiH`Swuo&U<>Bx&pazMRrM05Hn780VhYO37cjcy{^C_R ziGh7k&WPI^{2e>@4zSD5{lHb5#FbhgUE;R%Qz_9@upIlxz>@*kFfqv{ERL{&rbL^Z zP-Jsw&ot23>uX*G)#|EbQ+9e&T%@kRj27(yV$#;iiak33s5EI-9WWrA39dl|Enqqa z=gN(RF~J|opYvdV zYtlhd{vJ9iVNPvm55eT?&+MRNBWO6C=pFZI%B#?ee1oA z{*Sg=RKECtqk3w}#iP9O~_Pl+%wwDM#p=3UXsYDA7k4OeA}Akuw@O9WH1t81T7KlKIvue%O7Q@e_Tz zwAbaCs?oOrAXv(y|Er^148EuN2sgQz>({=h3o`n77kO^I3WQojo}gy`#>m0{3&5G>_lRF3Z|Bwx8 zvL^d(tu1Bzd^U+*-%umRH~ly5KFQa)M(}{{i2*=m@P zDuRpmam^nJW{c09BTavIEQeMr`h@Os$9W0QzP4TErEm%jn}EBL^3o32^2ghQA4~oE zsczQ#JO6C+q#YR08-vHy&GV)O^}>Uk;Fb#E4N$lHf|7;Gd^Xq>Rsn5rG};~}p{qhD zY1K-b-=ORbS9wN=M|5nKHO%%^|JQuek`G*-YRl~f0!`dL6A*~6ebJDnT^xOj9_I7f zF<3uq7D@?I33Hg1Yl=VbuVq7Pc{p$s+U=L?Uk>MqXlaF!%A}!Tz6edCh+0~ZT#_~( zeR&`@a7$|EEapvtj50NlsEA!%S?%d`zBv+T&K{z&PzIcopC6Sfyp(j@r%6~yziwjI zCUG=;Vxm9=A-(=dM7whf6KZB*rKm9p0W z{SkByFybAM{+?$jRRH*v0;tiqzBkGF3D4z?BHxcm7*84^e#ssiz{7PZI zjOhA85m0|6^3!BsaR@kE&$uDis6&$MQ5OG@QQIdoyTfMW>y*8N3uh40Hjm3Dwff4y z8R_ypFkaM=CrSR+Fbs1)6uM-Ty#wkoMDLBIY+e3mKhttZpq^VQZf@IZWs~0K* zf?1sw!4sC%_CAmK0bsQ!T6&(BQO^nw;zVI}dP9ODJir%@6Q1RPr($ENC~H`JDN6X3 zeZ&{m8%FpgB0943KK~z1@fc{8@pjEAdiA(2j;8wpaiJ3*K_(B~qmf(gj0RArDx3<) zCtC(0X|&b8-0;=Eiosvw7=Q4@@}qupfPst8XZEl~MNt!n5G`0V?Uiok>pM3|J0*odm*UKK4n^wkQ zdYQ$i>K@$s+MBf(6lim07bi;|92S(?o!r9b7FLtd@B&nhjNs_ZgKraDPVT=Sup5#8 aJGTj`f=tZyKZg8KsEFe+1mIXk*?#~Y1fBH& literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Example_03.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Example_03.png new file mode 100644 index 0000000000000000000000000000000000000000..2293e915dcf9473f0349610cc8831ebcaa5b641c GIT binary patch literal 1413 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 F{{h(6jx7KH literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_1.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_1.png new file mode 100644 index 0000000000000000000000000000000000000000..5e9b605331e4f83f810a8bc3c4c2ca4bd5c2231d GIT binary patch literal 429 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEU@Y-;aSW-L^LDml7L%bwYy1&z zkvEoiA~Fj@3zRFA_#bq(+O>pUZp=yj1wv*72LgGFIkq#cmGbY`*!|s{;pcOfDGYN3GG&UH z&b9u3G9}{G3HB)scmDIdT$1rLMSVfpEdK_n17Zn_**37=0GpY`5Z!QA@R{D~tLgdg QKoP>=>FVdQ&MBb@0L2u%GXMYp literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_10.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_10.png new file mode 100644 index 0000000000000000000000000000000000000000..0426bf3d5b343a8e745801e48e9b5e166bedc1b4 GIT binary patch literal 508 zcmV&!E za=!*3Pr~REP}~D@&fi>K_b#rB`SKkAuvzVN1YOdezywGOQqE7$raj8a6UL2`AcYB# z7VOvQUDqvjes*y#8t41-tMm1E+*nsBOn|f?cO@4ibv~}kB_SZy6<`9S1*PA$*!fZ? zoqz8{c7>U>bi#N0_HP|jHa&fd$aMe&+0w-)6FK^{M_p^ z=Su0l>YkaMTN$z~7_Kp`dB_mKy`hacLHB?ZUjtCkFN^=d9OJoORqtbcZG{7(B*x9mWf61K%i%mEe`!`&+T6LfOO3>>XU7v<+3fpfVYNX=WN>dOes|+%n6Zr=x5%Y6>_?1k-+l`KTFUfxPIQsk_jeb(!W)3D zd{TD%PWI0A+4IU9o%r6ZEzo?!*qXAQ-{Y^9O?^$)LjRPnpLiD3&1DciAgZM7KDXij zA?8Tcuvb94R{T9L5Z)zcd6m(wu{gP20JrVJw4Ab@7pCz z%{;1JS9ec3A;yR?Ghqv0qXe)8uu%fo0@x@4YyoVP0JZ=&N2NCiAyAi+F~(35F9&!7 zQ{}&(=Kz4s>k9zj^5*u>&x!PXj@RH-;*2`l6J10!; zRsify7`+2J_kgy_Ute7HE^Z&Q#Tx)%z1-?Zx~x4x4zLE~DnC7$_9&~J^xUQstjGb@ zfZaU3?Ycu(KGA$`Q&(5y0Bb7rI6}85x2RJEy zACRM2L2nQ6I;bmhfHlC|ZEK@hz=;yT7QjXcU<+VVZ5@!C$J^EeL(TsQIlvm=U31q) zdjTiadO*TDriU8)2|2(T;9YZDX8|kP3pg2k7N|A0H48Y=`+%Nx92}j`2ekb{Xjdni00000NkvXXu0mjf*X7j5 literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_4.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_4.png new file mode 100644 index 0000000000000000000000000000000000000000..d8de4c644a867e3a4c1790ded0e1a18ce3d3fd75 GIT binary patch literal 508 zcmV(#zh$m!8DoV<&8DpZ`YD} z?_=hBljJpZ-h1@S#9DwgOn|ijYnT9Q0oE`9)&i_y0;~mCb5MH$=bY(M(tGb};^~0W zK&kxK;~W6+`uqd{IKR67^|K(~$B2!OT~~CJI-z?9IPGpQR{7cDb6{z^SuYO(Y@IN< zSpm>Hp?e2p_kdL8FV8Lp7q^Sq;spS(UT$=VE^SYs1GE9L%1@7`1IkJx_YIw(g$~dL zZ0G4s*B!g^LU`X$S1xpbHXyZz?#wb0=a0rKlcKKjJ6I4oe*<`cK28d9iR>1Uf`e6Y!;|}hWSQlB%1dC?fhT5 zCtwdKB>Q|o>eu1P&;dLl(m84e`~?1yQ(~o+7wCZ6{|xb@=$=#U yfPJTX2QUHF0<2*ItOZ!Z1Xv5Oh6%71V9htz*5Tc_Veh2?0000iV98yM5-swHtJ3oiKU_IO*45s`9hNS7T}WSuc+P?3^&U zTLF+eVe}3t9s_cfzrMI?E^Z&Q#Tx)%z1->ux};+Q9Uu)zRepLhZ73^G=r?wP6gof} zuzOE$N8O<-ABg*nb(KN~NCR>+xg4kGXM3-IzJI9N)#{=HqyeSRTBvlWXnfo{j$?q5 z>h*w__!%#)XzISy^;XaU(g5vfo2zi@d~aQJz|bA$#(<%r1A2Fm8`Y(8t)T;YHC$W4 z+*_j+bbvGE-`K819U*`e}=eI^uVch yz@amG2QUHF0<2*ItOZ!Z1Xv5Oh6%71V9gKeKk;T+`z;L|cG1On|lkZI}RU0opJD+5)s;0<;BabCf%PF-G-O(pqbB;^~0Y zKv(&%%>n@M`uqd{xV*mk^RvT!@56V!Z(OG%)d}4@z(`kvzRJ&+pM|9zX1h8Duy?}b zb`3!8gzg;>-2+0Ezq*(e7k7yH@&y2}UF~!@UDBRF2S@{am7ktW3(5-P?mKjX6gof} zu%D-QU3cutJBIfi>WYO9kOqX_Ij z6%09vYk@?n&jMqu22-H}qyf@*?%EY}a4nEX^&O$H;4F{`cZCbrAyuCaF04}a?{{0;Y$5W)vPhB_Y&^lrC4sg=dV5;)V^;gT%j%d9IzSrGG?TMJtBVeh256tP*I)S5rl4?Cb`|QWC2UKP-379SBt-0zigGoRws`Y^Q*^Rq-T+P(+ z+Ry>gfZREET=CcusqU`r`dkYrslFBnXnO$X0hOt?7HGS2Tnkj9vo}H<1$s*F-v{Kv z9-t(9e?ZgM;nm%J4Sh+rj}HLK_X*Ge+#xb@#18lk?8z=MZRG=WK(wu{gPhUs)yb$2Smyl15&>Z&mW&! z=!>#?c?M8^PJj;J36YT_cEEq&k(?6KRz5%nl>TRkCq)NNu>*$A=pDcWSPQU*39uGm j4HIB3z#1mNT7Wfwu5Za$*>I2-00000NkvXXu0mjfv`^WT literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_9.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_9.png new file mode 100644 index 0000000000000000000000000000000000000000..dd5b4d734531ac48f110f66cafbb0944b823b027 GIT binary patch literal 499 zcmVLgDyFJn7#o%bFsGqDz64HIB3z#1mNT7Wf7fVBW?m;h@5)*R(tz&U3+mGs`boOn7w z8yG78wOIfFUZ0-;0GHP{e;x+}xXr0i!1DtdMu4oSvWkI{tWj&$=FafRgO@0a8Nc zrjsmrN107KN2C2$*RW#+4(lqY?Ccs*N zHPzMv@iSa|9>BGLlEFp+X*VBgO+W`o1GIDQ+7;zmK#AEKq0(>^P{Oyug>{UM-unZ( zz78+$?rZ2vvVMF3kiSoW4&V-v#t}QbKnLXhXNWsRM^3f_#!mANU;?ZK pSi=NZ3$TU>uohqq6JRaCnr}cb+&U)V>a`asI=9k$&Waba^yS4}`5ewAup6}2?Z0#F zS-Sc8xsUeUi(_O+55L~~IOtBi;;wr6^vX}7?>=7>yDo0YV42ANLG*6e#N6~5|?J?wX>h;zP(^V$=TV4U(8h46_%ItZ;0OUmQ`;0 z)f?JZ&Q#zA*K{3PIKDG1+c8&o!Olmua~|dk{t}Kj{`~BB zHfQbo6PPCmzhnGnx8(YPzzf+6euTOeGkmn4$c!E!!0`FZtg~mY3`g*0M_@!Sc)I$z JtaD0e0syzZwLSm< literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Settings_Bar01.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Settings_Bar01.png new file mode 100644 index 0000000000000000000000000000000000000000..9c8e5e66428c71e755ae7a76626f4733fae0a862 GIT binary patch literal 199 zcmeAS@N?(olHy`uVBq!ia0vp^4M42G!3HF6DHW&!sXk8^$B>FSZ*MvB9ySnRJ-9=w zV^u_#`hqU?h(((U68{`NC$NEkb@|5Y_kJ&UZCJ~g#;M@PP{`CF>@bJn5sQF&g9YOe zxX_*AUrfiJ->vK0^Ukz#4{JyC<^PQP)=!OV_`F@0q4=11L!=lU-9Q8XO_UQbm;bca Q1n4pbPgg&ebxsLQ0NAWWu>b%7 literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Settings_Bar02.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Settings_Bar02.png new file mode 100644 index 0000000000000000000000000000000000000000..01ed45ca0b3eb60d817bfa4aae6032ba45b54f20 GIT binary patch literal 186 zcmeAS@N?(olHy`uVBq!ia0vp^4M42G!3HF6DHW&!sYXv1$B>FSZ*Lh29#9ZrJvgD$ zsU>ORi62MbN;N!sX1d|sUjEqkUsz|THdrto;Z*QrC}ipoc9_HPh($mhE_DA3>%7gk z&QJWk?&M=#hdWaL`8Pb~F3@3UekR@!D2785)SSAXzWnCFSZ*OfBJfOhCV0a?c z!0mim3Xgb|Zi0AyUc&wA@R;*om}Z7D>|i<|%CL^1fVDxJA%^h*7ehE)==~R_-_c)+ zpV(&mpL@^r;F$h_U(3WL%F>RPGZyTYcig6jLm$+ry7whqwlkjY(gV7O!PC{xWt~$( F69857K3)I- literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Settings_Cross01.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Settings_Cross01.png new file mode 100644 index 0000000000000000000000000000000000000000..28cdbdc79e00f841eee2f84150ef760c2f20783b GIT binary patch literal 219 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJMV>B>ArY;~fByfsKkUG?jB#u2 z6~@JU5~U2x%*@;?r6eRJKZ?ox|NlQ-<>;P;!b}G9|KEP^JjE)>Iq5??!>;q^W$e4c zZGB`nDjNQ9W?;Ken-}&~uC;!ZufS?6r-TE}30D|hmN$MDS1in1!QYu3G3RlRyN<*k zMTXPIH`LfpX57zzIegJ8WfnFY775Q>QU2YxCOi>XU?%ZFK%(@(3T8K11_nky>vyJH S2^B!sGI+ZBxvXE0q2A(jEngcti%^Q zum2LmAgsRCM&g4&Ld#Xw7pc}(2c8y}BqU{?JpckvcCfCuis0a_;V>{TNYK4~k#X1g zv#ahc{@}lrbJ2%H#YXM}P8kQB8Rl`X2;DY0>s~`U!^NEmX%;PJSHzDj?|Jyg)#H{& zeCPc~j+40`WHtnxWaVBslSe>)Q9H}7^X)#K9wo61p9OVP{9Z0THThMs1kfE4r3Y3p opLi#J;FIlApj#UXk9_ZAbW%)u!Nxbm8|ZHaPgg&ebxsLQ0M#>m4*&oF literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Settings_Cross03.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Settings_Cross03.png new file mode 100644 index 0000000000000000000000000000000000000000..3499599bf06a42560ade653079012a8b98551cdc GIT binary patch literal 323 zcmV-J0lfZ+P){bT~C2X?IWRl&oh)@cc zdE_V}oD!!>+Wm{DmpUNxzHKJ$OPMH_0k6jQYd0G>RwLyx;MLf}QjL_y0HAA(jH=^h zHXzn{8UeC1z}6&0&2Hz-$iCelh$zg^vhPkWA}Y8G1_nHpYbHD(a|>W3I|H@Vw;EP$ z0nLyX5Buh4057q98;kYLfxqp7;R8g3KOflpws93b1?^|W&(1(_7U;QP^}p7HWycwT zQ*=G|0S!;pdR=VSl*a&SWZi1Lc@eIgu02p51AzC3^LWNhIuF?yFz$fyZ-E( z<1%-_nw>`!LmK%QGDR4c>=ZoFtv5e^{r~#-J2lVe9XOXex2)*+{kS~8InukESSoVn zEMs<%Vr9te+$$(1U2RqtwNw3vWXW>5{KpsG9ejA?}sV-65y9}r$;JaZ=e{Q0K;J4pCEKJ8V7(9yeTPn2` zU6XtwAGbIEd3`hg$$A~f%hmQS8Sj5Pvp1M8ijnN_M zi`Ioj6%*Hd{$ujL`{1dM`|P21UU2?cCf|2ge9K)My}wOudj&GzRO;rhHoLtr{JPC{ z=V*vB-H&m-8Qa~qpFi~ctXqZGX**dnZ<$RSkK3_cd)@wczP(Rvg+g=tqeerJ?V$?q z*PeU3RlqwfQub3=^}F|n3Ul5(yxOsXNtWRf$Oqw#cR1DfYGeHC^83Zx_uNR%k9!l_ z+Tr@f@ha4{B`l|W`u0SIJy8?i$o|;{;;pnUhndWlg(sZf95h-03FD=%3(h!l|Ct9= nb-Do(KcPa64WW(@9sd~GL_gO~X0B8MrXdDTS3j3^P60~J45U9+j zcqjv*Cu-rn9Q*lZxc;CN(* z6W3DLiwxVCnRttLZ)8$6|KDixY>sru+tvCE+MEq0j1$xtF0nYwV(<`V$YfIRWmtkD z_%Cs4!f*x1Z@Cvl=$dSWUz&_?9WU XlRvzBZr637M;JU^{an^LB{Ts5_x(%x literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_01_Bar03.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_01_Bar03.png new file mode 100644 index 0000000000000000000000000000000000000000..a836c953f3bc1e2be0a4c86a4a4c840f9f90eb9b GIT binary patch literal 254 zcmeAS@N?(olHy`uVBq!ia0vp^2Y}dsgAGW!`<2Q8siU4Qjv*Cu-rnBGd&oe*;iAMv zWA|<8v(Fr4ImhIosUh>_{_5D6&!u{A44>aw@M>cfLxd;863zw_#tCW+mslKTF?a|w zWHKrEA`9Ls{>rdl>iF^MynVBNXUELl$OuwYU(b5$-fH#-+rGV4HUsI&{UPE%lNY-Y eSk1*HXwBHs$8FaBF8c}4Lkym-elF{r5}E))FH%MT literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_02.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_02.png new file mode 100644 index 0000000000000000000000000000000000000000..ed52db0f7d0d71b0e22b0fc5a2bfac431845fc1d GIT binary patch literal 758 zcmeAS@N?(olHy`uVBq!ia0vp^20(m(gAGVJO?WVmfq`kSr;B4q#hkZu4ZWBhMcT?; z`4uu0E7}!SXoO8`TKYin$gOkCHzKn?usC@gyyPXK+;T^8g4+j%rtIf?4%n5cS)bf* z|0iDb(3u>&dymh);Ym5QYE_rWsZBnw0zyM)E?$4!>r_K*OI(8KvWdKZ-=EIB#9Z|C z>!nURrY}r+T$Ky|*Hr)gUsL(}?hl6B|2~;5xm*x%e;&l_ zo2QaHJ)2|RE}#7DzOy`I6{C6UnfO1PrPDyLZ0EL{_sEBixp z-_A+-?|AuQ@!R65PO(eBY!iEXJMzCx&Hl^W`JQDn@2btsxpiavhKm+G^&cf}ywq2Dw+s6C^OQNaMoY&U)%{!YPGmAWvDc`ft}-< zlJSByr1ioD?v&j=T7T6RG?@44O-@p{of>3#{dVM@-zOl!RU|s=WsX*mUXoUSD=3=l Z@9gH0mKXhd7MPwHJYD@<);T3K0RTQ#J>CES literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_02_Bar01.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_02_Bar01.png new file mode 100644 index 0000000000000000000000000000000000000000..31db6a45897facf705b679668e85bc1ceda799a1 GIT binary patch literal 329 zcmeAS@N?(olHy`uVBq!ia0vp^20(m(gAGVJO?WVmfq_xL)5S5QV$R!J8+n}+85|r9 z`4;Zo{CDA;vvJ!G_<3`is-N85CBU@jg3+@%yQjYW%4!$d5Jmzl`*C#H^v4x-PPfi2 zzy9azH<|qJmir2?ebNsg!4Og`Bt`AEngcV6_r-rbKCQlyX$uHGWtQXY;j7-V=TbB< Ph!{Lw{an^LB{Ts5jNFL! literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_02_Bar02.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_02_Bar02.png new file mode 100644 index 0000000000000000000000000000000000000000..8a9202732395f35579c8034010d8e9d71a81783d GIT binary patch literal 321 zcmeAS@N?(olHy`uVBq!ia0vp^20(m(gAGVJO?WVmfq{|3)5S5QV$R!JhI~$nA`Fg( z^Df+a99exd{pEX|D?KXKqKYgxE^JDBwq^6?eFv6vT_6SO+28!SD{lUTy!M~J6aUQk w+PipLeIwJBfroFIubtTw|MfWBId7Tgb*<&DR;e#B0|pI)r>mdKI;Vst0MmJV?*IS* literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_02_Bar03.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_02_Bar03.png new file mode 100644 index 0000000000000000000000000000000000000000..6274ef536b4627f0ea6e466c3fb97dd636073419 GIT binary patch literal 328 zcmeAS@N?(olHy`uVBq!ia0vp^20(m(gAGVJO?WVmfq{|V)5S5QV$R!J8+n}+MH~Y6 zZ!9;xUdVL4&{Vx*+Wm7*XD2?H*Tuqg=fbA5XA)E0Ya9JV7mxzy)@^)QR{duF1U-G> z@3j`!KY#1pJgvTwY0JRFr_9@CR_=e*ult{gH3I}sv)eFUILT8z?VR>@U0ka-*I6O2BED6q`L5hL zuXpW~OVt;nvL*kfMa~q})7CCsoqT#8N3_&LMulF6hApKj z{;ldp!rRMk|NES;zdrc;QKLQY?fH(E>HK`WeE*YkebLs>PBKhzV{nK{*S`?c?LNEv ztPW7o25}AxA)xU(Ee};T2(X=L0Gj0|e&V*>E8S^Z&+Oa#dV}mQ6W^V(R;R0tCceFJ zfC1!F^+TId`{#C6-(C17M|PIdWA#rdNlRpum>r~mCi&{0$k5EJaAaHaMk!~BOqT*g zIFB>Md&&uJjvuR?eNM_(>$ET?Lc-`~-`VT)SoLM}t?Qz!x}bsl<^6@1-DZ2X3C$?_ dGXdhm_y_6vSHI|HMi!PC{xWt~$(69AeE?-&38 literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_03_Bar01.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_03_Bar01.png new file mode 100644 index 0000000000000000000000000000000000000000..4d947c8b2a9d06e9f212bf0415af2e9bf8646866 GIT binary patch literal 255 zcmeAS@N?(olHy`uVBq!ia0vp^2Y}dsgAGW!`<2Q8sbiiljv*Cu-rjNKWpWf?I2hZ+ z(Ioaofy*{Safd~W!Ss@;O+QQ0>~{aQ=wl6?{*p0a5`%{@Lnf1gFT)bf1{1~!Y7CcH z9A@DYd~0QUAS!$9@w=!0Ec%}A-=Dw?()3O8A8Xa!Z+`^--2Gf_(Fiiv`Y0z$a1KH9 cu-I(>`I|ua5;MydppO_lUHx3vIVCg!09Q0n9{>OV literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_03_Bar02.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_03_Bar02.png new file mode 100644 index 0000000000000000000000000000000000000000..a05161d406afe32670e3f173c01f515e807610bf GIT binary patch literal 242 zcmeAS@N?(olHy`uVBq!ia0vp^2Y}dsgAGW!`<2Q8shyrKjv*Cu-rhOL%WTNO;3%{7 zkhY1gRcgVdb0G)PS>=DNK-HkjZOyj5P^ zFmvWlS^dx5^QyPGZ(syzd@lN*$<}YdlKH|MVVn(H%I7$2IKoDRVd&O=V|Q2P4_7m3 RKLhjxgQu&X%Q~loCIAm>N1p%y literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_03_Bar03.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_03_Bar03.png new file mode 100644 index 0000000000000000000000000000000000000000..d8fb4987602adec75bcb7646aebe7a13b0df81fd GIT binary patch literal 250 zcmeAS@N?(olHy`uVBq!ia0vp^2Y}dsgAGW!`<2Q8se_&_jv*Cu-rjNKJ!~N0>bU!r zt;L~zIbDl6VkD$ro>llUefd|_o6qO4hOU3fXyD1PgtNhfae^AdB^HNS3?9M^nM?}4 z_yoVrliP4D>i6%@sdX22uD9KLkQJof_7Ue6yUWZHh5q~nj2%EzZm$>AEM_FaJan7? ZGA1A4Hgi1OGZ*L?22WQ%mvv4FO#nOVPIv$S literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04.png new file mode 100644 index 0000000000000000000000000000000000000000..d96dc503a25efe98219fe511a15a31ae29df299f GIT binary patch literal 1129 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvE4M+yv$zf+;U@`D?aSW-L^Y*S|mrSZeL!$K& zW{yI|3U*EvrxT}^PFVVY(q z@#0qT^>+#f=M>J_#xy~V;ZoV-)(u;e&$rLFyYrjn{~o*DA3r{}`10W5%HDu20izhAV$gmHqIHB;=~ zjt~FumPT49T{P^wP`n0g`qkYI%ZsAjjPk^_88Vp^axeOREPj9Z=aYZa-Rm92pPye} zdvE8&92>c3XL>J$F4JZCx>4YHD> zWmqC> z!X*1?si^n9Jq+8@U?OL0@7$TBzgA-Zo&)Ok#aa*_s{zBPN+#ko*!bHi;x9t_nX@Aq zJ%m9a1~iGY!9@I!2gF%19Lf-9Z02^E1rB*&%(H;f3X*C@P;wZKg17EE`_+AT@$tj@ zboF{zh*@W_RDJz)G%)VSMmhFto4~R6hG`-s%zBDw^t7eG6)E;II1i> zq7-y-QbXu literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Bar02.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Bar02.png new file mode 100644 index 0000000000000000000000000000000000000000..ca4f5093a39b14ab3ac2bec1d600381d4eba73dc GIT binary patch literal 116 zcmeAS@N?(olHy`uVBq!ia0vp^?Lf@Q!3HEHV(k|JDQiy`$B>BDw^t1X85DR}91kcu zU6kN?lC4oDx@5=CGq)DAnOu9#{A0RBN1AMY>%6B5LhKVQI*vH1@Gas_d&$b3-xI+O PG={;`)z4*}Q$iB}(3T>Z literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Bar03.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Bar03.png new file mode 100644 index 0000000000000000000000000000000000000000..7c52e47799dd025a591fbd16c2068fc0ab33d783 GIT binary patch literal 117 zcmeAS@N?(olHy`uVBq!ia0vp^?Lf@Q!3HEHV(k|JDH~50$B>BDw^t1X85DR}4mMP% zEMjI)Xjs4SM(Bh`&n{=Ji7mJoFMnYF5yvZ8RoZtV`IHI;I-5OYTplS{)f=*U?`T`N Q8E6iJr>mdKI;Vst07rc#MF0Q* literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Bar04.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Bar04.png new file mode 100644 index 0000000000000000000000000000000000000000..0d185d88ea82412192d212522ccdc5cfffa5e98d GIT binary patch literal 115 zcmeAS@N?(olHy`uVBq!ia0vp^oIu>p!3HFcXczAQQdXWWjv*1PZ%-KtGAQsc7#?7a zD(Eu%c1lBp!3HFcXczAQQf8hmjv*1PZ%=LHWl-Q@*?8bz z@&%)04dsRM2g73@UfsScD80=61FNF|3jzhaV7wW}5-VA{`zp``22WQ%mvv4FO#ntn BATIy_ literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Bar06.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Bar06.png new file mode 100644 index 0000000000000000000000000000000000000000..70ce48f117837c6bf45a03a4da0052c321e8efde GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^oIu>p!3HFcXczAQQWl;rjv*1PZ%-NWGAIbJ9Ay8W z6wu5k^FfuEaktG3V{wjoBiOBF8`8 zUQjBMr1*pVMyv8N0qM6QNm`D7xSzcGBYQnU<0rR^$5O_(ERH&QLT_|*nKGHOWD9ip z^5SO$vO!KHhRfD@yM0l z_x^9^y@c$V#a}CK`rX<6U%_Hu)eprV&XwXn*WbU{*Jp1(^US)F_FGr|4E~I;%vknG z!0mavKkl@A?7paWmW)aAle~KuBmY%M_{qZ%ol0P>WNZL!8zZLkoe9!aqCoj9! zX&F3_;Fc)x$zAyL-0aSUvQ_^pY=8WGw|xJ_ohvK7o^Ib_d+W#L$=@nZ*v~({==C4V z%An)XpMo11k4FFd6Qh{SMM@j<-Psb|13`b{z?6;GN!H5wI|gZp3Zyz^qK$dyQ=?1;7&iWaI5g) zv&-&8-`Y86oAL9vMd6nJ9kvu(=H7n#jDP#$C-dD|*<_M|>0@S82KP<2jJI>1=A^IO zrhfI+Gkcyozki#4o$`mOSed@?DrcMd4$n__mZhG#rTkO5`lndOpYG!m>NlpIhN%+G zUZ{HdGXE8EvF}%yJfF;0`o9NB()xsA{f2Fv{}0?1|Mb24&)WcK^!#TuT{8dAME`k` Q!2H7C>FVdQ&MBb@09GWUqW}N^ literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Blue_Clear.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Blue_Clear.png new file mode 100644 index 0000000000000000000000000000000000000000..21cf128087ba9af63c8b152ee8077bcea6cabf82 GIT binary patch literal 423 zcmeAS@N?(olHy`uVBq!ia0vp^AAs0^gAGXP@BOaEz`&U2>EaktG3V`_y}pMNL|89K zT-=?;yllaOMT$a?4H$nfS(L`*6?TGU*^y~~IRAV;x$oJWQ(bSKn1;VUwdrz6Y&nzB z-5gs6gZT{328?Y9Ogsl!B$_x4ScDTe)D8%|DLZ-e?Cr1DmpkmbzHMf8U2gU4bgj?H z%wV;XwZAO*cIUe7ez)3LR_E`{m~STy)*4+RUiE;#^u?zueRXwp``4Kr%XwLP?$7^8 zykNuEhA;FEQC@M34Q$)1@E7wwU*~&gcDV7z0Rf)|#fZ&M>KA&S-}+7dhQ_sL=RZC^ z8>jx}?ES6s_RsIw_QcB={eSuK=IaXfG>9X*wZ1&)_FCWhwZA;T+PlxH{`;oCIai=A zTpjx2;Ph#)UYyHHKRNN){m*lB(jmG+fx6fay^i_yM`kv}RiW!IIM|xO{0(%+4D10S Z-D+AH_4E#RJum_oJYD@<);T3K0RRNYyBh!i literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Red.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Red.png new file mode 100644 index 0000000000000000000000000000000000000000..0c95bafe2ed607edf35111643d49b86d52a05649 GIT binary patch literal 871 zcmeAS@N?(olHy`uVBq!ia0vp^AAs0^gAGXP@BOaEz`(5M>EaktG3V{wjoBiOBF8`8 zP7v^Z$ijT}sVQ!q+ZvHM)#}KxD34SYZ*9RnG6(k`JYdS0?OC83 zW~BdS-<}u6~C#dy!*26_wGAqZ(Dzpu0b|8aX>@MG86CiWTg+yDIk z#G_O1A${lSj|qQ55)K%M86?cOVd-A0E>lObl-d?*R`|Njn^SJH(f4sNLZ~fq!=%XF&o0nWss8C#-TK+;x3|Uo48C=-dk#!*aQVWm z+gIiGKM6K^ROX&sp8dUkNA}wK3GuJiE!`V4@lQJU&9`T!+Aon+t#5_KF1OW*gGimu=p2QRjB& zgw1oy<9CTEEq!}&!hWZJWo^@74)M-fxYc}z=cc(FlAAvS+)H`ComHv6mgnbrvn{Z2 zj!4(tBJ$}A^Vim+Goutb|A>CN&Z`RxX0Z}6tCl^9vdez5mFqn@-~H!pKqKSfhyx9Y a^^9_R7CUWIl(`DbKn$L)elF{r5}E+~Vx=zt literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Red_Clear.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Red_Clear.png new file mode 100644 index 0000000000000000000000000000000000000000..93c462e97717326e4cf96dadc541c4090e9d3280 GIT binary patch literal 420 zcmeAS@N?(olHy`uVBq!ia0vp^AAs0^gAGXP@BOaEz`&U8>EaktG3V`_-M-9@BCG*o ziWiw!LLTy+SjaB)qRGkQMWfv|4i^Sp0o{eiC%I?XH-FcA-tsM??&Q_oo6_#`9xh-@ zv9kZlaG->V=OBwj6Q==-Z~}+g0Rf)|#Tg9F8@es!>vq1X+spdthiOUh`R&`bRUR|4 zHv_9O+vPNUYkK+3Z#CV&Ov>hMHMCci2AeSZt9D6dd2#KZ-v0A{XI|gF*!@@g@1@6P ztg))+`261=qI+9J>$MZ`ZS4QPyngW7O|az==FjUFW`B?3*UJgXDvpo& z@M8b>_48j99y)ujzWRCL9^TE3U14giZwBWeG|HPnq Vbwx~3Eim*MJYD@<);T3K0RW24uR{O; literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Yellow.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Yellow.png new file mode 100644 index 0000000000000000000000000000000000000000..2c61cf54b1adc4a0870ed0210ff96d7acaf20ad1 GIT binary patch literal 883 zcmeAS@N?(olHy`uVBq!ia0vp^AAs0^gAGXP@BOaEz`$(f>EaktG3V{wjaed&GRHsO zO^|c2=w_aOKy6M;i$cdV#1kzUn}7T7o1Z0|R#tb`pg!$++2=(Xn*@T--RN1{x7$;6mn|7)wHOdW!2gHI`bTK-+NzN*da(`F{-Hp527!&ii&qJN3U z?~jS`=c}(~pHN#qzy8Cv`roh0QsZ;k{k3&Z#J}_YXY~4KBvA2;0}Y8gZB7VAo9W2? zWS6KfyT0*ls-@w*>WG_m^)*317wqZ%bGy8J@7w?L&gri^x&K_H@8{#sGL`MM4H6`T zB@A?4D6~g?yYu;b>i|_Z1RsVm}5C6vVj%Ux?^TyZfie3O6 zyZ*NHdHvH5ZrJ2LNoQr_Nd~%krRtVDmE1-43(lU-{QAG2Z%=IUXOpS*2A`+fO22iT zSyC2d&;Rp!Wz{tMB|LenYCY=05)K%s86?bza>}=}_%{DxNb8hqx*0n4?kDc+YQueX z_N`NU#IN$%rW;j#wSNjv*dKk}6ya)}FMn?Q-IAqqeqxsFlSbaPIsf$5)$j8D`PtAi z&&zZ2r2i>N*7ei&_Dj9a`MI&^j@c*v|8I7?ZN9br{)Mu-O1?^qTkEstshGhHyl%Hv z>As&q?Zbcew+iz)IeyKY0SwiEvQygyGM@C!saf3Q@^5Ooas5p7)7#d4T0Uj5xg|`m z&)wE*`&OBqeG+Y?RPL@^o;}_EP6Ye=C%tzfZXFjrS-*3_-E*tY$nW0m{ZAC?^@t0O zd~4@OyxsBCZpj>PP3z!&dgYTp^Si&Z(@eb``t16tz)#aR!jeO?Ocr~fQ%!a7-wTm; z8PlG+oVa<8eSIwF#Gr2(C-yu2tNJxyIucP eML-kTf5zuU6V>nV+++u4CI(MeKbLh*2~7Ya2%(bz literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Yellow_Clear.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Yellow_Clear.png new file mode 100644 index 0000000000000000000000000000000000000000..56621030717c15ff1e4f4dec22ca294bc1f2a745 GIT binary patch literal 401 zcmeAS@N?(olHy`uVBq!ia0vp^AAs0^gAGXP@BOaEz`z*c>EaktG3V`_jlM@51Xu%l zGFqlg2+CmOlv>Ei6tzI)M8@$Y!W$l)`}XSF`@Q0k>d~{FWba$^RrmF-dki!4^lce7 z%w=#kU~EfZ;yK77(Zp%MBAmdXc0k}w-u27=r~fZ^u)Ci6`S0Fdv-3O8)PK!^$o=7p z%|7q^{CD-0<;Bk>dw*&|#B=UPzj*brrF!1||Ns6z{<^`?%sTbo#F_uKZXOWuX;8dT zD7wqlv?&jwcujrlx_#^ww|Rs&HY(0waNdx5{lC-pb6c-5ORv54Cv;u?sjo}z)aR7` z`z=2IvF{l+unWJ1zE~Cd{FP&$U{?C+HJ|tM8c5&00XIDB>duQmai7oqHtM?}c1FzS zid}kr=a-)|XYqnP_;vjS2Xix+FM+NCT8+*7;*%f6y^k)t5DyG$22WQ%mvv4FO#svQ Bu@e9Q literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_05_01.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_05_01.png new file mode 100644 index 0000000000000000000000000000000000000000..70fc9ffc734229d06391155b84bea7d8658c830b GIT binary patch literal 5104 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 literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_05_02.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_05_02.png new file mode 100644 index 0000000000000000000000000000000000000000..21436a6eeb852f9d159e3c4505e05c498ddbde5b GIT binary patch literal 5066 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 literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Blue.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Blue.png new file mode 100644 index 0000000000000000000000000000000000000000..0d97e44957786fb9541873a338e40323275a4191 GIT binary patch literal 403 zcmeAS@N?(olHy`uVBq!ia0vp^2Y}dsgAGW!`<2QuFfc}Wx;TbZ%z1lfV{UVR2y1}w zQqQNRMqaZbPFc)U_`T$mhTi5Tbp>6If1VGTe-xZwIidAq_PTe%3gxx;=PO)2e2w|Q zG?s=8CI=Hng;@*&z6>2|3@*Y9i)^pe?EJms?@y7n_4eh(=i{TZKl`NqtC9jK-Eva< z!isJ4uG`l4{+ey&pM7S(4lhW5)YaGp6YH~j@!e&D+Q4P9E8wN+^@oMb+0G{epTEDqZT{L+i1A0m7F=}oe(jPBl$rP}zw(?| z2E!swhKpg5jc)cEK3m=U{pXJ#GsuhEesQtrUIzOUD38tiK;b$D6N$>G`@D-M00W!B M)78&qol`;+0AF#XwEzGB literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Blue_1.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Blue_1.png new file mode 100644 index 0000000000000000000000000000000000000000..e6b7bdf25a4f54d45015c61ad00ba981da3b3f87 GIT binary patch literal 290 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpUtm!2+;AsLNtr=8|)cHm*%ELeWa zL(}r2zt)2JbAq^zZ*gFqy;?Uhb^j)}PjkXLSl!-d7M`-af8x$w`vublKB%$m;bhGJ zz~p`Y*_QeB|C@epU3Tra_W|_=agVK=C#*WSIN`A3wW;5yU*Vk35Vzp9NRY^t$A4q= z*XPgQ{B!FMmJdviwNe6$K9w~1Gw2)acWQcjyY5!R1K|VQA?Ka4HXDkcl%Ezk`*D@* zW0T%>Tr1fllefk2uj*w18kP{zdc}FI6)%+M)p~{7Wv<&*w?e{KO~nF|8BHr1 zz0-;+*X^!9_$$IIt>5+o=L1&0aE&X^tiI^fH~KT^Px!^f>QZ`0Jpry^Ql)nKpF=7VrE!7hoP&tfQk_hvOS&|?gqu6{1-oD!MGUKREIO}+-3`)113GUy9AC^MP}IXLSZRLJrD zj{M7YORPD)U+RnK0`VU*3sua5FK_@{qe5l@ds_buh^5k z>@UkL_C0Y66Lb6G&wuj#&AEkN$Ddz6SQ!^KHJ_s&5KAe@P|0JSEz?=Wzw|%DnG#4Bu23 zT!a}GaWY(FX~~yWq`pXzQ)EHc5?_=7! z_3MU@KXqT_zp}YHqdrs$q;E^Ae`8dC_?*hR|9`*tU%z^6#@ksxwT=JlKxDs$Ett1B z+;};}jN5grS+$}Sx0rc(K^ETH@S?u4>VD}K{XJFL=a=sMm$&ZwFPUF!KHuMe<*&>x z28g0fqARYhe*DcfqIK)S=W9Oi7oD-z03uVR9k4q5_|KIA!0<9e_&4K%6LIh7pSr&j7{Uymu6{1-oD!MB literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Orange_2.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Orange_2.png new file mode 100644 index 0000000000000000000000000000000000000000..5508646ab8ba448c1aab2b1cd078e6f6bd7ea6f0 GIT binary patch literal 254 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpUtqn<8~AsLNtXKdtcH4t#F-#4Re zQeZ^m%7~^wjY#z^d42*#6H`yFU&Nv4zs+MxQ1)NV&*}OL#JM+YWRT|GFj0KQu6uE( zKkHRox%9P{{fY1d@r?BX*V4|#oH7S-Ui{`(^uFcedqO>d(M&8MnbGVM(_3jtx&FnE zzid1FBjF6^`EY~(+#sbInc>|R4W-X}cy4tB@lUK45W1?l(yXI~=@V13R!hQMwmX6+ r)fMy&_Bl0A^gqCqqjTUGl*qdtuJM14$b#oU4>5SU`njxgN@xNA8$x3@ literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Orange_3.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Orange_3.png new file mode 100644 index 0000000000000000000000000000000000000000..eaf90460246194fd04fc2c0b597668d6bc8ee17a GIT binary patch literal 230 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpUtwVp1HAsLNtXKdtcFyLV^Op@|) zOJog8l&k>dS3h~pQFN`o6yhDt*lP{G8hj|+3>*sXil zwCIAmgMLEhezEN2Z!5)bYJXv>eEenlyD!XNI4^KNDOD8s!s)@#%W?rhT;R3|Pm|y* TFuz<2bU1^jtDnm{r-UW|3u9E< literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Orange_4.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Orange_4.png new file mode 100644 index 0000000000000000000000000000000000000000..0ba07933203d1b0aa71f1231df4a9f6c0e895b14 GIT binary patch literal 188 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpUtW=|K#kc`H+HypVd40sqEmG>Q6 z?Ei6Lonc(H_N+rfH+E+4UJ_zpRQx(;xp_b;!&!z|j9b1jy1#sB{Ve{V`r<9eTz+$I z;r~(BaxA%c-JMhRZ`Ci@EhyveU6-2wYsp_0o&}N)#tql(6#nS57&oMW^_wtfuw8&i cfCO%F@8#p>O@91zG0-^-p00i_>zopr0P!g+{roaPSA8$WM3_UG{>1{Yz5MZCX6S8Un2^WWdz{_}TdUf&+ypCkY7 zxYXm6t3SmL{aJnytf+Sz(^Z|lvm(uTLDt=RqJ3fB^X2t_tt?!?hF|0T8Nc9Gv23+? z+|;e<(J>Qp_m{6f|JCA4*7N;)v%eL-5X%6μ=X(kxkV@kHG`+2)fI=2bfcazsREM0p2*4B!R?7wel*(7k!nWj9yg;U3R_DTO|b3J4xO#gp4$3t`O49n>K4w`xi zz1$IxnXX;nHc>U-ub)`VZTC-8X6}9FTBa{dir;lwOkdo3`_tmw^_`!qU%AX@*z0gH zoa1HQ^{?`EhiyLpT-SHxU)_)Tm%(mjVzV?>F7tRaZAu9!%N-wSA xbpl!TmFsGo9i#Y$W2|cm8M9*!NQrMqW3v0?d5*c5*BuxF44$rjF6*2UngHIrdWFX>V=#<3j zki@n9%;5l&HwWZqHZg@QFp`xCTJ+#Y^zzB?LN$}r{mZ-dPx8{#PQCUko(_r`TOzbM)CRgS(hJval-b%-^1B8|LZ6CJH#ot{*9U=wYku( zYZb53{DwUa5v>zeTDe|jn+TL#;N2>)Cb+-ev5Fi3N6*a#tJoA1yx V|8Z9+VlU9244$rjF6*2UngC*}ZLR&5VI3V_NZ4tjJi}b#IU+!y8H>>*;{`gAPTK2TQ7fg#h7bWKP^wrRpsU-ZSiVC~v9BIis4W7XH8u@}l6m@-rv+?X<6 zuUGL4O>!fc+zhJ$j^yFjVrKS5A_a5+9 p`r`k>T7l6-$UzxSIO}gIp0;z-L1KIJ2iRapS|^N*JoQAbV}%^SAN12Mib@?whM>YbCmh*-LvXg-)~A9-V=^um*8qyffGDQ7ocv7!817ZTt i0!asBu<$I1fZfv_JX@BO=rIDF#o+1c=d#Wzp$P!VoJCXs literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts.png new file mode 100644 index 0000000000000000000000000000000000000000..c7907fbecf536b62647c32e56b995f0b35a54a0f GIT binary patch literal 2174 zcmchZYdD+P7ROy$?YPdgbs3amMztM6mC#7@xK-6iYSfHzi>Y3Mi70JSlsJQ`F{n#% z5GgZ6s;MFvLQ{h_9d#R28xr-1L=ho5q~VpE^i(`O=fiwFAKtY;?ET-*UjOx5>v^-z z!M(K94b_#Dl(f9hczh#YSLENFy=wC4SjA;1Ded+2_HaFaqk_+}yKNj`{A~4gkNbn_ zN6I=Ej+rwW*`c4QxSiCoA*Y8Qam%eA%_fXH=viL*)aR2BGr!xRhYsyqAN(>WFzZdX zqq6R?tc%u`OPesPRLm|1$J@&$1ti1Sq-HvIRVKR3WraJI>zbu?A96WUI&eA7lIxfO ztbGVi1HC`jyoU^YlKKNEQs!PP<>}xGVwhu771-N=B>p*(Sj7jy z&67-d6G@wRHOx_7O!7){EWi(L2pIH5A7;>7onW692Z7ix?QirW~~mH_$M!{Thv; zwqBW?Tk*yGyg0Th#68QLX)mh)>KkaSS8iN2FqTncXT|HG5D#g=8qlN#|492Hx@vAP z_?H~;%lootrv6~=;^tlVTv&%mCqAmOR;Yvgqz%fxF=FY9Sm2q-vR$D4rlV?Lhf@wP zT?5-+c551NTZV<*SN+nbXU6I%*Q>`|eCIIg5?YIMgRsEl;N&p?gPQT@U)v3N30hzyl4NIEkbbU{w@b9+u&riu8#r z_y*-I$8}E7nvdrb_)PIwMWob^F7*G0a?kr}s7goyoVUC7fT!+_bU?m5#I?7Rn0tLa zc?4lr>T*>)-Nk$Qo5|VM7ZO;q{S^0kE2SQA=37a86t!y8pd4JkevaXP{4cCm1U6d0 zzAE-cCw(C#!YBDl?$T0h5s+Q?AsgE@Pi+dpfI`dcx8sMr@X>Zg=Ux7%s6Wp+B5dD$ zXv#=#f$W;%VzIf`SbA?J%NKf4?uielfrQy{g`>wTe?Z+|_K<0!C82prZ*GeMzt!Ng z&d4`TQK5!HTsE+FZadAKmSLqqlWp5;Q{Yy8OOL#nq(E|fxtlccLx@luj?%F9k%=-P z%tznyxKww}5Ya~pXrg%}71djfsmeW_dO^pwgi1CE%+%9^cQcbCIpp1OF;Ng#X6&`(iJz}V$MtxItm-YnCiKB?D0xuq0DnI zz6|Dom{er>-mMmX0_5hXg~T*#7A&4a{-iN9&fy@p23nv!**$OGkur?vZ33p5u;c-+ z1RWby*(y@hHB7@A$FA-)vg6LCE#PHQU?&F$guf&+uLqq64x@UH7H%XkWafw=;WKKt zZw_KW!Bv7a^=53yff<}rfF0#}`j|`39A@u-G1(b9Z&0`r(_Fh2I$WGj5AMIC=tTeN z3=`81M7?7lS;Z?LE&C7r5AM--Knba!M?`!j>I}lvz0G8Z^t!BO&^kH(6 zJo!1YVf3&3`!nCi?I6lV9xfv7H=c@3JH5DElv+2`%gHwW3{7&$2p9IU@Zw6N!bGdr z$9eR1r@tq3AI{6-e+fN`Us$%0==*JRvXE^Y1hEUk>6An;+e=G~XOBQ|AZV@MfY1% zzuFoyT}Jf;5z0rk^I>=!iO8NxP*g$`grfYbU-frRq^hCwcP8sK`6N9?JDI;rqR%U! zUklseB(2)2fO%^+7{43S(I|`J|1G2Mg(PkyL@~c=A2X_2p_mLznSBa^ikR(L3Gs!e zyk6G?X{XH+^_xGKwJ>KXYc)rT|$7h!#aL--(;E(|*I`&I XrSIOEOB3b)6-wToaF2&>;XnNwEpT6T literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Blue_1.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Blue_1.png new file mode 100644 index 0000000000000000000000000000000000000000..8b78fa2131faebff57d24dae8d1c19c1fa444006 GIT binary patch literal 469 zcmV;`0V@89P)gs=~9IhCWLe;bA#vw{C1i7u*1C1 zeuxiz)v7b&KHg(y{%(jdwmgIo{2+xGV_o2s zJ%PkRmh|s$uYcFG`LA14=Z>ZuH-1rF*5)F1qE~>PKs(nqAJxt+I+}K~;%9S-UsUqH zh@I#apeJAsGSfCazi5+qZKv(p&!ty@o#-)$_MyAT7+U1+=3rz(Fv)&tA5&3FCvFxh|V zS9%Nd3eXb>`q833^{nnBXwa8KH?(H!Yrr=8Q2+TQ_3s}g{(PuERU}==daAr?iLANZ z3F$TuOWAV}+i1`$Ku^FNWR{%BKz00000 LNkvXXu0mjf3Od*X literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Blue_2.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Blue_2.png new file mode 100644 index 0000000000000000000000000000000000000000..208db9d45cddea9b724c0975237e87e3ff5d5b33 GIT binary patch literal 486 zcmV@P)&W8}32JgL|#1KO8C2B3`7Sn-V?3$8)TX?4e7;okuc!UST8o|F6@Vw;opV{Xd6|BzO%JQx7;o>7 z)%>4T9b;~%qvMkp_fE-*+)nTcz!MM$nP{5>y(zcNU~i!2e>%#aRAi@mq5l`W0`LS9 z3-Oc31i?-g+erBztGwrdR{)+sVj(^_oYQxhb;I&J@Cv{aXm<*&8Qm#_0C5+p7yMR5 z>#?*>hnWgLrti%$U%&bPb{0Z_=dTv|C3<#2Nuwd%l1ythj}5lrx3JeY>VJ7h z`Q4>{Q<0oP(p%-3BUy7#C(CIwzo9+HVjB*41>gyYgG{p1Xh_4wVkgUx%C!v#yaMn9 zyyY8NQjj_9+6*ZR;1z%;kZqx)Sg@0XQnulMR{)+sD+?u!3}56+*@gpN0eAw%Ec9Q1 cR{);C9|1VL98>IJ0ssI207*qoM6N<$g7+iZng9R* literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Blue_3.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Blue_3.png new file mode 100644 index 0000000000000000000000000000000000000000..a1b5060a1a0ef874591a91ce526d6222859f3469 GIT binary patch literal 493 zcmVg45)^*Nd1!2)>EA7IkupH&O5gwxl0;^uJHTtVNre zYrzRV0eAxmLQ?c^uP<>tD!f)jiK@CLkdE(@FY@wd9@QMnt( z+XsEM{%29eF~{k6a1zIzQ$msB1fKxB0kM&Z*c|GMa%{T$UA6wxQT?bSoT{1rEcgWA z4I~KhoyQ~rCkr-G{c@4_KJW>^8%Pl1jl((p46|-o-UmJbcmwrzp*5@Bg%Bj3g{m39 zRnhTSI!=e3=Quvke2L}!apiMcgb?KUr$v5=o?KE<(W6_E<(NIZxl$j@vEjF{`3J3k zJ*E2gQopH6b|LAj^2AZBxw{jWwA0gp$buPLi6!h6_FccmuT%N)|HwBi|G@T<{6N j8)yun{}Oxx@CN<>b?LmwKv!jH00000NkvXXu0mjf=BDMQ literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Blue_4.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Blue_4.png new file mode 100644 index 0000000000000000000000000000000000000000..bf3de110c07374e285c3d165718003d12ca03575 GIT binary patch literal 483 zcmV<90UZ8`P)05xfKN1tcRBbrx`vlnWa!cn9DMXoOId$nYYc3mYzY2jC0HhR}Zr-U0Xm Z{s0J5wSLG0t?&Q<002ovPDHLkV1ig&-z5M5 literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Blue_5.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Blue_5.png new file mode 100644 index 0000000000000000000000000000000000000000..464c225f7cc952650349a88f77addc07c7366ba9 GIT binary patch literal 462 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezr3(Fi!P!aSX|5d^^)V%gIsTSh*{| zfI;g7>ka{C#ZWKBr4Iy;SjF;9IA=9+jzWT@$4o7b%BClLO}!5onQoSx)7g`lRxACb z`Fkk;9Le$@b#LP3oeE~{`CcUu%yIDfiLEOp3e|KqceJbALP8p_=9avu^#8_Yp(ISzc%af^*y(U zjl2J5zx4JNrQ=}fBdtD%met-1>(0CT@Z)67haqRKhSab>knRw8WXwOEwK?YF?5%ck zdKC~M?)Ktea?kdsO7eqjo{_dNdF8xT0hM>HU6nsquDUFpB7Y?9YuvB>rcNB6{$x#* zUzD!Q^{Vvclq1`3h88@!@gexetJ$ym<2Co$U75dXQKZMN(0P`dww`*Ua@I`$e8}-8 zUU87QKd!L%tv~*$TAEY+v{LC;hmLFfG8smSBC0BSWuao4-4A3N@)aZ(YD6CJ4ARTl u0pczYatc&D++73ZDo8sW7Xlgehw+TqR@*%bZn^>EiNVv=&t;ucLK6TPZOf$q literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Red_1.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Red_1.png new file mode 100644 index 0000000000000000000000000000000000000000..0b6878183e081c9b4a8728fe3802b7db5d4c3340 GIT binary patch literal 484 zcmVF0(pnH>ck==n zGI0amgMC%lotfR?J!UeA4=gyA zJp=R!L=w`de|dVGuaD0^vq8~!aB!cszc^V*`dZ{f&j5V_-Z@t!o1t2_1Xtg(F11HF zZEo+(*S%elBFc%L0r~_~f;HLnm-9h~ur9U7@6M)qUwQ`U6Nn^)mB)hq+G_g6`-j-v z+$Bw#qy21r%m2_bK%YP)A#5DZ<@YceRB08$@;~$p&?nHU3Yl;^`m<|Sg&3gjg=W+A zRE7H%wP7i;X~4eulIOUc#_x6#Vt~h^MI53hhhns`E>n_an~jakA{(5-Uf(4C=vHjH zp?*;jRUz$EdE#(NDyvSGYIA)laV)Yy&@(`vfG@q^cs@&?+eC5#VOpBh)>tIkU2w3$ zB%#Qmu-Ua~QWnrNK%YRdgd%4}P8w>-20_mNeFCi{6a_Ne$k&n$f}R2T1gc5szd+9b aeFDGMd&3ZvmVCbe00005j{AepXi`46b9f((KR;gf&XwQJg48vmgvzCR$Gs!{w| z@D9Ki;2^|1k3{9hn)iCK6vxJiXP8YVY3~E?0DJ)sLVV#+D!zxAwIuHY?*M!OwYt!( z)pQ{Q1@}VL$bYKp@}T#r9hTR(c6@i*^X440`0*EA=i6`o+y)^8Y5r-EAEJjx6tuNT zrzFWXo9~e%*zi->>l@|o-B3((sGnCkT`2Tbd8o+N)b1qdHdhyvM}iF(yaVtB=+qw^ z&u9McHV#eWOYfh!00000NkvXXu0mjf2qNiW literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Red_3.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Red_3.png new file mode 100644 index 0000000000000000000000000000000000000000..ff3add3eec504be8cad5dfc09848bafa120b071a GIT binary patch literal 506 zcmVn0K5SYA>Mg3N)Pr*e^0i;*m(5}vwRtMKkx~_8}Ja~8;3FBXP8w>b3gD2z#FL4 zg;uSm3n55+7Ru)3r>Z_L`pAo6diOxT-;ASfB40d}UbjIAL6*N-FZo7pX`qrrv?J^^?G;zlOn z)a}sbBH_f(X_`$c78@@31mF!=%{MY{LgZ#wWs}kfJ^^?G(Fl2y1)L<+g$);c0`LYZ wA>=Kj_#$5yHeB!tz#FIyq5l$m0`La@0Q-u#i&8qAsQ>@~07*qoM6N<$g1flnSpWb4 literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Red_4.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Red_4.png new file mode 100644 index 0000000000000000000000000000000000000000..7b0f7174f38a33dddc735eae9fb2bcab1b229d9a GIT binary patch literal 490 zcmV*I|R}QK7iud z-OR!bJL_#QAKX_h$?V<^-;ddU);VWFvet%!n4EJqMPH3tF~O@S_ypFZKe+WjPp46X zrs}J~3Elzt1U!T!>)&3U%l2UO861@N?Vmjadd$~MPo|L~}6w~h%#juX5C@Ck@ONZ9n&!-KZB2%Bs(3&wvKiXV&!r(zoZ z7Q6%S33v$c%A-+lC$IcH+X!Rh#WTzn^SJrII{=@6hY(*lj0x{ymMzVE;2nTZpjH)H zwwfx0AaO5LOsh{-`R+;bNi1fMmEUa;LXhQGi~JBhIj5lR9-Wf-HaqYB9Vd-qL$-?5Nr978ZSHPp91S*H@D9KyU}GORUaza~Z6usDi`2mt zA=*4}ph<<0*CBGTtFuUH1n&TR0?`P0odujErNV{_-U0XoY9ZteqPyhe`07*qoM6N<$g0q0wsQ>@~ literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Red_5.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Red_5.png new file mode 100644 index 0000000000000000000000000000000000000000..464c225f7cc952650349a88f77addc07c7366ba9 GIT binary patch literal 462 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezr3(Fi!P!aSX|5d^^)V%gIsTSh*{| zfI;g7>ka{C#ZWKBr4Iy;SjF;9IA=9+jzWT@$4o7b%BClLO}!5onQoSx)7g`lRxACb z`Fkk;9Le$@b#LP3oeE~{`CcUu%yIDfiLEOp3e|KqceJbALP8p_=9avu^#8_Yp(ISzc%af^*y(U zjl2J5zx4JNrQ=}fBdtD%met-1>(0CT@Z)67haqRKhSab>knRw8WXwOEwK?YF?5%ck zdKC~M?)Ktea?kdsO7eqjo{_dNdF8xT0hM>HU6nsquDUFpB7Y?9YuvB>rcNB6{$x#* zUzD!Q^{Vvclq1`3h88@!@gexetJ$ym<2Co$U75dXQKZMN(0P`dww`*Ua@I`$e8}-8 zUU87QKd!L%tv~*$TAEY+v{LC;hmLFfG8smSBC0BSWuao4-4A3N@)aZ(YD6CJ4ARTl u0pczYatc&D++73ZDo8sW7Xlgehw+TqR@*%bZn^>EiNVv=&t;ucLK6TPZOf$q literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Yellow_1.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Yellow_1.png new file mode 100644 index 0000000000000000000000000000000000000000..050405e6c55f2e5a6e9940ef48f2c5ab50e59169 GIT binary patch literal 475 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezr3(FfQ_RaSX|5d>iTL#q20Bhx2E{ z{SQJy*P1z+%KMx6KL`|klbu&8D6jD3g{?wIyAGRv-C=o)olCP5X1_~h_c?eibJNRr zcNcZNn|XIe5SLg|Pnm$iQjzQ@RR)@zc5WAhXJpkc_)vi)_mlg*VJ|NK^Fv z_3rcI4RN-AX1N-EfAj2YPyHVImuJ7;=!|2mVlr$ANSXcalHRR7r+1miy}dFgUgp|l z?Tt#ez_PCtf^&^QwS=f#w&jCLIgm=8*G_V8-sCM#eB;Cu@ZIV5)||D{7NMoJ2Nj<%dv87~zNS8< z^LFN*_Zf<8E;U;_+FyJNYkFpEsk(F9O^XvU+YScbco+RjKVCJ>=8C-bCcPzlL-UyD zP2$=;dGT!C@JI+;5&@XqGg~UZyWBi=InIP7APj)5fF8 z|3Y+udO*%Xoyd)o+oHBPUeI>%PngiU$*E8O)7G5k1t7q!hD1m8!k1*4SYO$0oFE$IfA{`dYQscB!e z7CXT!08b#YkR*Nm^w`A3;#+OXYiFl-YX0TLc5AK0PVfrA6Y$QtB-_+Sdei)Y;_>cG z&Hq_dP0a0-9~?IE=$Nd??F6p?JOOc#iMH9(n~p!oHrY<5=6^ZLFBW8{YN;oM3-ljG@Z~X$+XSpdnAKx z_$lo5jrz}TC}%j-Zz`fyNcyT=I+8Vcbuz3r*H_fXVjB*41>gyYgG{p1bV9?$VkgUx zO0^9KyaMn9yyYKRRFF9Ax(q1`;1z%;kZhr-Sg@0XzHGw*uK+xOP8Nz98UD!kWg8B7 l1>gzvW}*KAyaMn9{s6}nxDNEqac2Mk002ovPDHLkV1jB1gsZi`h}&F7Gqu zmdyfxm}VCgX}SpLu)bxnKj0MfTXyBw58N#xVoggGI$Eqb{xyUu^gdwcSa?kL|FZj8f%B~QRRlfG&)*hZ?EHphJ%bHnc2DOf-`X|ex^bB{ zccfQ;v_HK{ve;U*-TiDufm7HMh#5CdxZHm8+O>Ut%IqBV;K%!Uj`}Z4S)R4*JS)hE z^G-F-IS)Dhew?%J_Ob4CvuNGN*O(y^dkQ5M&riJ8JK^gAZ3h1ZxAnH9ec3I*bYydY z>Egwu2lGO=PALf8B;Oy{9iclf`uSVk7KYRDx}UsDDm@*4cI0^~9)I)s=SnNX?Y#S2 zb?sLC`^tRu`}AMFt=qXh^QU?{Z*qI@$8#yKxZb_gDd#XF$j*%uJ7`s)90-nm52(EAH8HW@r!{an^LB{Ts52zc7A literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Yellow_4.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Yellow_4.png new file mode 100644 index 0000000000000000000000000000000000000000..8b0f7079b24bbc17eeec36fb364e891c53022c38 GIT binary patch literal 497 zcmVv)o0zQF_L@t5^0+xcM;3G(CJ0HL|g7gGm@;eauAbq&Zg+AQ7a~R8wFp$PUsJA{qOU6 zRMJ#^H8{aL0AGNIP{{hl>r2@lj=sdCyl?N~NsPa{-mUDb!3o|0_yVjkCJLLyKu(&^ zsNLT0iSa+HqHJ@V@{@kqZk-Z}94B}O;0p*YWI=2W$Aj_{7`5}6INkQF1IwkRKcBjj5iWJ9& zpTd^!l>h3H;+sSLw#u7@Lbu9eL$>l+g5UXQa0<;f5BHQugAEtF1Mmgd*awcc+viC8bZ5}w#q(aE+5V_dZS)??AcL2VCXoS4Z0h~fgg$)ka{C#ZWKBr4Iy;SjF;9IA=9+jzWT@$4o7b%BClLO}!5onQoSx)7g`lRxACb z`Fkk;9Le$@b#LP3oeE~{`CcUu%yIDfiLEOp3e|KqceJbALP8p_=9avu^#8_Yp(ISzc%af^*y(U zjl2J5zx4JNrQ=}fBdtD%met-1>(0CT@Z)67haqRKhSab>knRw8WXwOEwK?YF?5%ck zdKC~M?)Ktea?kdsO7eqjo{_dNdF8xT0hM>HU6nsquDUFpB7Y?9YuvB>rcNB6{$x#* zUzD!Q^{Vvclq1`3h88@!@gexetJ$ym<2Co$U75dXQKZMN(0P`dww`*Ua@I`$e8}-8 zUU87QKd!L%tv~*$TAEY+v{LC;hmLFfG8smSBC0BSWuao4-4A3N@)aZ(YD6CJ4ARTl u0pczYatc&D++73ZDo8sW7Xlgehw+TqR@*%bZn^>EiNVv=&t;ucLK6TPZOf$q literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory.png new file mode 100644 index 0000000000000000000000000000000000000000..20b95f9a1393194feed933763350a2956c53cc91 GIT binary patch literal 3321 zcmcguSyWT!7CsOncYApnsZ2id+HU{qLO}>^#2A;g4cdkWWDaZ#T0X@$Wf)AEURPwI8t zwF^U@2hZv-ulJk90JhdWP9FoY#vcN)MgP>S=QpI7qhbBLBUae{uw`|&lBBksY`93K z$N}2=?*ZCnMyQ8%^HJ+-7bI)ptxdxOGC0T)#58-flsK_sg4r$$0S1#bqEtQbTq4;@ z4X|}nfsE;?c)MDkSrx8DiS`T^-Z;HJSBE66E)9a8d7B$Tt`e_tvO4TtQ5>6@=|gA{ zon>|8vR3%j>vQYOHs3BQr~4^N63jR+Q=TzfV z<5ffTMXeL!4i!m%DeS|R{tR45YWwOoE!~IOi0UipCM%KQ+!b!2lp<3@ z*q@xPV$&ls88Phi<}7syEupESGbH4x-C=-|2IXWT+nz?zPbV^Be#`O3)`bS>oRkIr zlP&`pLWyhrhh_+v>QF2V96P>g@(H@M7U`@gG;eUvi@AhGOE@A+`1nF-2v>GG=WyeIh?8*mXJXl+ zV6^WNI&D(>-vH|v060I8_SRm~)UBpvbdr*r7@j(U+Ci>&J(po}o4WN_@DXhPj7`%B zpDg0fWpB&{lwFNkuW4<0JvDOAW-iBfL);IpfeXQ89}oc!cejPVC<)@`UJU4Hm!ydr zAV}t#Za2fTJ~kzcUV}_BygF=w*#t{%yTlAG3}y4w+chPF%6dLWrN(OqFStMYrZYf* z+(Jzh?D>Qqn_~U&UNvMt=rj!8>vAAp;?NrnHHX{~~>qhUu@z~sE*AnqOC zHuZ5Eh&Os5&K+us9%O@5S&vMgZ~||hAmL{nS)c1^U0Z9e&gv2lls4j6>Rrv3s7S6@^s8QfFQg|{3WZ{}L@z$2q*`Rp zjSz$!;f?s_bW-wRjs0M1dn;;DYc6N;G#t~swikAIMO-*dv#zSR$|6pBnQg^g;tKmz z%0`GsKZxHUkJo1&Cx+yWoRj$(z#P;kn^LoBJlR79reYrm!7?2?N} zoj5s2VPjc6>?WIR;!k$Z^4W#5A5E=9HP{xy;i|1^5A|i9WRchj(L?8FxS2mOGSGX@ zWT{GA+4Q(KDA#G|L~aqT%-`Mi1S(g$V60SMTQk@6KpGy|>tL~q9R~~WWz<9J&d1)P zrjLl3wV=`XW!w2ywmAFw+3sUo65x@5a1k)RFFlIG`^yDZdh$Xid8?6?i9|Xh8@|OK zCzIp?p1-Lx^u^wNuU-1|p0=8C5ciXC*ymAV$&&%m$LR9rDVW1*XXQ!<&fVQ_&(yDT zCG2GNM}01pC<*f3&`LyXOV@Y#yBNjuv#!CBI(=S{X1N**9cm=%X#Z1fdwu`{ogf4S z582gFb?66>U+*}sOYIP7oZwzNVmO70*9rvGM(65q<6s?uAH;1>sdwh@1W zxuMt^6Z44NY6s&f9m$VNrN zXeU<^eIFynUJ8=oPfGopkxST@6hu zXEti2svb9sSQCYqzA-Woo$8m@wN>Z@n2xtMIj6HY39$|T6`Du z>@5nu5jCJ2gwtA9Hm{D4PYdH>L?mW35j` zZHR6Cf4ft>nPf3j3ypLQk8hFWA)Re_l*C5e4{@&y`gkD0@^;;Y(B_FVV`QvFpodsj z(u~o4=h^O~FG!_98g$AhKUZR&cac`_o6NG7U9D=l2>7-a3b$4QC(eJGK)(=5M5!;5 z&(~4&yh4*;(kr>!qVMfT-}?~Fm%V3?3mH^+QuHY%Y{T)LcMdtomwNOYd2!u_*6fO0 zGZ}G4W&9m`;YCW0Bdt@cNo%BCeq8pKt8p?TT{n6ijKZzbXj<{vvx$R@?Qf3A4ov*Y zrk*Uxai5CwOT&|U6W%$6RKkt^iT%s>;-L*EU_jHfouT$lQu`Hdg*v{pTJ^MUuaVbmnNFV^!zh#ecEhD+K|3l# zx2vzk@;9QT;~zg*@Z1ZhtkEGu$4BhM>sJ8UyTutb!wSe8isC`8GG4Wj?|xK_GvzoL z;N9@%UL1VnzOsfmcQkVVqx$8ah-9l~j^}s@Jmq@*vnQRhs~Z^FUgso=V7~$u65MKHmnLWM}EF VaVPVOXo!Un4;@cBRN4F8`Y)vf;?Mv9 literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_9Slices.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_9Slices.png new file mode 100644 index 0000000000000000000000000000000000000000..910730e96a69e762d1ff4cb29e27428316e25245 GIT binary patch literal 613 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>VAAn)aSW-L^Y*Tz*I@^dV;`4% zVQQ`FQ(B?1?#}M-9QO_~SG3C=tUcKKz)M7QiTX?4o^Gd4NuNrb-0b6i|6jXo(yum7sx;mnW}{QKe8vatUCbLU=ve3JCF-hYd; zoxJ|*kH0?$+uH5Cc_lfmL4vVk>$C;^Uys#fE$f-<>tG-Kqj(u-;?Lzgf)aSp}{jWDjhT9Clu(8B;U7)ZF8CcRiB$lV6zA|!6~o$Gn~0;F>9 zf-^U`xqvEGG#M@CLSev^yXhE9Z3L->Gqf~}A@;x-Xg0u2g_-g8#hDv@d_Ze5juaDE>W(7l-F^8Dl0BD29kxmH!TVdBv34E$nSmO{2QSuO<>*evPX^x%yR!* Q2~0i=p00i_>zopr09y?X;{X5v literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Example_01.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Example_01.png new file mode 100644 index 0000000000000000000000000000000000000000..d26b3caedbe3ec049070fff8d6550c6af1b7f122 GIT binary patch literal 2815 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 KIoBbAuKo|+*&FNt literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Example_02.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Example_02.png new file mode 100644 index 0000000000000000000000000000000000000000..7357b31e19b5d1459b7664c9dbabd73964ecd265 GIT binary patch literal 2351 zcmYM0eLR!<8^_hJ(@&=d>imu(QE5sVZAvY%@-%E2;*@QsV$D1jiaboGU*&{D8uvEF zRBTbEV`pdS#hmImH4jONSZIYMX2;XW*v`F7{c+!~`*pv*_xHL!*XMm*-}{=6H){3w zCg1Dn>8=_@^55Bn#wi6T{ z7+pR4;gR*v_Lq89`XU$e5=DtcqU{U!lGnRkBqu^uW)FW3H`?p(hW+Wy+KjAw&**(Z zuX6q^3pOri{U&5hpZt7!(3L0B`&BjOB@NX2szzUI(z#{tOt$UaoM-zIdGIaLF-Y@) zjb!oIwTXN}jvm~pt{!|ft4J&2#f&-?^8&&T?YLoL*mzqii!pbyOglh7PZ1w;_+iP5 zt^4{@SXIycbqlEUz!cT&(}^izE1URmpluZeXT(Wkdnv?L6uVCECUTs-bW&3#2@KI# zOkLn9WwVjIBLAquq3aV|wr!eizKLN*>GI3fNK+f^X%3$?bn9(=?#4qgHFGpcI?wMv z&bjRueMB=VygF_0eXYTyO4irXV}7wG&1o+7;qw8uscr+mJ#)>_%gM?CenpIOut!p% zLdB`ZUynp(i1QejD1;>>Gx_Mwy_^5%xmv%BqU5{c>}A=8L%*f(d0nm8m9X>-8UeLD zdI}D)q|csE{_tjlc3qf)jGG1Wav`J%obozQo5d+*8PTj@%yqQe1k;zT-KrIlm%!{_ z{yt+LfVZzA^?hvVxnR)!WPX6&*562hA=c3>V2FzOb3lXePmU*8Gq)$LI0z|DE^3SL zRI#kN=wGz?UGsF%>3B(+VM^`_fNDhkaGadF2WC|tjNwejAS?yCl6%o_r-!d^%AfgE zSVgADTKhDNvZ8MC$Xc3zRLWq6OZe}MtV0-1cQ!J1((pb^>tbXw@@QO3bg4^4{DXFQ zd!ryQf)`l14s!NRDqt36V@zYUQIGrvFeYctxq2&t$>m_pwXBTV63g?g^YK@Ighbf{ zQt5Z(abkY^8W}1Z8_wH8=C$Oq9l0nBuVG~SeF_-Ji{6D@sV`C3`Z};8nTKs)&X}Yy z;|!v;rG)`A`KfE!^2=Mxm)wj;B!$^P_=)I0?5exFS_Sy=KzFmo{0&iKQ87A;NDmr> zkZ|(s*7Be|4#x^jD@nf5ISrr6bEh*ZbjjId#xRd5q*4OvAe6$JuZ+0RYlbUM6`uq@ zU8r@?X=E}xMRJO_#a+S3+Re8g7Yl3HzY^H!;xbe-YX4_@8SaU^7v>A*$`zUy3E(4HwC{@V1qr-eE=LDlqGa`Y&@-Pf%3|R zZK=HkHNtd_3e67C?VKr^j)Nw50Nb8}0^PKkWwSViaIJdRu|-E(`47P#5paoiafvCdq)=DVnkO!;I(%HcnFVLzjn(w`>n7 zh}iK^8Cc)#_#CQ1xo79=dN+lkq70T4T7p*Jqbm9R z{D@=l((r3JTQ=k_n&sA~3bfu=Y9igi$i?od19C&EfultHscK;Uey?hU&tZ@$sNKtm|Sr26Lep4mg>F(l8Xr^)? zB%BS06ncP912`1sd?FBMs1bORJ7r~(%P?y@>w9)}L9yY_=7^_xFYx_QN z+4P|ee?|)>b?UK{dHF;=^2jmWG(U(-!NdET<-(;OxAdf|ljx%O!SPk)Ivd0`q88ZP iG9Yga@@qqDmU5Ww9XFhGyo~N6)E6@4FrGEkTxRj*; literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Example_03.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Example_03.png new file mode 100644 index 0000000000000000000000000000000000000000..8ca1e23351d8a80117c323f6b3676a2de6a3f830 GIT binary patch literal 2284 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 K=d#Wzp$Py<%GAmL literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_10.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_10.png new file mode 100644 index 0000000000000000000000000000000000000000..68de5def2b7d7ebe2b3b524e875b0597ad62f017 GIT binary patch literal 631 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>V6yXcaSW-L^Y)In*AWK^*F@z! zhYPVkW~|WYV^Mcne}Z*~(!tIL9xSCx-W`^eNPn5N!DQpB?oN7jA!HS6{c|eILJ{U;Q7OsddZy?Vi6Z=6|`_<;p~5prIR;!kZ!QKS)yBh-9BA(Feu2{x_UQj`Iax=K3To^y|en9Z8E1j z$RCQE7YJ?5QZGNZuyo3*uRWSz!H1g{*cELJ4iew_dHR)!n-@qLF+}|M-_*7$)Ou&J z+nG&X7EHhZkUr|96D}yYSwTE0Dv@DB;Q1!6RmzJ_DM2J-S(mn5yBLzC{uJbaoGYSJ zUIl9gubevt?6AN+g3?;Qc%%CAkEESoNSHj6b*Dfu2XjO3quv`KKiqQKco@uPwnohV z|80lBWuRTNpXhW%rC4&$Qe@myhT>-Un0$0?|hTGTcPlF?-g7sx}A9@U;N9r5l@~F38MZA7S Q0TUF1r>mdKI;Vst06mWjGXMYp literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_2.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_2.png new file mode 100644 index 0000000000000000000000000000000000000000..d94cf430c40c8640fd5d1fa1ba5678fd9ea6e47e GIT binary patch literal 635 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>U~=+waSW-L^Y)In*AWMi)^c9yE@Ornj0boa(ijYw8zdRdFeI=sAPeQX$}h;AHTB=#-MP2bivIh7R49s0 zxpwaQ;nMFvUtdn%URzSO@5ASiUvp(Ff1k8${}QG2>I5gys5P2j1MHTU`Q_Pl%V^*F zoU{A3-|LLekG9>`3v~w@vpCGrIs5sS4;5z~PJ8y`+0xr}^Zv$oSM@FpgUBDvJ5rRD zo?gEE%0s86&+ml0FSTFhdDOmX-z8KEG4mn+z6jUDK4cp?t&s57zSe z@0-6#7WRT|dANE(s#o^%^>Ib3R`#!6z-hz~@#8U{d#VaSW-L^Y)Iv*I@^Vw!+{& z4FUNN=l6;oo%+D-MQ2sRycG}J3RIm+m)<)pEm8hbB**uxdF`^7pQBrqj5+o1ANNrT zzFJzVaIRrrV{JNv0ds>S!x@GIHU?vc8H@*b7}Ai0tk2jwhK5@I-M4$)Ypv%$!axcm zqXKS~?VEq>?E8HE^Jl*v{d#u({^ghUXD&W#k^4@ucACuF6I?(e*L3~TxWDXek6OH9 z@l&(;a;Dcat)=dqoAr7{SqdxAnroS&TQYC&wA%m1=n+a literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_4.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_4.png new file mode 100644 index 0000000000000000000000000000000000000000..20a9beccdde572364ffa236455a3026810f2aa9b GIT binary patch literal 631 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>V6yXcaSW-L^Y)Iv*I@^Vw!+{& z4GXq^IPd0KsJcV2N{RnLCyRNLw%{u5uX6Ve?C@TmQ1a-^^U24aZ|8DV=hVM{+-JGZ zG^Y*-X|;lj4!qJog3-S)*bU$4aKubcjU(!S)iHz$e$ zT@$gjHNx-q&3iZ3mF^CBY5aZb;;1zsC$8GM;OhIhY24FFGp$$dOMh2=Tqq3W1I5h? zqRPI1U)A&Z+o!11IUVI&z>b~I$~$%2+%2U%x$d?9qc8Q%VfCHBc;HaI_>{%#_f9#} zRCH{OskK%jE70p^uUc>1*dZ`mfw`eKNo>li*)yl;9Pv8VnFf~JC3Z#qdG$g=oi%M> z|3SlRo0)ZzYL@Q4JAz<+d5Y6E&zyI8R#xsi_st*&6uajzlpj$$!H}>xqQmuk(4Wjj zRgw%jB|z8S|L^jq&ysr<&_2<*;wE1<^!+^gbp89U)AQct?SB1Yb2He#Hk;mX%SC?c zmta^E#T_MC;>5n)ZOhy#;2`qgj@sFH{CfRqE?_L`>|uRb4Gsz%kqM69Z=9#TJ(7)c STqgrePz;`~elF{r5}E*Vu?@`t literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_5.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_5.png new file mode 100644 index 0000000000000000000000000000000000000000..6f99eb8f47b26a98662733fe7196f31c7cd37aee GIT binary patch literal 630 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>V6ydeaSW-L^Y)In*AWL1w?ySU zhYPVkW~|WYV^MF?E@IrPbg=V*2TSRacZX#q(qCp7gq1Lg)vhBFKaYz)Q>GmwOGw12VqUS9L{^Sf=ivrjyZW&|p1 z-LhcI+Zx-L^8NemY|C!Dr$68SZ=ToplM{>Q%*pn6uewM!5~R#@Y22yjTjoxleO~E# z*EgHn@3vho+frG0J9m1Pdl<;B#cqzyx6i$N_;JSNwP#MA_1^y1X7+vWs{W;J5c#8O zNtU_k>E+9>ymVT6{m%71uZ>JV2Wnqy5}UvGjJnS2OJzZ|*YA|~Cr5yt)zhT(M)nQ= zt^beT?)_ova%Zt9&@&M~G&J0A#a+5~@w;`YkGrFA3d4pE@0)&o>CHN}rEkvvXK(g< zfn5{of23{OlsUD>3Y5$o*%%H39mgxnx|UlT7&;qXMP0bS8L~1tXbmWIR%JL|)J=NP zk~bC1-C&+{kp(iavCv2h%R0baztZkTiNyFT);qG^U6Fa`PRhwYFgbuoj*(r z-FcNI)ssN(Smo}xwNS(aXq)ubxCv7RDE$Z>0Zfe_ZzNwd`+T PdSdW&^>bP0l+XkK_q-5t literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_6.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_6.png new file mode 100644 index 0000000000000000000000000000000000000000..d545a0ddc11985073662ba4775cdced83a3a8630 GIT binary patch literal 611 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>VAAq*aSW-L^Y)Iv*I@^VwnF7N zhZWmDoOkPfzU~Z6PIKz;@#(*p&yCiPGs#Rn3lq& z721`(g$d{dZ#&knr8QMXe^RdfsQG{6NB!I;t`vq1Pu{bZ3T~ULsH?Pn>#hwTg@^Nw z+=@ClBRJrG*^}OgMjnQ3mxBZN)_?+KQ-)*mJ%L`!!cvIwuG~djLdO^qZU}GOmYH%! z?Be#jKV8;1urZuY;#ySKcdk(s7;+J1>(!oJQ*yEs)Sdt|qT19j{a%OY35JB5zK)i~ zTv|XuwzuL}$~X7@zP-Emy42-I9~WBs?Jb`G(mW%2+TVk^r^`AR4=mccV5+`u#^Y-u zx#}rk-z{NXtMlO7*KBams^&XJod<;>MuZ^?)iKyM-I&p(y5t_Fb literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_7.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_7.png new file mode 100644 index 0000000000000000000000000000000000000000..6de658d90f0575402518fd8721274f0993584065 GIT binary patch literal 614 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>VAAz;aSW-L^Y)In7qg>?TVnK` zh6TGn7zPH)aQd-?KVjOWbg=V*2TSRacZX$foU^)`aI`P>dhp9R`Bz#l@YlV28fF!8 zdE47Ii`idz*H|+*NHUyZNMK_yW|+ZvfQKQC!2n6$`*|Ep5#?>PbIbH-2PE~R&7xQBs^^j$gqlk=@}C(ou& z%yzAwms@^3)B4t)9dG-*w<>D`?b#3*d|^}W^DiGN&OH5h_T*Xb@;^4S?Y)2XEL{eX zPvYLFn46wnzWmBlr={QLWVCI!hE7DiNZ(8sYzTicOq7tF30a8Q5!~djwB!ofVTU zWwg=7$8?|IYz5|qP(#+IEQM9qvCT}lVh$Ra8}icvp5*O4JU1Meei%Gm{an^LB{Ts5zAOSt literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_8.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_8.png new file mode 100644 index 0000000000000000000000000000000000000000..2d99d3c0b2e5f3a5a3b652e1b88f63a321c47100 GIT binary patch literal 611 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>VAAq*aSW-L^Y)Iv*I@^VwnF7N zhYQg^(ibnzU~Z6PIKz;@#(*p&yF_lms#Sse->-YU%d4_h8>n!D zimpbEwf*_Y&%S@ZKK|1&QDq-8_5VXEp(&Zsq#$g@XPNt zl9z6qA!~L#(fZc@+2+SL-kK;1wB*Ku$bkFXifijEQ+BM~IcuNk@$Q?SA`hkam|AaQ z0y?96o51T!Wqp19NxaiTD$eJ=)er+)pP`%a_3r7SgGSS~31{tozAatm5EsztIwy5j zglNvY2xkA($>^B8`Eu{`n%WvS$<^HHj;uxu5ug6Ee!aT%?UW;6HM?7Hn5CQ6?iA3T zz<6NOY}VGI=#HfctiZsS7RxQQNkBUJG}s-d*RhrgZkxDISKG$Ey(|T+;H&sXvn$%! z`#VHWFeEIV$;vzP=ayJ?_ApQYF8-tBwp9=+71I$I16FkNY*W!QofQsj4ANWT1MZ)- zQv1&TUoLz0Gyndxg|GHDuK~rwolS4}^CGu7^Dv05WnCMyWkGLr@Rqp~K%o^eNo^u7(8A5T-G@yGywo2!up#4 literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_9.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_9.png new file mode 100644 index 0000000000000000000000000000000000000000..a60be77e7c425baa5ec82ef51665195cae62e749 GIT binary patch literal 618 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>U^4V{aSW-L^Y)Iv*I@^VwnF7N zhZWmDoOkPf`4?`M~kadULf>o>5*4>Z2UMgBys|}Rg zFhOj}wz=<@|9FzCaKR(eu z9dYs5<-^Y`a@~KMZ+>PGZoP>KXxnR3?x^i=W9G$vk<3dzHu?Iq3cJ6x)4oU+pM~h( zq*^b@sR#Pv1)gw|Z_q-u}#@>b(LegblXqL?6;UUDm;PVAIwGt=)VXkFRxX zn+y&$?o3B+<{hsg7^2PYG_TFx5T{Qj_Udg>1nfOF8TWVe^}xPl}jI! z<@7x}49yx=-MmuIV4%vxkjcVuiGyK@AcKc8!vr@5gzaL#J zk$j_6@7wVSrC*+1JHQ9lv}(FT+3DYvd+%1Ad-E*y`Of?A^WT2FdA0O?#k)KImmGWl z`0Uo~=TlAY&02Oww^bHwNvLMyo=d7rw|Kl1s!$ES{G{*syy6#Gf9&p7czGsQ{^$Q@ zwe_@O8Q3LaUbQ0jOWvF;{kS2oUtj$B(e?c$@~hW>`|?eME`}LnEZC=Mo9G_siHOqcV3Om>{M|U|$i<`FV2YZmH=)|tu50>1_m2k{6&NO%WpT2+k%qV*JtUEX_^tQ?i@i2Y T2QCEyBbLF_)z4*}Q$iB}U9;kp literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Settings_Bar01.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Settings_Bar01.png new file mode 100644 index 0000000000000000000000000000000000000000..0878de6da683d606b786f062b6ddf48d8b0bb08f GIT binary patch literal 255 zcmeAS@N?(olHy`uVBq!ia0vp^2Y}dsgAGW!`<2Q8sbiiljv*Cu-rjNKWik+8aP$&z zywD`)vXEVuMcmaz-gmPx*Sh|*UoZDrK4)FEZZ~_wTo#8}3?9M^nM?}43`;m0Oc*Dq zFI|8({Wgl`q@W(4|(!PC{xWt~$(69AS$Ph8fjv*Cu-rhOL%dE)5a8O|B zA?+_wDy| zqE_tOi>?M064t>7RlC!b-WH#0T6O*l!weOMODqnv7(9d-GMN;78J2K1m@rOI!zcLL zkAKJ3`%hQ@b9^3IU3HjO9HishcmD-n{rEeom$Ba|WCWRZd%d7$F_=Z9xiA;}Ww3m} V6)RTTIf*$; zDQ=BY?Tu#E8_lW_t?Gg*3Smj?>yE!sbTt2+C^4g)=S_dVrMarlh6AvKYN}d_KQWO!Hp@yEBeJxsqJU;tn+5<$o)5St;2kVy$-va zKCTmbZ}40%yz_ACuQNJdI4^KVthTrK61ZQWsxeK}$#H%I%LNaHUMNw+Xd-OGcT=PD QG0>k3p00i_>zopr0AR6jxc~qF literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Settings_Cross02.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Settings_Cross02.png new file mode 100644 index 0000000000000000000000000000000000000000..109261ab0854c72a66ef97bb76ed403008b5c149 GIT binary patch literal 348 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezr3(Fe-bxIEG|2zMbXBb;yCoRsAS; z#U!N;g>`p!f9JS&kh!8=?%>|)a3v8(n@;nBJC>S08QzzV@mH=6S{7{g-`F!l^O(dI z#YJzH8M3swDhGVP| zkD1mqiW{t$;Q6;dQv z$x~WHW?%g{cW?asXE%S__{Bb$*j&yK=dgH!ucc?^UU85C&0LXN{jYZ$)gSV(iGR7o zYEHH7tJXK`Gv+we-j0&nskOwzqi|Bv{s@mDvCQ literal 0 HcmV?d00001 diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Settings_Cross03.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Settings_Cross03.png new file mode 100644 index 0000000000000000000000000000000000000000..abb3e3de0e110d88876156f8b83485708e7cb81f GIT binary patch literal 369 zcmV-%0gnEOP)1;9W!zSMEKL2k=DhT@^tD@g&~B=dQd$gD=iQ2~1X9%%>qS?@h;IW{Yf; zQf3!5vy#16m%ZSxAy1&sTFBexb>4p*lVvA)1>^~YEX1bD?J`g=QIlHo3dj?H7GhI{ zMOJFbD`EuYf#(5^1M>8eEUd*)gllc6>&bh>Teup5t>X zWD1yAh%HRWPAt4_BJkFdS3sUXAL&UDB1z(bEXVHa-&tM(c>= this.frames.length) { + throw new Error(`Index de frame invalide: ${index}`); + } + return this.frames[index]!; + } +} + diff --git a/src/engine/assets/AssetLoader.ts b/src/engine/assets/AssetLoader.ts new file mode 100644 index 0000000..1431a23 --- /dev/null +++ b/src/engine/assets/AssetLoader.ts @@ -0,0 +1,224 @@ +/** + * AssetLoader - Système centralisé de chargement d'assets + * Gère le cache, le préchargement et la progression + */ + +import { Texture } from '../rendering/Texture'; + +export interface AssetManifest { + textures?: Array<{ name: string; url: string }>; + json?: Array<{ name: string; url: string }>; +} + +export interface LoadingProgress { + total: number; + loaded: number; + percentage: number; + current?: string; // Nom de l'asset en cours de chargement +} + +export type ProgressCallback = (progress: LoadingProgress) => void; + +export class AssetLoader { + private _gl: WebGLRenderingContext | WebGL2RenderingContext; + private _textures: Map = new Map(); + private _json: Map = new Map(); + + private _loadingPromises: Map> = new Map(); + + constructor(gl: WebGLRenderingContext | WebGL2RenderingContext) { + this._gl = gl; + } + + /** + * Charge une texture et la met en cache + */ + async loadTexture(name: string, url: string): Promise { + // Vérifie si déjà chargée + if (this._textures.has(name)) { + return this._textures.get(name)!; + } + + // Vérifie si en cours de chargement + if (this._loadingPromises.has(`texture:${name}`)) { + return await this._loadingPromises.get(`texture:${name}`)! as Texture; + } + + // Lance le chargement + const promise = Texture.loadFromURL(this._gl, url).then((texture) => { + this._textures.set(name, texture); + this._loadingPromises.delete(`texture:${name}`); + return texture; + }).catch((error) => { + this._loadingPromises.delete(`texture:${name}`); + throw new Error(`Échec du chargement de la texture "${name}": ${error}`); + }); + + this._loadingPromises.set(`texture:${name}`, promise); + return await promise; + } + + /** + * Charge un fichier JSON et le met en cache + */ + async loadJSON(name: string, url: string): Promise { + // Vérifie si déjà chargé + if (this._json.has(name)) { + return this._json.get(name)! as T; + } + + // Vérifie si en cours de chargement + if (this._loadingPromises.has(`json:${name}`)) { + return await this._loadingPromises.get(`json:${name}`)! as T; + } + + // Lance le chargement + const promise = fetch(url) + .then((response) => { + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + return response.json(); + }) + .then((data: T) => { + this._json.set(name, data); + this._loadingPromises.delete(`json:${name}`); + return data; + }) + .catch((error) => { + this._loadingPromises.delete(`json:${name}`); + throw new Error(`Échec du chargement du JSON "${name}": ${error}`); + }); + + this._loadingPromises.set(`json:${name}`, promise); + return await promise; + } + + /** + * Précharge plusieurs assets avec progression + */ + async loadAll(manifest: AssetManifest, onProgress?: ProgressCallback): Promise { + const allAssets: Array<{ name: string; type: 'texture' | 'json'; url: string }> = []; + + if (manifest.textures) { + for (const tex of manifest.textures) { + allAssets.push({ name: tex.name, type: 'texture', url: tex.url }); + } + } + + if (manifest.json) { + for (const json of manifest.json) { + allAssets.push({ name: json.name, type: 'json', url: json.url }); + } + } + + const total = allAssets.length; + let loaded = 0; + + // Lance tous les chargements en parallèle + const promises = allAssets.map(async (asset) => { + try { + if (asset.type === 'texture') { + await this.loadTexture(asset.name, asset.url); + } else { + await this.loadJSON(asset.name, asset.url); + } + + loaded++; + if (onProgress) { + onProgress({ + total, + loaded, + percentage: loaded / total, + current: asset.name + }); + } + } catch (error) { + console.error(`Erreur lors du chargement de ${asset.name}:`, error); + // Continue quand même + loaded++; + if (onProgress) { + onProgress({ + total, + loaded, + percentage: loaded / total, + current: asset.name + }); + } + } + }); + + await Promise.all(promises); + } + + /** + * Récupère une texture par son nom + */ + getTexture(name: string): Texture | null { + return this._textures.get(name) || null; + } + + /** + * Récupère des données JSON par nom + */ + getJSON(name: string): T | null { + return (this._json.get(name) as T) || null; + } + + /** + * Vérifie si une texture est chargée + */ + hasTexture(name: string): boolean { + return this._textures.has(name); + } + + /** + * Vérifie si un JSON est chargé + */ + hasJSON(name: string): boolean { + return this._json.has(name); + } + + /** + * Libère une texture de la mémoire + */ + unloadTexture(name: string): void { + const texture = this._textures.get(name); + if (texture) { + texture.dispose(); + this._textures.delete(name); + } + } + + /** + * Libère toutes les ressources + */ + dispose(): void { + // Libère toutes les textures + for (const texture of this._textures.values()) { + texture.dispose(); + } + this._textures.clear(); + + // Nettoie les JSON + this._json.clear(); + + // Nettoie les promesses en cours + this._loadingPromises.clear(); + } + + /** + * Liste toutes les textures chargées + */ + get loadedTextures(): string[] { + return Array.from(this._textures.keys()); + } + + /** + * Liste tous les JSON chargés + */ + get loadedJSONs(): string[] { + return Array.from(this._json.keys()); + } +} + diff --git a/src/engine/assets/SpriteSheet.ts b/src/engine/assets/SpriteSheet.ts new file mode 100644 index 0000000..726517c --- /dev/null +++ b/src/engine/assets/SpriteSheet.ts @@ -0,0 +1,153 @@ +/** + * SpriteSheet - Gestion d'un spritesheet (image avec plusieurs sprites) + * Découpe une texture en plusieurs sprites avec dimensions uniformes + */ + +import { Texture } from '../rendering/Texture'; +import { Rect } from '../math/Rect'; + +export interface SpriteInfo { + name: string; + x: number; + y: number; + width: number; + height: number; +} + +export class SpriteSheet { + private _texture: Texture; + private _sprites: Map = new Map(); + private _spriteWidth: number; + private _spriteHeight: number; + private _columns: number; + private _rows: number; + + /** + * Crée un SpriteSheet avec une grille uniforme + * @param texture Texture source + * @param spriteWidth Largeur d'un sprite + * @param spriteHeight Hauteur d'un sprite + */ + constructor(texture: Texture, spriteWidth: number, spriteHeight: number) { + this._texture = texture; + this._spriteWidth = spriteWidth; + this._spriteHeight = spriteHeight; + this._columns = Math.floor(texture.width / spriteWidth); + this._rows = Math.floor(texture.height / spriteHeight); + + // Crée automatiquement les sprites de la grille + this._createGridSprites(); + } + + /** + * Crée les sprites à partir d'une configuration JSON + * @param sprites Liste des sprites avec leurs positions + */ + createSprites(sprites: SpriteInfo[]): void { + for (const sprite of sprites) { + const rect = new Rect( + sprite.x, + sprite.y, + sprite.width, + sprite.height + ); + this._sprites.set(sprite.name, rect); + } + } + + /** + * Récupère le Rect d'un sprite par son nom + */ + getSprite(name: string): Rect | null { + return this._sprites.get(name) || null; + } + + /** + * Récupère le Rect d'un sprite par son index dans la grille (0-based) + */ + getSpriteByIndex(index: number): Rect | null { + const column = index % this._columns; + const row = Math.floor(index / this._columns); + return this.getSpriteByGridPosition(column, row); + } + + /** + * Récupère le Rect d'un sprite par sa position dans la grille (0-based) + */ + getSpriteByGridPosition(column: number, row: number): Rect | null { + if (column < 0 || column >= this._columns || row < 0 || row >= this._rows) { + return null; + } + + const x = column * this._spriteWidth; + const y = row * this._spriteHeight; + return new Rect(x, y, this._spriteWidth, this._spriteHeight); + } + + /** + * Texture source + */ + get texture(): Texture { + return this._texture; + } + + /** + * Largeur d'un sprite + */ + get spriteWidth(): number { + return this._spriteWidth; + } + + /** + * Hauteur d'un sprite + */ + get spriteHeight(): number { + return this._spriteHeight; + } + + /** + * Nombre de colonnes dans la grille + */ + get columns(): number { + return this._columns; + } + + /** + * Nombre de lignes dans la grille + */ + get rows(): number { + return this._rows; + } + + /** + * Nombre total de sprites dans la grille + */ + get totalSprites(): number { + return this._columns * this._rows; + } + + /** + * Liste tous les noms de sprites + */ + get spriteNames(): string[] { + return Array.from(this._sprites.keys()); + } + + /** + * Crée automatiquement les sprites de la grille uniforme + */ + private _createGridSprites(): void { + for (let row = 0; row < this._rows; row++) { + for (let col = 0; col < this._columns; col++) { + const x = col * this._spriteWidth; + const y = row * this._spriteHeight; + const rect = new Rect(x, y, this._spriteWidth, this._spriteHeight); + + // Nom par défaut : "sprite_0_0", "sprite_0_1", etc. + const name = `sprite_${row}_${col}`; + this._sprites.set(name, rect); + } + } + } +} + diff --git a/src/engine/assets/TextureAtlas.ts b/src/engine/assets/TextureAtlas.ts new file mode 100644 index 0000000..53ae2ba --- /dev/null +++ b/src/engine/assets/TextureAtlas.ts @@ -0,0 +1,121 @@ +/** + * TextureAtlas - Gestion d'un atlas de textures + * Utilise un fichier JSON pour définir les régions de chaque sprite dans l'atlas + */ + +import { Texture } from '../rendering/Texture'; +import { Rect } from '../math/Rect'; + +export interface AtlasFrame { + frame: { x: number; y: number; w: number; h: number }; + rotated?: boolean; + trimmed?: boolean; + spriteSourceSize?: { x: number; y: number; w: number; h: number }; + sourceSize?: { w: number; h: number }; + pivot?: { x: number; y: number }; +} + +export interface AtlasJSON { + frames: Record; + meta?: { + image?: string; + size?: { w: number; h: number }; + format?: string; + scale?: number; + }; +} + +export class TextureAtlas { + private _texture: Texture; + private _frames: Map = new Map(); + private _atlasData: AtlasJSON; + + /** + * Crée un TextureAtlas à partir d'une texture et d'un JSON d'atlas + * @param texture Texture source de l'atlas + * @param atlasData Données JSON de l'atlas (format compatible TexturePacker, etc.) + */ + constructor(texture: Texture, atlasData: AtlasJSON) { + this._texture = texture; + this._atlasData = atlasData; + this._parseAtlasData(); + } + + /** + * Récupère le Rect d'un sprite par son nom + */ + getSprite(name: string): Rect | null { + return this._frames.get(name) || null; + } + + /** + * Vérifie si un sprite existe dans l'atlas + */ + hasSprite(name: string): boolean { + return this._frames.has(name); + } + + /** + * Texture source de l'atlas + */ + get texture(): Texture { + return this._texture; + } + + /** + * Données JSON de l'atlas + */ + get atlasData(): AtlasJSON { + return this._atlasData; + } + + /** + * Liste tous les noms de sprites dans l'atlas + */ + get spriteNames(): string[] { + return Array.from(this._frames.keys()); + } + + /** + * Nombre de sprites dans l'atlas + */ + get spriteCount(): number { + return this._frames.size; + } + + /** + * Parse les données JSON de l'atlas + */ + private _parseAtlasData(): void { + if (!this._atlasData.frames) { + console.warn('Atlas JSON ne contient pas de "frames"'); + return; + } + + for (const [name, frameData] of Object.entries(this._atlasData.frames)) { + const frame = frameData.frame; + const rect = new Rect(frame.x, frame.y, frame.w, frame.h); + this._frames.set(name, rect); + } + } + + /** + * Crée un TextureAtlas depuis un AssetLoader + * Charge automatiquement la texture et le JSON + * @param assetLoader AssetLoader avec les assets chargés + * @param textureName Nom de la texture dans l'AssetLoader + * @param jsonName Nom du JSON dans l'AssetLoader + */ + static fromAssetLoader(assetLoader: { getTexture(name: string): Texture | null; getJSON(name: string): any }, textureName: string, jsonName: string): TextureAtlas | null { + const texture = assetLoader.getTexture(textureName); + const json = assetLoader.getJSON(jsonName) as AtlasJSON | null; + + if (!texture || !json) { + console.error(`Impossible de créer TextureAtlas: texture "${textureName}" ou JSON "${jsonName}" manquant`); + return null; + } + + return new TextureAtlas(texture, json); + } +} + diff --git a/src/engine/assets/index.ts b/src/engine/assets/index.ts new file mode 100644 index 0000000..d8161a9 --- /dev/null +++ b/src/engine/assets/index.ts @@ -0,0 +1,8 @@ +/** + * Exports du système Asset Management + */ + +export { AssetLoader, type AssetManifest, type LoadingProgress, type ProgressCallback } from './AssetLoader'; +export { SpriteSheet, type SpriteInfo } from './SpriteSheet'; +export { TextureAtlas, type AtlasJSON, type AtlasFrame } from './TextureAtlas'; + diff --git a/src/engine/components/Animator.ts b/src/engine/components/Animator.ts new file mode 100644 index 0000000..51d5a6e --- /dev/null +++ b/src/engine/components/Animator.ts @@ -0,0 +1,196 @@ +/** + * Animator - Gère les animations de sprite + * Anime un SpriteRenderer en changeant son sourceRect + */ + +import { Component } from '../entities/Component'; +import { SpriteRenderer } from './SpriteRenderer'; +import { Animation } from '../animation/Animation'; + +export class Animator extends Component { + private _animations: Map = new Map(); + private _currentAnimation: Animation | null = null; + private _currentFrame: number = 0; + private _frameTime: number = 0; // Temps écoulé sur la frame actuelle + private _isPlaying: boolean = false; + private _speed: number = 1.0; // Multiplicateur de vitesse (1 = normal, 2 = 2× plus rapide) + private _spriteRenderer: SpriteRenderer | null = null; + + /** + * Animation actuellement en cours + */ + get currentAnimation(): Animation | null { + return this._currentAnimation; + } + + /** + * Frame actuelle de l'animation + */ + get currentFrame(): number { + return this._currentFrame; + } + + /** + * Indique si l'animation est en cours de lecture + */ + get isPlaying(): boolean { + return this._isPlaying; + } + + /** + * Vitesse de lecture (1 = normal, 2 = 2× plus rapide, 0.5 = 2× plus lent) + */ + get speed(): number { + return this._speed; + } + + set speed(value: number) { + this._speed = Math.max(0, value); + } + + /** + * Initialise l'Animator et trouve le SpriteRenderer + */ + awake(): void { + // Cherche le SpriteRenderer sur le même GameObject + this._spriteRenderer = this.getComponent(SpriteRenderer); + + if (!this._spriteRenderer) { + console.warn(`Animator sur "${this.gameObject.name}" ne trouve pas de SpriteRenderer`); + } + } + + /** + * Ajoute une animation à l'animator + */ + addAnimation(name: string, animation: Animation): void { + this._animations.set(name, animation); + } + + /** + * Joue une animation + * @param name Nom de l'animation + * @param restart Si true, redémarre même si c'est déjà l'animation en cours + */ + play(name: string, restart: boolean = false): void { + const animation = this._animations.get(name); + + if (!animation) { + console.warn(`Animation "${name}" introuvable`); + return; + } + + // Si c'est la même animation et qu'on ne veut pas redémarrer, on continue + if (this._currentAnimation === animation && !restart && this._isPlaying) { + return; + } + + this._currentAnimation = animation; + this._currentFrame = 0; + this._frameTime = 0; + this._isPlaying = true; + + // Met à jour le sprite immédiatement + this._updateSpriteFrame(); + } + + /** + * Arrête l'animation + */ + stop(): void { + this._isPlaying = false; + } + + /** + * Met en pause + */ + pause(): void { + this._isPlaying = false; + } + + /** + * Reprend l'animation + */ + resume(): void { + if (this._currentAnimation) { + this._isPlaying = true; + } + } + + /** + * Met à jour l'animation + */ + update(deltaTime: number): void { + if (!this._isPlaying || !this._currentAnimation || !this._spriteRenderer) { + return; + } + + // Avance le temps + this._frameTime += deltaTime * this._speed; + const frameDuration = this._currentAnimation.frameDuration; + + // Passe à la frame suivante si nécessaire + while (this._frameTime >= frameDuration) { + this._frameTime -= frameDuration; + this._currentFrame++; + + // Vérifie si on est à la fin de l'animation + if (this._currentFrame >= this._currentAnimation.frameCount) { + if (this._currentAnimation.loop) { + // Boucle : retourne au début + this._currentFrame = 0; + } else { + // One-shot : reste sur la dernière frame et arrête + this._currentFrame = this._currentAnimation.frameCount - 1; + this._isPlaying = false; + break; + } + } + + // Déclenche les événements à cette frame + this._triggerEvents(this._currentFrame); + } + + // Met à jour le sprite + this._updateSpriteFrame(); + } + + /** + * Met à jour le sourceRect du SpriteRenderer avec la frame actuelle + */ + private _updateSpriteFrame(): void { + if (!this._currentAnimation || !this._spriteRenderer) { + return; + } + + const frame = this._currentAnimation.getFrame(this._currentFrame); + this._spriteRenderer.sourceRect = frame; + } + + /** + * Déclenche les événements à une frame spécifique + */ + private _triggerEvents(frame: number): void { + if (!this._currentAnimation) { + return; + } + + for (const event of this._currentAnimation.events) { + if (event.frame === frame) { + // Dispatch l'événement (pour l'instant juste un log, peut être étendu avec EventBus) + console.log(`Animation event: ${event.event} at frame ${frame}`, event.data); + // TODO: Intégrer avec EventBus quand il sera créé + } + } + } + + /** + * Nettoie le composant + */ + onDestroy(): void { + this._animations.clear(); + this._currentAnimation = null; + this._spriteRenderer = null; + } +} + diff --git a/src/engine/components/SpriteRenderer.ts b/src/engine/components/SpriteRenderer.ts new file mode 100644 index 0000000..b6380cf --- /dev/null +++ b/src/engine/components/SpriteRenderer.ts @@ -0,0 +1,159 @@ +/** + * SpriteRenderer - Composant pour afficher un sprite 2D + * Utilise SpriteBatch pour le rendu optimisé + */ + +import { Component } from '../entities/Component'; +import { Texture } from '../rendering/Texture'; +import { Rect } from '../math/Rect'; +import { Color } from '../math/Color'; +import { Vector2 } from '../math/Vector2'; +import { SpriteBatch } from '../rendering/SpriteBatch'; + +export class SpriteRenderer extends Component { + private _texture: Texture | null = null; + private _sourceRect: Rect | null = null; + private _tint: Color = Color.white; + private _flipX: boolean = false; + private _flipY: boolean = false; + private _layer: number = 0; + private _pivot: Vector2 = new Vector2(0.5, 0.5); // 0.5, 0.5 = centre + + /** + * Texture à afficher + */ + get texture(): Texture | null { + return this._texture; + } + + set texture(value: Texture | null) { + this._texture = value; + // Si sourceRect n'est pas défini, utilise toute la texture + if (value && !this._sourceRect) { + this._sourceRect = new Rect(0, 0, value.width, value.height); + } + } + + /** + * Quelle partie de la texture afficher (pour spritesheets) + */ + get sourceRect(): Rect | null { + return this._sourceRect; + } + + set sourceRect(value: Rect | null) { + this._sourceRect = value; + } + + /** + * Couleur de teinte (blanc = normal, modifie les couleurs du sprite) + */ + get tint(): Color { + return this._tint; + } + + set tint(value: Color) { + this._tint = value; + } + + /** + * Miroir horizontal + */ + get flipX(): boolean { + return this._flipX; + } + + set flipX(value: boolean) { + this._flipX = value; + } + + /** + * Miroir vertical + */ + get flipY(): boolean { + return this._flipY; + } + + set flipY(value: boolean) { + this._flipY = value; + } + + /** + * Layer de rendu (z-index pour le tri) + */ + get layer(): number { + return this._layer; + } + + set layer(value: number) { + this._layer = value; + } + + /** + * Point d'ancrage (0, 0 = haut-gauche, 0.5, 0.5 = centre, 1, 1 = bas-droite) + */ + get pivot(): Vector2 { + return this._pivot; + } + + set pivot(value: Vector2) { + this._pivot = value; + } + + /** + * Définit le sprite (texture + sourceRect en une seule fois) + */ + setSprite(texture: Texture, sourceRect?: Rect): void { + this._texture = texture; + if (sourceRect) { + this._sourceRect = sourceRect; + } else { + this._sourceRect = new Rect(0, 0, texture.width, texture.height); + } + } + + /** + * Rend le sprite via SpriteBatch + * @internal - Appelé par le système de rendu + */ + render(spriteBatch: SpriteBatch): void { + if (!this._texture || !this.gameObject.active || !this.enabled) { + return; + } + + const transform = this.transform; + const position = transform.position; + const rotation = transform.rotation * Math.PI / 180; // Convertit degrés → radians + const scale = transform.scale; + + // Calcule le sourceRect effectif (avec flip) + let sourceRect = this._sourceRect; + if (!sourceRect) { + sourceRect = new Rect(0, 0, this._texture.width, this._texture.height); + } + + // Gère le flip en inversant sourceRect si nécessaire + // Note: Le flip peut aussi être géré dans le shader ou en ajustant les UVs + // Pour l'instant, on laisse SpriteBatch gérer avec les matrices de transformation + + spriteBatch.draw( + this._texture, + position, + sourceRect, + this._tint, + rotation, + scale, + this._pivot + ); + } + + /** + * Nettoie le composant + */ + onDestroy(): void { + // Les textures sont gérées par le cache du renderer, pas besoin de les libérer ici + this._texture = null; + this._sourceRect = null; + } +} + diff --git a/src/engine/core/Engine.ts b/src/engine/core/Engine.ts new file mode 100644 index 0000000..d383c82 --- /dev/null +++ b/src/engine/core/Engine.ts @@ -0,0 +1,401 @@ +/** + * Engine - Orchestrateur principal + * Point d'entrée, initialise et coordonne tous les systèmes + */ + +import { GLContext } from '../webgl/GLContext'; +import { WebGLRenderer } from '../rendering/WebGLRenderer'; +import { SceneManager } from './SceneManager'; +import { Scene } from './Scene'; +import { GameLoop } from './GameLoop'; +import { InputManager } from '../input/InputManager'; +import { PhysicsSystem } from '../physics/PhysicsSystem'; +import { AssetLoader } from '../assets/AssetLoader'; +import { DebugPanel } from '../debug/DebugPanel'; + +export class Engine { + private _canvas: HTMLCanvasElement; + private _glContext: GLContext; + private _renderer: WebGLRenderer; + private _sceneManager: SceneManager; + private _inputManager: InputManager; + private _physicsSystem: PhysicsSystem; + private _assetLoader: AssetLoader; + private _gameLoop: GameLoop; + private _debugPanel: DebugPanel; + private _isRunning: boolean = false; + private _isPaused: boolean = false; + private _frameCount: number = 0; + private _lastFpsUpdate: number = 0; + private _currentFps: number = 0; + + constructor(canvasId: string | HTMLCanvasElement, width: number = 800, height: number = 600) { + // Récupère ou crée le canvas + if (typeof canvasId === 'string') { + const canvas = document.getElementById(canvasId) as HTMLCanvasElement; + if (!canvas) { + throw new Error(`Canvas "${canvasId}" introuvable`); + } + this._canvas = canvas; + } else { + this._canvas = canvasId; + } + + // Initialise GLContext + this._glContext = new GLContext(); + if (!this._glContext.initialize(this._canvas)) { + throw new Error('Échec de l\'initialisation WebGL'); + } + + this._glContext.resize(width, height); + + // Crée le renderer + this._renderer = new WebGLRenderer(this._glContext); + + // Crée le SceneManager + this._sceneManager = new SceneManager(); + + // Crée l'InputManager + this._inputManager = new InputManager(); + + // Crée le PhysicsSystem + this._physicsSystem = new PhysicsSystem(); + + // Crée l'AssetLoader + this._assetLoader = new AssetLoader(this._glContext.gl); + + // Crée le DebugPanel + this._debugPanel = new DebugPanel(); + + // Crée la GameLoop + this._gameLoop = new GameLoop(); + this._setupGameLoop(); + } + + /** + * Configure la GameLoop avec les callbacks + */ + private _setupGameLoop(): void { + // Update (logique de jeu) + this._gameLoop.onUpdate((deltaTime: number, currentTime: number) => { + if (!this._isPaused) { + // Update la scène EN PREMIER pour que les composants puissent détecter Down + this._sceneManager.update(deltaTime); + + // Update Debug Panel + this._updateDebugPanel(deltaTime, currentTime); + } + + // Update InputManager EN DERNIER pour préparer les états pour la prochaine frame + // (Down -> Held, Released -> Up) + this._inputManager.update(); + }); + + // Fixed Update (physique) + this._gameLoop.onFixedUpdate((_fixedDeltaTime: number) => { + if (!this._isPaused) { + const activeScene = this._sceneManager.getActiveScene(); + if (activeScene) { + this._physicsSystem.update(activeScene); + } + } + }); + + // Render + this._gameLoop.onRender(() => { + if (!this._isPaused) { + this._sceneManager.render(this._renderer); + } + }); + } + + /** + * Met à jour le panel de debug avec les informations courantes + */ + private _updateDebugPanel(deltaTime: number, currentTime: number): void { + // Calcule le FPS + this._frameCount++; + if (currentTime - this._lastFpsUpdate >= 1000) { + this._currentFps = this._frameCount; + this._frameCount = 0; + this._lastFpsUpdate = currentTime; + } + + const activeScene = this._sceneManager.getActiveScene(); + if (!activeScene) return; + + // Collecte les informations de base + const debugInfo: any = { + fps: this._currentFps, + deltaTime: deltaTime, + entityCount: activeScene.gameObjects.length, + cameraPosition: { + x: activeScene.camera.position.x, + y: activeScene.camera.position.y + }, + mouseScreenPos: { + x: this._inputManager.getMousePosition().x, + y: this._inputManager.getMousePosition().y + }, + mouseWorldPos: { + x: this._inputManager.getMouseWorldPosition(activeScene.camera).x, + y: this._inputManager.getMouseWorldPosition(activeScene.camera).y + } + }; + + // Cherche le joueur dans la scène + const player = activeScene.findGameObject('Player'); + if (player) { + debugInfo.playerPosition = { + x: player.transform.position.x, + y: player.transform.position.y + }; + + // Récupère les composants par nom de classe + const components = player.getComponents(); + + // Cherche Rigidbody2D + const rigidbody = components.find(c => c.constructor.name === 'Rigidbody2D'); + if (rigidbody && 'velocity' in rigidbody) { + debugInfo.playerVelocity = { + x: (rigidbody as any).velocity.x, + y: (rigidbody as any).velocity.y + }; + } + + // Cherche PlayerController + const controller = components.find(c => c.constructor.name === 'PlayerController'); + if (controller) { + // Utilise les getters publics + if ('direction' in controller) { + debugInfo.playerDirection = (controller as any).direction; + } + + // État du joueur (priorité: attacking > dashing > walking > idle) + const states = []; + if ('isPlayerAttacking' in controller && (controller as any).isPlayerAttacking) { + states.push('attacking'); + } else if ('isPlayerDashing' in controller && (controller as any).isPlayerDashing) { + states.push('dashing'); + } else if ('isPlayerMoving' in controller && (controller as any).isPlayerMoving) { + states.push('walking'); + } + debugInfo.playerState = states.length > 0 ? states.join(', ') : 'idle'; + } + + // Cherche HealthComponent + const healthComponent = components.find(c => c.constructor.name === 'HealthComponent'); + if (healthComponent) { + if ('health' in healthComponent && 'maxHealth' in healthComponent) { + debugInfo.playerHealth = { + current: (healthComponent as any).health, + max: (healthComponent as any).maxHealth + }; + } + } + } + + // Compte les colliders actifs + let activeColliders = 0; + activeScene.gameObjects.forEach(go => { + if (go.active) { + const colliders = go.getComponents().filter(c => + c.constructor.name.includes('Collider') + ); + activeColliders += colliders.length; + } + }); + debugInfo.activeColliders = activeColliders; + + this._debugPanel.setInfo(debugInfo); + this._debugPanel.update(currentTime); + } + + /** + * Initialise le moteur (charge les ressources, etc.) + */ + async initialize(): Promise { + // Initialise le renderer + if (!this._renderer.initialize()) { + console.error('Échec de l\'initialisation du renderer'); + return false; + } + + // Initialise l'InputManager + this._inputManager.initialize(this._canvas); + + console.log('Engine initialisé'); + return true; + } + + /** + * Démarre le moteur + */ + start(): void { + if (this._isRunning) { + console.warn('Engine déjà en cours d\'exécution'); + return; + } + + this._isRunning = true; + this._isPaused = false; + this._gameLoop.start(); + console.log('Engine démarré'); + } + + /** + * Arrête le moteur + */ + stop(): void { + if (!this._isRunning) { + return; + } + + this._isRunning = false; + this._gameLoop.stop(); + console.log('Engine arrêté'); + } + + /** + * Met en pause + */ + pause(): void { + if (!this._isRunning || this._isPaused) { + return; + } + + this._isPaused = true; + console.log('Engine en pause'); + } + + /** + * Reprend + */ + resume(): void { + if (!this._isRunning || !this._isPaused) { + return; + } + + this._isPaused = false; + console.log('Engine repris'); + } + + /** + * Charge une scène + */ + loadScene(name: string): boolean { + return this._sceneManager.loadScene(name); + } + + /** + * Enregistre une scène + */ + registerScene(name: string, scene: Scene): void { + this._sceneManager.registerScene(name, scene); + } + + /** + * Récupère la scène active + */ + getActiveScene(): Scene | null { + return this._sceneManager.getActiveScene(); + } + + /** + * Redimensionne le viewport + */ + resize(width: number, height: number): void { + this._glContext.resize(width, height); + this._renderer.resize(width, height); + + // Met à jour la caméra de la scène active + const activeScene = this._sceneManager.getActiveScene(); + if (activeScene) { + activeScene.camera.resize(width, height); + } + } + + /** + * Canvas HTML + */ + get canvas(): HTMLCanvasElement { + return this._canvas; + } + + /** + * Renderer + */ + get renderer(): WebGLRenderer { + return this._renderer; + } + + /** + * SceneManager + */ + get sceneManager(): SceneManager { + return this._sceneManager; + } + + /** + * InputManager + */ + get input(): InputManager { + return this._inputManager; + } + + /** + * PhysicsSystem + */ + get physics(): PhysicsSystem { + return this._physicsSystem; + } + + /** + * AssetLoader + */ + get assets(): AssetLoader { + return this._assetLoader; + } + + /** + * DebugPanel + */ + get debug(): DebugPanel { + return this._debugPanel; + } + + /** + * GameLoop + */ + get gameLoop(): GameLoop { + return this._gameLoop; + } + + /** + * État du moteur + */ + get isRunning(): boolean { + return this._isRunning; + } + + /** + * État de pause + */ + get isPaused(): boolean { + return this._isPaused; + } + + /** + * Nettoie toutes les ressources + */ + dispose(): void { + this.stop(); + this._inputManager.dispose(); + this._assetLoader.dispose(); + this._sceneManager.dispose(); + this._renderer.dispose(); + this._debugPanel.dispose(); + // GLContext se nettoie automatiquement + } +} + diff --git a/src/engine/core/GameLoop.ts b/src/engine/core/GameLoop.ts new file mode 100644 index 0000000..d8e9aeb --- /dev/null +++ b/src/engine/core/GameLoop.ts @@ -0,0 +1,152 @@ +/** + * GameLoop - Boucle de jeu + * Cœur battant du moteur, appelle update() et render() en boucle + */ + +import { Time } from './Time'; + +export type UpdateCallback = (deltaTime: number, currentTime: number) => void; +export type RenderCallback = () => void; +export type FixedUpdateCallback = (fixedDeltaTime: number) => void; + +export class GameLoop { + private _isRunning: boolean = false; + private _lastTime: number = 0; + private _targetFPS: number = 60; + private _targetFrameTime: number = 1000 / 60; // 16.67ms pour 60 FPS + + // Fixed timestep pour la physique + private _fixedDeltaTime: number = 1 / 60; // 60 FPS fixe pour la physique + private _accumulator: number = 0; + + // Callbacks + private _updateCallback: UpdateCallback | null = null; + private _fixedUpdateCallback: FixedUpdateCallback | null = null; + private _renderCallback: RenderCallback | null = null; + + /** + * État de la boucle + */ + get isRunning(): boolean { + return this._isRunning; + } + + /** + * FPS cible (optionnel, pour limiter) + */ + get targetFPS(): number { + return this._targetFPS; + } + + set targetFPS(value: number) { + this._targetFPS = value; + this._targetFrameTime = 1000 / value; + } + + /** + * DeltaTime fixe pour la physique (en secondes) + */ + get fixedDeltaTime(): number { + return this._fixedDeltaTime; + } + + set fixedDeltaTime(value: number) { + this._fixedDeltaTime = value; + } + + /** + * Définit le callback pour la logique de jeu (update) + */ + onUpdate(callback: UpdateCallback): void { + this._updateCallback = callback; + } + + /** + * Définit le callback pour la physique (fixedUpdate) + */ + onFixedUpdate(callback: FixedUpdateCallback): void { + this._fixedUpdateCallback = callback; + } + + /** + * Définit le callback pour le rendu + */ + onRender(callback: RenderCallback): void { + this._renderCallback = callback; + } + + /** + * Démarre la boucle de jeu + */ + start(): void { + if (this._isRunning) { + console.warn('GameLoop déjà en cours d\'exécution'); + return; + } + + this._isRunning = true; + this._lastTime = performance.now(); + Time.reset(); + + this._loop(); + } + + /** + * Arrête la boucle de jeu + */ + stop(): void { + this._isRunning = false; + console.log('GameLoop arrêté'); + } + + /** + * Boucle principale + */ + private _loop = (): void => { + if (!this._isRunning) { + return; + } + + const currentTime = performance.now(); + const deltaTimeMs = currentTime - this._lastTime; + const deltaTimeSeconds = deltaTimeMs / 1000; + + // Limite les FPS si nécessaire (capping) + if (deltaTimeMs < this._targetFrameTime) { + requestAnimationFrame(this._loop); + return; + } + + // Met à jour Time + Time.update(deltaTimeSeconds); + + // Fixed timestep pour la physique + this._accumulator += deltaTimeSeconds; + + // Exécute plusieurs fixedUpdates si nécessaire + const maxFixedSteps = 5; // Limite pour éviter le spiral of death + let fixedSteps = 0; + + while (this._accumulator >= this._fixedDeltaTime && fixedSteps < maxFixedSteps) { + if (this._fixedUpdateCallback) { + this._fixedUpdateCallback(this._fixedDeltaTime); + } + this._accumulator -= this._fixedDeltaTime; + fixedSteps++; + } + + // Update normal (logique de jeu) + if (this._updateCallback) { + this._updateCallback(Time.deltaTime, currentTime); + } + + // Render + if (this._renderCallback) { + this._renderCallback(); + } + + this._lastTime = currentTime; + requestAnimationFrame(this._loop); + }; +} + diff --git a/src/engine/core/Scene.ts b/src/engine/core/Scene.ts new file mode 100644 index 0000000..31afdd7 --- /dev/null +++ b/src/engine/core/Scene.ts @@ -0,0 +1,226 @@ +/** + * Scene - Scène de jeu + * Contient tous les GameObjects d'un niveau/écran + */ + +import { WebGLRenderer } from '../rendering/WebGLRenderer'; +import { Camera } from '../rendering/Camera'; +import { GameObject } from '../entities/GameObject'; +import { SpriteRenderer } from '../components/SpriteRenderer'; +import { UICanvas } from '../ui/UICanvas'; + +export class Scene { + private _name: string; + private _gameObjects: GameObject[] = []; + private _camera: Camera; + private _uiCanvas: UICanvas | null = null; + private _isLoaded: boolean = false; + + constructor(name: string, viewportWidth: number = 800, viewportHeight: number = 600) { + this._name = name; + this._camera = new Camera(viewportWidth, viewportHeight); + } + + /** + * Nom de la scène + */ + get name(): string { + return this._name; + } + + /** + * Caméra de la scène + */ + get camera(): Camera { + return this._camera; + } + + /** + * UICanvas de la scène (interface utilisateur) + */ + get uiCanvas(): UICanvas | null { + return this._uiCanvas; + } + + /** + * Crée ou récupère le UICanvas + */ + createUICanvas(width?: number, height?: number): UICanvas { + if (!this._uiCanvas) { + const w = width || this._camera.viewportWidth; + const h = height || this._camera.viewportHeight; + this._uiCanvas = new UICanvas(w, h); + } + return this._uiCanvas; + } + + /** + * État de chargement + */ + get isLoaded(): boolean { + return this._isLoaded; + } + + /** + * Appelé au chargement de la scène + */ + onLoad(): void { + this._isLoaded = true; + } + + /** + * Appelé au déchargement de la scène + */ + onUnload(): void { + // Nettoie tous les GameObjects + for (const obj of this._gameObjects) { + obj.destroy(); + } + this._gameObjects.length = 0; + + // Nettoie le UICanvas + this._uiCanvas = null; + + this._isLoaded = false; + } + + /** + * Update tous les objets de la scène + */ + update(deltaTime: number): void { + // Update tous les GameObjects actifs (seulement ceux sans parent - les enfants sont mis à jour par le parent) + for (const obj of this._gameObjects) { + if (!obj.parent && obj.active && !obj.isDestroyed) { + obj.update(deltaTime); + } + } + + // Nettoie les objets détruits + this._gameObjects = this._gameObjects.filter(obj => !obj.isDestroyed); + + // Update le UICanvas si présent + if (this._uiCanvas) { + this._uiCanvas.update(deltaTime); + } + } + + /** + * Récupère tous les SpriteRenderer de la scène (trie par layer) + */ + getAllRenderables(): SpriteRenderer[] { + const renderables: SpriteRenderer[] = []; + + // Parcourt tous les GameObjects (récursif pour inclure les enfants) + this._collectRenderablesRecursive(this._gameObjects, renderables); + + // Trie par layer (z-index) + renderables.sort((a, b) => { + // Si même layer, on peut trier par nom pour stabilité + if (a.layer !== b.layer) { + return a.layer - b.layer; + } + return a.gameObject.name.localeCompare(b.gameObject.name); + }); + + return renderables; + } + + /** + * Collecte récursivement tous les SpriteRenderer d'une liste de GameObjects + */ + private _collectRenderablesRecursive(gameObjects: GameObject[], renderables: SpriteRenderer[]): void { + for (const obj of gameObjects) { + if (!obj.active || obj.isDestroyed) { + continue; + } + + // Ajoute les SpriteRenderer de cet objet + const spriteRenderer = obj.getComponent(SpriteRenderer); + if (spriteRenderer && spriteRenderer.enabled) { + renderables.push(spriteRenderer); + } + + // Parcourt les enfants récursivement + if (obj.children.length > 0) { + this._collectRenderablesRecursive(Array.from(obj.children), renderables); + } + } + } + + /** + * Rend la scène (déprécié - maintenant géré par WebGLRenderer.render()) + * @deprecated Cette méthode est appelée par WebGLRenderer qui gère maintenant le rendu complet + */ + render(_renderer: WebGLRenderer): void { + // Cette méthode est maintenant vide car le rendu est géré directement + // par WebGLRenderer.render() qui appelle getAllRenderables() + } + + /** + * Ajoute un GameObject à la scène + */ + addGameObject(gameObject: GameObject): void { + if (this._gameObjects.indexOf(gameObject) !== -1) { + console.warn(`GameObject "${gameObject.name}" est déjà dans la scène`); + return; + } + this._gameObjects.push(gameObject); + gameObject._setScene(this); + } + + /** + * Retire un GameObject de la scène + */ + removeGameObject(gameObject: GameObject): void { + const index = this._gameObjects.indexOf(gameObject); + if (index !== -1) { + this._gameObjects.splice(index, 1); + gameObject._setScene(null); + } + } + + /** + * Recherche un GameObject par nom + */ + findGameObjectByName(name: string): GameObject | null { + for (const obj of this._gameObjects) { + if (obj.name === name) { + return obj; + } + // Recherche récursive dans les enfants + const found = obj.findChild(name); + if (found) { + return found; + } + } + return null; + } + + /** + * Récupère tous les GameObjects avec un tag spécifique + */ + findGameObjectsByTag(tag: string): GameObject[] { + const result: GameObject[] = []; + for (const obj of this._gameObjects) { + if (obj.tag === tag) { + result.push(obj); + } + } + return result; + } + + /** + * Liste de tous les GameObjects de la scène + */ + /** + * Trouve un GameObject par son nom + */ + findGameObject(name: string): GameObject | null { + return this._gameObjects.find(go => go.name === name) || null; + } + + get gameObjects(): readonly GameObject[] { + return this._gameObjects; + } +} + diff --git a/src/engine/core/SceneManager.ts b/src/engine/core/SceneManager.ts new file mode 100644 index 0000000..4dce4ce --- /dev/null +++ b/src/engine/core/SceneManager.ts @@ -0,0 +1,137 @@ +/** + * SceneManager - Gestion des scènes + * Gère les différents "écrans" du jeu (menu, niveaux, game over...) + */ + +import { Scene } from './Scene'; +import { WebGLRenderer } from '../rendering/WebGLRenderer'; + +export class SceneManager { + private _scenes: Map = new Map(); + private _activeScene: Scene | null = null; + private _isTransitioning: boolean = false; + + /** + * Enregistre une scène + */ + registerScene(name: string, scene: Scene): void { + if (this._scenes.has(name)) { + console.warn(`Scène "${name}" déjà enregistrée, remplacement`); + } + this._scenes.set(name, scene); + } + + /** + * Charge une scène (décharge l'ancienne et charge la nouvelle) + */ + loadScene(name: string): boolean { + if (this._isTransitioning) { + console.warn('Transition en cours, impossible de charger une nouvelle scène'); + return false; + } + + const scene = this._scenes.get(name); + if (!scene) { + console.error(`Scène "${name}" introuvable`); + return false; + } + + // Décharge la scène active + if (this._activeScene && this._activeScene.isLoaded) { + this._activeScene.onUnload(); + } + + // Charge la nouvelle scène + this._activeScene = scene; + this._activeScene.onLoad(); + + console.log(`Scène "${name}" chargée`); + return true; + } + + /** + * Décharge une scène + */ + unloadScene(name: string): boolean { + const scene = this._scenes.get(name); + if (!scene) { + console.error(`Scène "${name}" introuvable`); + return false; + } + + if (scene.isLoaded) { + scene.onUnload(); + } + + // Si c'est la scène active, désactive-la + if (this._activeScene === scene) { + this._activeScene = null; + } + + return true; + } + + /** + * Récupère la scène active + */ + getActiveScene(): Scene | null { + return this._activeScene; + } + + /** + * Récupère une scène par son nom + */ + getScene(name: string): Scene | null { + return this._scenes.get(name) || null; + } + + /** + * Update la scène active + */ + update(deltaTime: number): void { + if (this._activeScene && this._activeScene.isLoaded) { + this._activeScene.update(deltaTime); + } + } + + /** + * Rend la scène active + */ + render(renderer: WebGLRenderer): void { + if (this._activeScene && this._activeScene.isLoaded) { + // Appelle directement WebGLRenderer.render() avec la scène et sa caméra + renderer.render(this._activeScene, this._activeScene.camera); + } + } + + /** + * État de transition + */ + get isTransitioning(): boolean { + return this._isTransitioning; + } + + /** + * Définit l'état de transition + */ + set isTransitioning(value: boolean) { + this._isTransitioning = value; + } + + /** + * Nettoie toutes les scènes + */ + dispose(): void { + // Décharge toutes les scènes + for (const scene of this._scenes.values()) { + if (scene.isLoaded) { + scene.onUnload(); + } + } + + this._scenes.clear(); + this._activeScene = null; + this._isTransitioning = false; + } +} + diff --git a/src/engine/core/Time.ts b/src/engine/core/Time.ts new file mode 100644 index 0000000..9e2464c --- /dev/null +++ b/src/engine/core/Time.ts @@ -0,0 +1,103 @@ +/** + * Time - Gestion du temps + * Centralise toutes les informations temporelles du jeu + */ + +export class Time { + private static _deltaTime: number = 0; + private static _unscaledDeltaTime: number = 0; + private static _timeScale: number = 1.0; + private static _time: number = 0; + private static _frameCount: number = 0; + private static _fps: number = 0; + + // Pour calculer les FPS + private static _fpsUpdateInterval: number = 0.5; // Mise à jour FPS toutes les 0.5 secondes + private static _fpsAccumulator: number = 0; + private static _fpsFrameCount: number = 0; + + /** + * Temps écoulé depuis la dernière frame (en secondes) + * Affecté par timeScale + */ + static get deltaTime(): number { + return this._deltaTime; + } + + /** + * Temps écoulé depuis la dernière frame (en secondes) + * NON affecté par timeScale + */ + static get unscaledDeltaTime(): number { + return this._unscaledDeltaTime; + } + + /** + * Multiplicateur de temps + * 1 = normal, 0.5 = ralenti (2× plus lent), 2 = rapide (2× plus rapide) + */ + static get timeScale(): number { + return this._timeScale; + } + + static set timeScale(value: number) { + this._timeScale = Math.max(0, value); // Pas de valeur négative + } + + /** + * Temps total depuis le démarrage (en secondes) + */ + static get time(): number { + return this._time; + } + + /** + * Nombre de frames rendues depuis le démarrage + */ + static get frameCount(): number { + return this._frameCount; + } + + /** + * FPS moyen calculé + */ + static get fps(): number { + return this._fps; + } + + /** + * Met à jour Time (appelé par GameLoop chaque frame) + * @param deltaTimeSecondes Temps écoulé en secondes depuis la dernière frame + */ + static update(deltaTimeSecondes: number): void { + this._unscaledDeltaTime = deltaTimeSecondes; + this._deltaTime = deltaTimeSecondes * this._timeScale; + + this._time += this._deltaTime; + this._frameCount++; + + // Calcul des FPS + this._fpsAccumulator += deltaTimeSecondes; + this._fpsFrameCount++; + + if (this._fpsAccumulator >= this._fpsUpdateInterval) { + this._fps = this._fpsFrameCount / this._fpsAccumulator; + this._fpsAccumulator = 0; + this._fpsFrameCount = 0; + } + } + + /** + * Réinitialise Time (utile pour les restart de jeu) + */ + static reset(): void { + this._deltaTime = 0; + this._unscaledDeltaTime = 0; + this._time = 0; + this._frameCount = 0; + this._fps = 0; + this._fpsAccumulator = 0; + this._fpsFrameCount = 0; + } +} + diff --git a/src/engine/debug/DebugPanel.ts b/src/engine/debug/DebugPanel.ts new file mode 100644 index 0000000..be4bfc3 --- /dev/null +++ b/src/engine/debug/DebugPanel.ts @@ -0,0 +1,191 @@ +/** + * DebugPanel - Panel HTML pour afficher les informations de debug + */ + +export interface DebugInfo { + fps?: number; + deltaTime?: number; + playerPosition?: { x: number; y: number }; + playerVelocity?: { x: number; y: number }; + mouseScreenPos?: { x: number; y: number }; + mouseWorldPos?: { x: number; y: number }; + playerDirection?: string; + playerState?: string; + entityCount?: number; + activeColliders?: number; + cameraPosition?: { x: number; y: number }; + [key: string]: any; // Permet d'ajouter des propriétés dynamiques +} + +export class DebugPanel { + private panel: HTMLDivElement; + private isVisible: boolean = true; + private refreshRate: number = 100; // ms entre chaque update + private lastUpdate: number = 0; + private debugInfo: DebugInfo = {}; + + constructor() { + this.panel = this.createPanel(); + document.body.appendChild(this.panel); + + // Toggle avec F3 + window.addEventListener('keydown', (e) => { + if (e.key === 'F3') { + e.preventDefault(); + this.toggle(); + } + }); + } + + private createPanel(): HTMLDivElement { + const panel = document.createElement('div'); + panel.id = 'debug-panel'; + panel.style.cssText = ` + position: fixed; + bottom: 10px; + left: 10px; + background: rgba(0, 0, 0, 0.85); + color: #00ff00; + font-family: 'Courier New', monospace; + font-size: 12px; + padding: 15px; + border-radius: 8px; + border: 2px solid #00ff00; + z-index: 10000; + min-width: 300px; + max-width: 400px; + box-shadow: 0 4px 20px rgba(0, 255, 0, 0.3); + line-height: 1.6; + user-select: none; + `; + + panel.innerHTML = ` +
+ 🎮 DEBUG PANEL [F3] +
+
+ `; + + return panel; + } + + public update(currentTime: number): void { + if (!this.isVisible) return; + + // Limite le taux de rafraîchissement + if (currentTime - this.lastUpdate < this.refreshRate) { + return; + } + + this.lastUpdate = currentTime; + this.render(); + } + + private render(): void { + const content = this.panel.querySelector('#debug-content'); + if (!content) return; + + let html = ''; + + // Performance + if (this.debugInfo.fps !== undefined || this.debugInfo.deltaTime !== undefined) { + html += this.section('Performance', [ + this.debugInfo.fps !== undefined ? `FPS: ${this.debugInfo.fps.toFixed(0)}` : null, + this.debugInfo.deltaTime !== undefined ? `Delta: ${(this.debugInfo.deltaTime * 1000).toFixed(2)}ms` : null, + ]); + } + + // Joueur + if (this.debugInfo.playerPosition || this.debugInfo.playerVelocity || this.debugInfo.playerDirection) { + html += this.section('Player', [ + this.debugInfo.playerPosition ? `Pos: (${this.debugInfo.playerPosition.x.toFixed(1)}, ${this.debugInfo.playerPosition.y.toFixed(1)})` : null, + this.debugInfo.playerVelocity ? `Vel: (${this.debugInfo.playerVelocity.x.toFixed(1)}, ${this.debugInfo.playerVelocity.y.toFixed(1)})` : null, + this.debugInfo.playerDirection ? `Dir: ${this.debugInfo.playerDirection}` : null, + this.debugInfo.playerState ? `State: ${this.debugInfo.playerState}` : null, + ]); + } + + // Input + if (this.debugInfo.mouseScreenPos || this.debugInfo.mouseWorldPos) { + html += this.section('Input', [ + this.debugInfo.mouseScreenPos ? `Mouse Screen: (${this.debugInfo.mouseScreenPos.x.toFixed(0)}, ${this.debugInfo.mouseScreenPos.y.toFixed(0)})` : null, + this.debugInfo.mouseWorldPos ? `Mouse World: (${this.debugInfo.mouseWorldPos.x.toFixed(1)}, ${this.debugInfo.mouseWorldPos.y.toFixed(1)})` : null, + ]); + } + + // Caméra + if (this.debugInfo.cameraPosition) { + html += this.section('Camera', [ + `Pos: (${this.debugInfo.cameraPosition.x.toFixed(1)}, ${this.debugInfo.cameraPosition.y.toFixed(1)})`, + ]); + } + + // Scène + if (this.debugInfo.entityCount !== undefined || this.debugInfo.activeColliders !== undefined) { + html += this.section('Scene', [ + this.debugInfo.entityCount !== undefined ? `Entities: ${this.debugInfo.entityCount}` : null, + this.debugInfo.activeColliders !== undefined ? `Colliders: ${this.debugInfo.activeColliders}` : null, + ]); + } + + // Données custom + const customKeys = Object.keys(this.debugInfo).filter(key => + !['fps', 'deltaTime', 'playerPosition', 'playerVelocity', 'mouseScreenPos', + 'mouseWorldPos', 'playerDirection', 'playerState', 'entityCount', + 'activeColliders', 'cameraPosition'].includes(key) + ); + + if (customKeys.length > 0) { + const customData = customKeys.map(key => { + const value = this.debugInfo[key]; + if (typeof value === 'object') { + return `${key}: ${JSON.stringify(value)}`; + } + return `${key}: ${value}`; + }); + html += this.section('Custom', customData); + } + + content.innerHTML = html; + } + + private section(title: string, items: (string | null)[]): string { + const validItems = items.filter(item => item !== null); + if (validItems.length === 0) return ''; + + return ` +
+
+ ${title}: +
+
+ ${validItems.map(item => `
${item}
`).join('')} +
+
+ `; + } + + public setInfo(info: Partial): void { + this.debugInfo = { ...this.debugInfo, ...info }; + } + + public toggle(): void { + this.isVisible = !this.isVisible; + this.panel.style.display = this.isVisible ? 'block' : 'none'; + } + + public show(): void { + this.isVisible = true; + this.panel.style.display = 'block'; + } + + public hide(): void { + this.isVisible = false; + this.panel.style.display = 'none'; + } + + public dispose(): void { + this.panel.remove(); + } +} + diff --git a/src/engine/entities/Component.ts b/src/engine/entities/Component.ts new file mode 100644 index 0000000..f7ad1f6 --- /dev/null +++ b/src/engine/entities/Component.ts @@ -0,0 +1,120 @@ +/** + * Component - Classe abstraite de base pour tous les composants + * Philosophie ECS : Composition > Héritage + */ + +import { GameObject } from './GameObject'; +import { Transform } from './Transform'; + +export abstract class Component { + private _gameObject: GameObject | null = null; + private _enabled: boolean = true; + private _hasStarted: boolean = false; + + /** + * GameObject parent (assigné automatiquement lors de l'ajout) + */ + get gameObject(): GameObject { + if (!this._gameObject) { + throw new Error('Component n\'est pas attaché à un GameObject'); + } + return this._gameObject; + } + + /** + * Transform du GameObject parent (raccourci pratique) + */ + get transform(): Transform { + return this.gameObject.transform; + } + + /** + * État actif/inactif du composant + */ + get enabled(): boolean { + return this._enabled; + } + + set enabled(value: boolean) { + this._enabled = value; + } + + /** + * Indique si start() a été appelé + */ + get hasStarted(): boolean { + return this._hasStarted; + } + + /** + * Assigne ce composant à un GameObject (appelé par GameObject.addComponent) + * @internal + */ + _setGameObject(gameObject: GameObject): void { + if (this._gameObject && this._gameObject !== gameObject) { + throw new Error('Component déjà attaché à un autre GameObject'); + } + this._gameObject = gameObject; + } + + /** + * Marque que start() a été appelé (appelé par GameObject) + * @internal + */ + _markStarted(): void { + this._hasStarted = true; + } + + /** + * Récupère un composant du même GameObject par type + * Raccourci pratique pour gameObject.getComponent() + */ + getComponent(type: new (...args: any[]) => T): T | null { + return this.gameObject.getComponent(type); + } + + /** + * Cycle de vie : Appelé à l'ajout du composant (avant start) + * Override pour initialiser le composant + */ + awake(): void { + // À override par les composants enfants + } + + /** + * Cycle de vie : Appelé avant la première update + * Tous les composants sont créés, safe pour référencer d'autres composants + * Override pour initialiser après que tous les composants soient créés + */ + start(): void { + // À override par les composants enfants + } + + /** + * Cycle de vie : Appelé chaque frame + * @param deltaTime Temps écoulé depuis la dernière frame (en secondes) + */ + update(_deltaTime: number): void { + // À override par les composants enfants + } + + /** + * Cycle de vie : Appelé à la destruction du composant + * Override pour nettoyer les ressources + */ + onDestroy(): void { + // À override par les composants enfants + } + + /** + * Nettoie le composant (appelé par GameObject) + * @internal + */ + _destroy(): void { + this.onDestroy(); + this._gameObject = null; + this._enabled = false; + this._hasStarted = false; + } +} + diff --git a/src/engine/entities/GameObject.ts b/src/engine/entities/GameObject.ts new file mode 100644 index 0000000..7e1da2a --- /dev/null +++ b/src/engine/entities/GameObject.ts @@ -0,0 +1,406 @@ +/** + * GameObject - Entité de base du jeu + * Tout ce qui existe dans le jeu est un GameObject + * Contient un Transform et des Components + */ + +import { Transform } from './Transform'; +import { Component } from './Component'; +import { Scene } from '../core/Scene'; + +export class GameObject { + private _name: string; + private _active: boolean = true; + private _tag: string = 'Untagged'; + private _layer: number = 0; + + private _transform: Transform; + private _components: Map = new Map(); + + private _parent: GameObject | null = null; + private _children: GameObject[] = []; + + private _scene: Scene | null = null; + + // Cycle de vie + private _hasAwakened: boolean = false; + private _hasStarted: boolean = false; + private _isDestroyed: boolean = false; + private _destroyNextFrame: boolean = false; + + constructor(name: string = 'GameObject') { + this._name = name; + this._transform = new Transform(); + } + + /** + * Nom de l'objet (pour debug/recherche) + */ + get name(): string { + return this._name; + } + + set name(value: string) { + this._name = value; + } + + /** + * État actif/inactif + * Si false, update() n'est pas appelé + */ + get active(): boolean { + return this._active; + } + + set active(value: boolean) { + if (this._active === value) { + return; + } + + this._active = value; + + // Propage aux enfants + for (const child of this._children) { + child.active = value; + } + } + + /** + * Tag pour catégorisation (ex: "Player", "Enemy", "Coin") + */ + get tag(): string { + return this._tag; + } + + set tag(value: string) { + this._tag = value; + } + + /** + * Layer de rendu (z-index pour le tri) + */ + get layer(): number { + return this._layer; + } + + set layer(value: number) { + this._layer = value; + } + + /** + * Transform de l'objet (toujours présent) + */ + get transform(): Transform { + return this._transform; + } + + /** + * Scène à laquelle appartient cet objet + */ + get scene(): Scene | null { + return this._scene; + } + + /** + * Parent dans la hiérarchie + */ + get parent(): GameObject | null { + return this._parent; + } + + /** + * Liste des enfants + */ + get children(): readonly GameObject[] { + return this._children; + } + + /** + * Indique si l'objet a été détruit + */ + get isDestroyed(): boolean { + return this._isDestroyed; + } + + /** + * Assigne la scène (appelé par Scene.addGameObject) + * @internal + */ + _setScene(scene: Scene | null): void { + this._scene = scene; + } + + /** + * Déclenche awake() si ce n'est pas déjà fait + */ + private _awake(): void { + if (this._hasAwakened || !this._active) { + return; + } + + this._hasAwakened = true; + + // Appelle awake() sur tous les composants + for (const component of this._components.values()) { + if (component.enabled) { + component.awake(); + } + } + + // Appelle awake() sur tous les enfants + for (const child of this._children) { + child._awake(); + } + } + + /** + * Déclenche start() si ce n'est pas déjà fait + */ + private _start(): void { + if (this._hasStarted || !this._active || !this._hasAwakened) { + return; + } + + this._hasStarted = true; + + // Appelle start() sur tous les composants + for (const component of this._components.values()) { + if (component.enabled && !component.hasStarted) { + component.start(); + component._markStarted(); + } + } + + // Appelle start() sur tous les enfants + for (const child of this._children) { + child._start(); + } + } + + /** + * Update tous les composants actifs + */ + update(deltaTime: number): void { + // S'assure que awake() et start() ont été appelés + if (!this._hasAwakened) { + this._awake(); + } + if (!this._hasStarted && this._hasAwakened) { + this._start(); + } + + if (!this._active || this._isDestroyed) { + return; + } + + // Update tous les composants actifs + for (const component of this._components.values()) { + if (component.enabled) { + component.update(deltaTime); + } + } + + // Update tous les enfants + for (const child of this._children) { + child.update(deltaTime); + } + + // Gère la destruction différée + if (this._destroyNextFrame) { + this._destroy(); + } + } + + /** + * Ajoute un composant à l'objet + * @returns Le composant ajouté (pour chaînage) + */ + addComponent(component: T): T { + const componentName = component.constructor.name; + + if (this._components.has(componentName)) { + console.warn(`GameObject "${this._name}" a déjà un composant de type "${componentName}"`); + } + + component._setGameObject(this); + this._components.set(componentName, component); + + // Si l'objet est déjà initialisé, appelle awake() immédiatement + if (this._hasAwakened) { + if (component.enabled) { + component.awake(); + } + // start() sera appelé au prochain update + } + + return component; + } + + /** + * Récupère un composant par type + * @returns Le composant trouvé ou null + */ + getComponent(type: new (...args: any[]) => T): T | null { + const componentName = type.name; + const component = this._components.get(componentName); + return component instanceof type ? component : null; + } + + /** + * Récupère tous les composants + */ + getComponents(): Component[] { + return Array.from(this._components.values()); + } + + /** + * Retire un composant + */ + removeComponent(type: new (...args: any[]) => T): void { + const componentName = type.name; + const component = this._components.get(componentName); + + if (component) { + component._destroy(); + this._components.delete(componentName); + } + } + + /** + * Définit le parent dans la hiérarchie + */ + setParent(parent: GameObject | null): void { + if (this._parent === parent) { + return; + } + + // Retire de l'ancien parent + if (this._parent) { + const index = this._parent._children.indexOf(this); + if (index !== -1) { + this._parent._children.splice(index, 1); + } + } + + // Ajoute au nouveau parent + this._parent = parent; + if (parent) { + parent._children.push(this); + // Met à jour le transform parent + this._transform.setParent(parent.transform); + // Hérite de la scène + this._setScene(parent.scene); + } else { + this._transform.setParent(null); + } + } + + /** + * Ajoute un enfant + */ + addChild(child: GameObject): void { + child.setParent(this); + } + + /** + * Retire un enfant + */ + removeChild(child: GameObject): void { + if (child.parent === this) { + child.setParent(null); + } + } + + /** + * Recherche un enfant par nom + */ + findChild(name: string): GameObject | null { + for (const child of this._children) { + if (child.name === name) { + return child; + } + // Recherche récursive + const found = child.findChild(name); + if (found) { + return found; + } + } + return null; + } + + /** + * Recherche un composant dans cet objet ou ses enfants + */ + getComponentInChildren(type: new (...args: any[]) => T): T | null { + // Cherche dans cet objet + const component = this.getComponent(type); + if (component) { + return component; + } + + // Cherche dans les enfants + for (const child of this._children) { + const childComponent = child.getComponentInChildren(type); + if (childComponent) { + return childComponent; + } + } + + return null; + } + + /** + * Marque l'objet pour destruction au prochain frame + * (destruction différée pour éviter de détruire pendant update) + */ + destroy(): void { + if (this._isDestroyed || this._destroyNextFrame) { + return; + } + this._destroyNextFrame = true; + } + + /** + * Détruit l'objet immédiatement (appelé à la fin du frame) + * @internal + */ + private _destroy(): void { + if (this._isDestroyed) { + return; + } + + this._isDestroyed = true; + this._active = false; + + // Détruit tous les composants + for (const component of this._components.values()) { + component._destroy(); + } + this._components.clear(); + + // Détruit tous les enfants + for (const child of this._children) { + child._destroy(); + } + this._children.length = 0; + + // Retire du parent + this.setParent(null); + + // Retire de la scène + if (this._scene) { + this._scene.removeGameObject(this); + } + + // Nettoie le transform + this._transform.destroy(); + } + + /** + * Crée un nouveau GameObject avec un nom + */ + static create(name: string = 'GameObject'): GameObject { + return new GameObject(name); + } +} + diff --git a/src/engine/entities/Transform.ts b/src/engine/entities/Transform.ts new file mode 100644 index 0000000..287670f --- /dev/null +++ b/src/engine/entities/Transform.ts @@ -0,0 +1,340 @@ +/** + * Transform - Position, rotation, scale avec hiérarchie parent-enfant + * Gère les transformations locales vs monde avec dirty flags pour optimisation + */ + +import { Vector2 } from '../math/Vector2'; +import { Matrix3 } from '../math/Matrix3'; + +export class Transform { + private _localPosition: Vector2 = new Vector2(0, 0); + private _localRotation: number = 0; // en degrés + private _localScale: Vector2 = new Vector2(1, 1); + + private _parent: Transform | null = null; + private _children: Transform[] = []; + + // Dirty flags pour optimisation + private _dirty: boolean = true; + private _dirtyWorld: boolean = true; + private _cachedWorldMatrix: Matrix3 = Matrix3.identity(); + private _cachedWorldPosition: Vector2 = new Vector2(0, 0); + private _cachedWorldRotation: number = 0; + private _cachedWorldScale: Vector2 = new Vector2(1, 1); + + /** + * Position locale (relative au parent) + */ + get localPosition(): Vector2 { + return this._localPosition; + } + + set localPosition(value: Vector2) { + this._localPosition = value.clone(); + this._markDirty(); + } + + /** + * Rotation locale (relative au parent, en degrés) + */ + get localRotation(): number { + return this._localRotation; + } + + set localRotation(value: number) { + this._localRotation = value; + this._markDirty(); + } + + /** + * Scale local (relative au parent) + */ + get localScale(): Vector2 { + return this._localScale; + } + + set localScale(value: Vector2) { + this._localScale = value.clone(); + this._markDirty(); + } + + /** + * Position dans l'espace monde (calculée à partir de la hiérarchie) + */ + get position(): Vector2 { + this._updateWorldTransform(); + return this._cachedWorldPosition; + } + + set position(value: Vector2) { + if (this._parent) { + // Convertit la position monde en position locale + const parentInverse = this._parent.getWorldMatrix().inverse(); + const localPos = parentInverse.transformPoint(value); + this.localPosition = localPos; + } else { + this.localPosition = value; + } + } + + /** + * Rotation dans l'espace monde (en degrés) + */ + get rotation(): number { + this._updateWorldTransform(); + return this._cachedWorldRotation; + } + + set rotation(value: number) { + if (this._parent) { + // Rotation monde = rotation parent + rotation locale + const parentRotation = this._parent.rotation; + this.localRotation = value - parentRotation; + } else { + this.localRotation = value; + } + } + + /** + * Scale dans l'espace monde + */ + get scale(): Vector2 { + this._updateWorldTransform(); + return this._cachedWorldScale; + } + + set scale(value: Vector2) { + if (this._parent) { + const parentScale = this._parent.scale; + this.localScale = new Vector2( + value.x / parentScale.x, + value.y / parentScale.y + ); + } else { + this.localScale = value; + } + } + + /** + * Transform parent dans la hiérarchie + */ + get parent(): Transform | null { + return this._parent; + } + + /** + * Liste des transforms enfants + */ + get children(): readonly Transform[] { + return this._children; + } + + /** + * Définit le parent (retire automatiquement de l'ancien parent si nécessaire) + */ + setParent(parent: Transform | null): void { + if (this._parent === parent) { + return; + } + + // Retire de l'ancien parent + if (this._parent) { + const index = this._parent._children.indexOf(this); + if (index !== -1) { + this._parent._children.splice(index, 1); + } + } + + // Ajoute au nouveau parent + this._parent = parent; + if (parent) { + parent._children.push(this); + } + + this._markDirty(); + } + + /** + * Matrice de transformation monde (avec cache et dirty flags) + */ + getWorldMatrix(): Matrix3 { + this._updateWorldTransform(); + return this._cachedWorldMatrix; + } + + /** + * Met à jour les transformations monde si nécessaire + */ + private _updateWorldTransform(): void { + if (!this._dirty && !this._dirtyWorld) { + return; // Déjà à jour + } + + // Si le parent est aussi dirty, on doit attendre qu'il se mette à jour d'abord + if (this._parent && this._parent._dirtyWorld) { + this._parent._updateWorldTransform(); + } + + // Calcule la matrice monde + const localMatrix = this._calculateLocalMatrix(); + + if (this._parent) { + // Combine avec la matrice parent + const parentMatrix = this._parent.getWorldMatrix(); + this._cachedWorldMatrix = parentMatrix.clone().multiply(localMatrix); + + // Calcule position/rotation/scale monde à partir de la matrice + this._extractWorldFromMatrix(); + } else { + // Pas de parent = local = monde + this._cachedWorldMatrix = localMatrix; + this._cachedWorldPosition = this._localPosition.clone(); + this._cachedWorldRotation = this._localRotation; + this._cachedWorldScale = this._localScale.clone(); + } + + this._dirty = false; + this._dirtyWorld = false; + } + + /** + * Calcule la matrice locale (translation, rotation, scale) + */ + private _calculateLocalMatrix(): Matrix3 { + const matrix = Matrix3.identity(); + matrix.translate(this._localPosition.x, this._localPosition.y); + matrix.rotate(this._localRotation * Math.PI / 180); // Convertit degrés → radians + matrix.scale(this._localScale.x, this._localScale.y); + return matrix; + } + + /** + * Extrait position/rotation/scale monde à partir de la matrice monde + * (approximation pour rotation et scale - pour une extraction exacte, il faudrait décomposer SVD) + */ + private _extractWorldFromMatrix(): void { + const m = this._cachedWorldMatrix.toArray(); + + // Position (colonne de translation) + this._cachedWorldPosition = new Vector2(m[6]!, m[7]!); + + // Rotation et scale (décomposition approximative) + const a = m[0]!, b = m[1]!; + const c = m[3]!, d = m[4]!; + + // Scale (magnitude des vecteurs de base) + const scaleX = Math.sqrt(a * a + b * b); + const scaleY = Math.sqrt(c * c + d * d); + this._cachedWorldScale = new Vector2(scaleX, scaleY); + + // Rotation (angle du premier vecteur de base) + this._cachedWorldRotation = Math.atan2(b, a) * 180 / Math.PI; // Convertit radians → degrés + } + + /** + * Marque ce transform et ses enfants comme dirty + */ + private _markDirty(): void { + this._dirty = true; + this._dirtyWorld = true; + + // Propage aux enfants + for (const child of this._children) { + child._markDirtyWorld(); + } + } + + /** + * Marque comme dirty monde uniquement (sans recalculer local) + * @internal + */ + _markDirtyWorld(): void { + this._dirtyWorld = true; + for (const child of this._children) { + child._markDirtyWorld(); + } + } + + /** + * Déplace le transform (additionne à la position) + */ + translate(delta: Vector2): void { + this.localPosition = this._localPosition.add(delta); + } + + /** + * Tourne le transform (ajoute à la rotation) + * @param angle Angle en degrés + */ + rotate(angle: number): void { + this.localRotation = this._localRotation + angle; + } + + /** + * Fait regarder le transform vers une position + * @param target Position cible dans l'espace monde + */ + lookAt(target: Vector2): void { + const currentPos = this.position; + const direction = target.subtract(currentPos); + const angle = Math.atan2(direction.y, direction.x) * 180 / Math.PI; + this.rotation = angle; + } + + /** + * Transforme un point de l'espace local vers l'espace monde + */ + transformPoint(localPoint: Vector2): Vector2 { + return this.getWorldMatrix().transformPoint(localPoint); + } + + /** + * Transforme un point de l'espace monde vers l'espace local + */ + inverseTransformPoint(worldPoint: Vector2): Vector2 { + const inverseMatrix = this.getWorldMatrix().inverse(); + return inverseMatrix.transformPoint(worldPoint); + } + + /** + * Vecteur droit (direction +X après rotation) + */ + get right(): Vector2 { + const angle = this.rotation * Math.PI / 180; + return new Vector2(Math.cos(angle), Math.sin(angle)); + } + + /** + * Vecteur haut (direction +Y après rotation, WebGL: Y vers le haut = négatif) + */ + get up(): Vector2 { + const angle = (this.rotation + 90) * Math.PI / 180; + return new Vector2(Math.cos(angle), Math.sin(angle)); + } + + /** + * Direction avant (même que right pour 2D) + */ + get forward(): Vector2 { + return this.right; + } + + /** + * Réinitialise le transform (position 0, rotation 0, scale 1) + */ + reset(): void { + this._localPosition = new Vector2(0, 0); + this._localRotation = 0; + this._localScale = new Vector2(1, 1); + this._markDirty(); + } + + /** + * Nettoie le transform (retire de la hiérarchie) + */ + destroy(): void { + this.setParent(null); + // Les enfants sont détruits avec le GameObject parent + } +} + diff --git a/src/engine/input/InputManager.ts b/src/engine/input/InputManager.ts new file mode 100644 index 0000000..55d919e --- /dev/null +++ b/src/engine/input/InputManager.ts @@ -0,0 +1,255 @@ +/** + * InputManager - Gestion des entrées utilisateur + * Centralise toutes les entrées (clavier, souris, tactile) + */ + +import { Vector2 } from '../math/Vector2'; +import { Camera } from '../rendering/Camera'; + +/** + * États possibles pour une touche/bouton + */ +export enum KeyState { + Up, // Pas pressé + Down, // Pressé cette frame + Held, // Maintenu + Released // Relâché cette frame +} + +export enum MouseButton { + Left = 0, + Middle = 1, + Right = 2 +} + +export class InputManager { + private _keys: Map = new Map(); + private _mousePosition: Vector2 = new Vector2(0, 0); + private _mouseButtons: Map = new Map(); + private _wheelDelta: number = 0; + private _canvas: HTMLCanvasElement | null = null; + + /** + * Initialise l'InputManager avec un canvas + */ + initialize(canvas: HTMLCanvasElement): void { + this._canvas = canvas; + + // Écouteurs clavier + window.addEventListener('keydown', this._onKeyDown.bind(this)); + window.addEventListener('keyup', this._onKeyUp.bind(this)); + + // Écouteurs souris + canvas.addEventListener('mousedown', this._onMouseDown.bind(this)); + canvas.addEventListener('mouseup', this._onMouseUp.bind(this)); + canvas.addEventListener('mousemove', this._onMouseMove.bind(this)); + canvas.addEventListener('wheel', this._onWheel.bind(this)); + + // Empêche le menu contextuel sur clic droit + canvas.addEventListener('contextmenu', (e) => e.preventDefault()); + + // Empêche les raccourcis clavier (ex: F5, Ctrl+R) + window.addEventListener('keydown', (e) => { + // Autorise quelques touches utiles + if (e.key === 'F12' || (e.key === 'F5' && e.ctrlKey)) { + return; // Autorise F12 (dev tools) et Ctrl+F5 + } + // Empêche les autres raccourcis pendant le jeu + if (e.key.startsWith('F') || (e.ctrlKey && ['r', 'R'].includes(e.key))) { + e.preventDefault(); + } + }); + } + + /** + * Met à jour l'InputManager (doit être appelé chaque frame) + * Réinitialise les états Down et Released + */ + update(): void { + // Passe Down -> Held + for (const [key, state] of this._keys.entries()) { + if (state === KeyState.Down) { + this._keys.set(key, KeyState.Held); + } else if (state === KeyState.Released) { + this._keys.set(key, KeyState.Up); + } + } + + // Passe Down -> Held pour les boutons souris + for (const [button, state] of this._mouseButtons.entries()) { + if (state === KeyState.Down) { + this._mouseButtons.set(button, KeyState.Held); + } else if (state === KeyState.Released) { + this._mouseButtons.set(button, KeyState.Up); + } + } + + // Reset wheel delta chaque frame + this._wheelDelta = 0; + } + + /** + * Touche pressée cette frame (une seule fois par pression) + */ + isKeyDown(key: string): boolean { + return this._keys.get(key) === KeyState.Down; + } + + /** + * Touche maintenue (down ou held) + */ + isKeyHeld(key: string): boolean { + const state = this._keys.get(key); + return state === KeyState.Down || state === KeyState.Held; + } + + /** + * Touche relâchée cette frame + */ + isKeyUp(key: string): boolean { + return this._keys.get(key) === KeyState.Released; + } + + /** + * Bouton souris pressé cette frame + */ + isMouseButtonDown(button: number = MouseButton.Left): boolean { + return this._mouseButtons.get(button) === KeyState.Down; + } + + /** + * Bouton souris maintenu + */ + isMouseButtonHeld(button: number = MouseButton.Left): boolean { + const state = this._mouseButtons.get(button); + return state === KeyState.Down || state === KeyState.Held; + } + + /** + * Bouton souris relâché cette frame + */ + isMouseButtonUp(button: number = MouseButton.Left): boolean { + return this._mouseButtons.get(button) === KeyState.Released; + } + + /** + * Position de la souris en pixels (coordonnées écran) + */ + getMousePosition(): Vector2 { + return this._mousePosition.clone(); + } + + /** + * Position de la souris dans l'espace monde (convertit via caméra) + */ + getMouseWorldPosition(camera: Camera): Vector2 { + return camera.screenToWorld(this._mousePosition); + } + + /** + * Delta de la molette cette frame + * Positif = vers le haut, Négatif = vers le bas + */ + getWheelDelta(): number { + return this._wheelDelta; + } + + /** + * Normalise le nom de touche (standardise les noms) + */ + private _normalizeKey(key: string): string { + // Mappe certaines touches spéciales + const keyMap: Record = { + ' ': 'Space', + 'ArrowUp': 'ArrowUp', + 'ArrowDown': 'ArrowDown', + 'ArrowLeft': 'ArrowLeft', + 'ArrowRight': 'ArrowRight', + 'Escape': 'Escape', + 'Enter': 'Enter', + 'Tab': 'Tab', + 'Shift': 'Shift', + 'Control': 'Control', + 'Alt': 'Alt', + 'Meta': 'Meta' + }; + + return keyMap[key] || key; + } + + /** + * Handler pour keydown + */ + private _onKeyDown(event: KeyboardEvent): void { + const key = this._normalizeKey(event.key); + const currentState = this._keys.get(key); + + // Évite les répétitions de touches + if (currentState !== KeyState.Held && currentState !== KeyState.Down) { + this._keys.set(key, KeyState.Down); + } + } + + /** + * Handler pour keyup + */ + private _onKeyUp(event: KeyboardEvent): void { + const key = this._normalizeKey(event.key); + this._keys.set(key, KeyState.Released); + } + + /** + * Handler pour mousedown + */ + private _onMouseDown(event: MouseEvent): void { + const button = event.button; + const currentState = this._mouseButtons.get(button); + + if (currentState !== KeyState.Held && currentState !== KeyState.Down) { + this._mouseButtons.set(button, KeyState.Down); + } + + event.preventDefault(); + } + + /** + * Handler pour mouseup + */ + private _onMouseUp(event: MouseEvent): void { + const button = event.button; + this._mouseButtons.set(button, KeyState.Released); + event.preventDefault(); + } + + /** + * Handler pour mousemove + */ + private _onMouseMove(event: MouseEvent): void { + if (!this._canvas) { + return; + } + + const rect = this._canvas.getBoundingClientRect(); + this._mousePosition.x = event.clientX - rect.left; + this._mousePosition.y = event.clientY - rect.top; + } + + /** + * Handler pour wheel (molette) + */ + private _onWheel(event: WheelEvent): void { + this._wheelDelta = event.deltaY > 0 ? 1 : -1; + event.preventDefault(); + } + + /** + * Nettoie les ressources + */ + dispose(): void { + // Les event listeners seront automatiquement nettoyés quand la page se ferme + // Mais on peut les retirer manuellement si nécessaire + this._keys.clear(); + this._mouseButtons.clear(); + } +} + diff --git a/src/engine/math/Color.ts b/src/engine/math/Color.ts new file mode 100644 index 0000000..d73bbe8 --- /dev/null +++ b/src/engine/math/Color.ts @@ -0,0 +1,139 @@ +/** + * Color - Couleur RGBA + * Représente une couleur avec alpha (transparence) + */ + +export class Color { + public r: number; // Rouge (0-1) + public g: number; // Vert (0-1) + public b: number; // Bleu (0-1) + public a: number; // Alpha (0-1, 0 = transparent, 1 = opaque) + + constructor(r: number = 1, g: number = 1, b: number = 1, a: number = 1) { + this.r = Math.max(0, Math.min(1, r)); + this.g = Math.max(0, Math.min(1, g)); + this.b = Math.max(0, Math.min(1, b)); + this.a = Math.max(0, Math.min(1, a)); + } + + /** + * Crée une couleur depuis des valeurs 0-255 + */ + static fromRGB(r: number, g: number, b: number, a: number = 255): Color { + return new Color(r / 255, g / 255, b / 255, a / 255); + } + + /** + * Crée une couleur depuis une valeur hexadécimale (#RRGGBB ou #RRGGBBAA) + */ + static fromHex(hex: string): Color { + hex = hex.replace('#', ''); + + if (hex.length === 6) { + const r = parseInt(hex.substring(0, 2), 16) / 255; + const g = parseInt(hex.substring(2, 4), 16) / 255; + const b = parseInt(hex.substring(4, 6), 16) / 255; + return new Color(r, g, b, 1); + } else if (hex.length === 8) { + const r = parseInt(hex.substring(0, 2), 16) / 255; + const g = parseInt(hex.substring(2, 4), 16) / 255; + const b = parseInt(hex.substring(4, 6), 16) / 255; + const a = parseInt(hex.substring(6, 8), 16) / 255; + return new Color(r, g, b, a); + } + + throw new Error(`Format hex invalide: ${hex}`); + } + + /** + * Interpolation linéaire entre deux couleurs + */ + lerp(other: Color, t: number): Color { + const clampedT = Math.max(0, Math.min(1, t)); + return new Color( + this.r + (other.r - this.r) * clampedT, + this.g + (other.g - this.g) * clampedT, + this.b + (other.b - this.b) * clampedT, + this.a + (other.a - this.a) * clampedT + ); + } + + /** + * Multiplication de couleurs (utile pour les teintes) + */ + multiply(other: Color): Color { + return new Color( + this.r * other.r, + this.g * other.g, + this.b * other.b, + this.a * other.a + ); + } + + /** + * Convertit en format hexadécimal (#RRGGBBAA) + */ + toHex(): string { + const r = Math.round(this.r * 255).toString(16).padStart(2, '0'); + const g = Math.round(this.g * 255).toString(16).padStart(2, '0'); + const b = Math.round(this.b * 255).toString(16).padStart(2, '0'); + const a = Math.round(this.a * 255).toString(16).padStart(2, '0'); + return `#${r}${g}${b}${a}`; + } + + /** + * Convertit en format rgba(r, g, b, a) + */ + toRGBA(): string { + const r = Math.round(this.r * 255); + const g = Math.round(this.g * 255); + const b = Math.round(this.b * 255); + return `rgba(${r}, ${g}, ${b}, ${this.a})`; + } + + /** + * Retourne un tableau [r, g, b, a] + */ + toArray(): [number, number, number, number] { + return [this.r, this.g, this.b, this.a]; + } + + /** + * Copie cette couleur + */ + clone(): Color { + return new Color(this.r, this.g, this.b, this.a); + } + + /** + * Copie les valeurs d'une autre couleur + */ + copyFrom(other: Color): Color { + this.r = other.r; + this.g = other.g; + this.b = other.b; + this.a = other.a; + return this; + } + + /** + * Retourne une représentation string + */ + toString(): string { + return `Color(${this.r}, ${this.g}, ${this.b}, ${this.a})`; + } + + /** + * Constantes prédéfinies + */ + static readonly white = new Color(1, 1, 1, 1); + static readonly black = new Color(0, 0, 0, 1); + static readonly red = new Color(1, 0, 0, 1); + static readonly green = new Color(0, 1, 0, 1); + static readonly blue = new Color(0, 0, 1, 1); + static readonly yellow = new Color(1, 1, 0, 1); + static readonly cyan = new Color(0, 1, 1, 1); + static readonly magenta = new Color(1, 0, 1, 1); + static readonly transparent = new Color(0, 0, 0, 0); +} + diff --git a/src/engine/math/Matrix3.ts b/src/engine/math/Matrix3.ts new file mode 100644 index 0000000..029ebcd --- /dev/null +++ b/src/engine/math/Matrix3.ts @@ -0,0 +1,223 @@ +/** + * Matrix3 - Matrice 3×3 + * Représente une transformation 2D (translation, rotation, scale) + * + * Structure (colonne-major pour WebGL) : + * [a, c, tx] [m[0], m[3], m[6]] + * [b, d, ty] = [m[1], m[4], m[7]] + * [0, 0, 1 ] [m[2], m[5], m[8]] + * + * Stockage en ligne-major pour facilité : + * [a, b, 0, c, d, 0, tx, ty, 1] + */ + +import { Vector2 } from './Vector2'; + +export class Matrix3 { + private _m: Float32Array; + + constructor( + a: number = 1, c: number = 0, tx: number = 0, + b: number = 0, d: number = 1, ty: number = 0 + ) { + // Stockage ligne-major: [a, b, 0, c, d, 0, tx, ty, 1] + this._m = new Float32Array(9); + this.set(a, c, tx, b, d, ty); + } + + /** + * Définit les valeurs de la matrice + */ + set( + a: number, c: number, tx: number, + b: number, d: number, ty: number + ): Matrix3 { + this._m[0] = a; this._m[1] = b; this._m[2] = 0; + this._m[3] = c; this._m[4] = d; this._m[5] = 0; + this._m[6] = tx; this._m[7] = ty; this._m[8] = 1; + return this; + } + + /** + * Retourne la matrice identité + */ + identity(): Matrix3 { + return this.set(1, 0, 0, 0, 1, 0); + } + + /** + * Applique une translation + */ + translate(x: number, y: number): Matrix3 { + this._m[6]! += this._m[0]! * x + this._m[3]! * y; + this._m[7]! += this._m[1]! * x + this._m[4]! * y; + return this; + } + + /** + * Applique une rotation (angle en radians) + */ + rotate(angle: number): Matrix3 { + const cos = Math.cos(angle); + const sin = Math.sin(angle); + + const a = this._m[0]!; + const b = this._m[1]!; + const c = this._m[3]!; + const d = this._m[4]!; + + this._m[0] = a * cos + c * sin; + this._m[1] = b * cos + d * sin; + this._m[3] = c * cos - a * sin; + this._m[4] = d * cos - b * sin; + + return this; + } + + /** + * Applique un scale + */ + scale(x: number, y: number): Matrix3 { + this._m[0]! *= x; + this._m[1]! *= y; + this._m[3]! *= x; + this._m[4]! *= y; + return this; + } + + /** + * Multiplie par une autre matrice (this * other) + */ + multiply(other: Matrix3): Matrix3 { + const a1 = this._m[0]!, b1 = this._m[1]!, c1 = this._m[3]!, d1 = this._m[4]!, tx1 = this._m[6]!, ty1 = this._m[7]!; + const a2 = other._m[0]!, b2 = other._m[1]!, c2 = other._m[3]!, d2 = other._m[4]!, tx2 = other._m[6]!, ty2 = other._m[7]!; + + this._m[0] = a1 * a2 + c1 * b2; + this._m[1] = b1 * a2 + d1 * b2; + this._m[3] = a1 * c2 + c1 * d2; + this._m[4] = b1 * c2 + d1 * d2; + this._m[6] = a1 * tx2 + c1 * ty2 + tx1; + this._m[7] = b1 * tx2 + d1 * ty2 + ty1; + + return this; + } + + /** + * Applique la transformation à un point + */ + transformPoint(point: Vector2): Vector2 { + const x = point.x; + const y = point.y; + return new Vector2( + this._m[0]! * x + this._m[3]! * y + this._m[6]!, + this._m[1]! * x + this._m[4]! * y + this._m[7]! + ); + } + + /** + * Calcule la matrice inverse + */ + inverse(): Matrix3 { + const a = this._m[0]!; + const b = this._m[1]!; + const c = this._m[3]!; + const d = this._m[4]!; + const tx = this._m[6]!; + const ty = this._m[7]!; + + const det = a * d - b * c; + if (Math.abs(det) < 1e-10) { + console.warn('Matrice non inversible'); + return this.identity(); + } + + const invDet = 1 / det; + + return new Matrix3( + d * invDet, -c * invDet, (c * ty - d * tx) * invDet, + -b * invDet, a * invDet, (b * tx - a * ty) * invDet + ); + } + + /** + * Retourne un Float32Array pour WebGL (colonne-major) + */ + toArray(): Float32Array { + // Convertit ligne-major vers colonne-major pour WebGL + return new Float32Array([ + this._m[0]!, this._m[1]!, this._m[2]!, + this._m[3]!, this._m[4]!, this._m[5]!, + this._m[6]!, this._m[7]!, this._m[8]! + ]); + } + + /** + * Copie cette matrice + */ + clone(): Matrix3 { + return new Matrix3( + this._m[0], this._m[3], this._m[6], + this._m[1], this._m[4], this._m[7] + ); + } + + /** + * Copie les valeurs d'une autre matrice + */ + copyFrom(other: Matrix3): Matrix3 { + this._m.set(other._m); + return this; + } + + /** + * Retourne une représentation string + */ + toString(): string { + return `Matrix3(${this._m[0]}, ${this._m[3]}, ${this._m[6]}, ${this._m[1]}, ${this._m[4]}, ${this._m[7]})`; + } + + /** + * Matrice identité + */ + static identity(): Matrix3 { + return new Matrix3(1, 0, 0, 0, 1, 0); + } + + /** + * Matrice de translation + */ + static translation(x: number, y: number): Matrix3 { + return new Matrix3(1, 0, x, 0, 1, y); + } + + /** + * Matrice de rotation (angle en radians) + */ + static rotation(angle: number): Matrix3 { + const cos = Math.cos(angle); + const sin = Math.sin(angle); + return new Matrix3(cos, -sin, 0, sin, cos, 0); + } + + /** + * Matrice de scale + */ + static scaling(x: number, y: number): Matrix3 { + return new Matrix3(x, 0, 0, 0, y, 0); + } + + /** + * Matrice de projection orthographique + * Convertit coordonnées pixel en coordonnées NDC (-1 à 1) + */ + static orthographic(left: number, right: number, bottom: number, top: number): Matrix3 { + const width = right - left; + const height = top - bottom; + + return new Matrix3( + 2 / width, 0, -(right + left) / width, + 0, 2 / height, -(top + bottom) / height + ); + } +} + diff --git a/src/engine/math/Rect.ts b/src/engine/math/Rect.ts new file mode 100644 index 0000000..6b92fe3 --- /dev/null +++ b/src/engine/math/Rect.ts @@ -0,0 +1,178 @@ +/** + * Rect - Rectangle + * Représente un rectangle (pour bounds, collisions, UVs...) + */ + +import { Vector2 } from './Vector2'; + +export class Rect { + public x: number; + public y: number; + public width: number; + public height: number; + + constructor(x: number = 0, y: number = 0, width: number = 0, height: number = 0) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + /** + * Bord gauche + */ + get left(): number { + return this.x; + } + + /** + * Bord droit + */ + get right(): number { + return this.x + this.width; + } + + /** + * Bord haut (WebGL: Y vers le haut = plus petit) + */ + get top(): number { + return this.y; + } + + /** + * Bord bas + */ + get bottom(): number { + return this.y + this.height; + } + + /** + * Centre du rectangle + */ + get center(): Vector2 { + return new Vector2(this.x + this.width / 2, this.y + this.height / 2); + } + + /** + * Taille du rectangle (width, height) + */ + get size(): Vector2 { + return new Vector2(this.width, this.height); + } + + /** + * Teste si un point est à l'intérieur du rectangle + */ + contains(point: Vector2): boolean { + return point.x >= this.left && + point.x <= this.right && + point.y >= this.top && + point.y <= this.bottom; + } + + /** + * Teste si deux rectangles se chevauchent + */ + overlaps(other: Rect): boolean { + return this.left < other.right && + this.right > other.left && + this.top < other.bottom && + this.bottom > other.top; + } + + /** + * Calcule l'intersection de deux rectangles + * Retourne null si pas d'intersection + */ + intersection(other: Rect): Rect | null { + const left = Math.max(this.left, other.left); + const right = Math.min(this.right, other.right); + const top = Math.max(this.top, other.top); + const bottom = Math.min(this.bottom, other.bottom); + + if (left >= right || top >= bottom) { + return null; // Pas d'intersection + } + + return new Rect(left, top, right - left, bottom - top); + } + + /** + * Calcule l'union (plus petit rectangle contenant les deux) + */ + union(other: Rect): Rect { + const left = Math.min(this.left, other.left); + const right = Math.max(this.right, other.right); + const top = Math.min(this.top, other.top); + const bottom = Math.max(this.bottom, other.bottom); + + return new Rect(left, top, right - left, bottom - top); + } + + /** + * Agrandit le rectangle de chaque côté par `amount` + */ + expand(amount: number): Rect { + return new Rect( + this.x - amount, + this.y - amount, + this.width + amount * 2, + this.height + amount * 2 + ); + } + + /** + * Réduit le rectangle de chaque côté par `amount` + */ + shrink(amount: number): Rect { + const doubleAmount = amount * 2; + return new Rect( + this.x + amount, + this.y + amount, + Math.max(0, this.width - doubleAmount), + Math.max(0, this.height - doubleAmount) + ); + } + + /** + * Copie ce rectangle + */ + clone(): Rect { + return new Rect(this.x, this.y, this.width, this.height); + } + + /** + * Copie les valeurs d'un autre rectangle + */ + copyFrom(other: Rect): Rect { + this.x = other.x; + this.y = other.y; + this.width = other.width; + this.height = other.height; + return this; + } + + /** + * Définit les valeurs x, y, width, height + */ + set(x: number, y: number, width: number, height: number): Rect { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + return this; + } + + /** + * Retourne une représentation string + */ + toString(): string { + return `Rect(${this.x}, ${this.y}, ${this.width}, ${this.height})`; + } + + /** + * Rectangle vide (width et height à 0) + */ + static readonly empty = new Rect(0, 0, 0, 0); +} + diff --git a/src/engine/math/Vector2.ts b/src/engine/math/Vector2.ts new file mode 100644 index 0000000..d541d53 --- /dev/null +++ b/src/engine/math/Vector2.ts @@ -0,0 +1,246 @@ +/** + * Vector2 - Vecteur 2D + * Représente un point ou une direction en 2D + */ + +export class Vector2 { + public x: number; + public y: number; + + constructor(x: number = 0, y: number = 0) { + this.x = x; + this.y = y; + } + + /** + * Addition de deux vecteurs (retourne un nouveau vecteur) + */ + add(other: Vector2): Vector2 { + return new Vector2(this.x + other.x, this.y + other.y); + } + + /** + * Addition mutative (modifie ce vecteur) + */ + addMut(other: Vector2): Vector2 { + this.x += other.x; + this.y += other.y; + return this; + } + + /** + * Soustraction de deux vecteurs (retourne un nouveau vecteur) + */ + subtract(other: Vector2): Vector2 { + return new Vector2(this.x - other.x, this.y - other.y); + } + + /** + * Soustraction mutative (modifie ce vecteur) + */ + subtractMut(other: Vector2): Vector2 { + this.x -= other.x; + this.y -= other.y; + return this; + } + + /** + * Multiplication par un scalaire (retourne un nouveau vecteur) + */ + multiply(scalar: number): Vector2 { + return new Vector2(this.x * scalar, this.y * scalar); + } + + /** + * Multiplication mutative (modifie ce vecteur) + */ + multiplyMut(scalar: number): Vector2 { + this.x *= scalar; + this.y *= scalar; + return this; + } + + /** + * Division par un scalaire (retourne un nouveau vecteur) + */ + divide(scalar: number): Vector2 { + if (scalar === 0) { + throw new Error('Division par zéro'); + } + return new Vector2(this.x / scalar, this.y / scalar); + } + + /** + * Division mutative (modifie ce vecteur) + */ + divideMut(scalar: number): Vector2 { + if (scalar === 0) { + throw new Error('Division par zéro'); + } + this.x /= scalar; + this.y /= scalar; + return this; + } + + /** + * Longueur (magnitude) du vecteur + */ + length(): number { + return Math.sqrt(this.x * this.x + this.y * this.y); + } + + /** + * Longueur au carré (plus rapide, évite la racine carrée) + */ + lengthSquared(): number { + return this.x * this.x + this.y * this.y; + } + + /** + * Normalise le vecteur (retourne un nouveau vecteur de longueur 1) + */ + normalize(): Vector2 { + const len = this.length(); + if (len === 0) { + return new Vector2(0, 0); + } + return new Vector2(this.x / len, this.y / len); + } + + /** + * Normalise mutatif (modifie ce vecteur pour qu'il ait une longueur de 1) + */ + normalizeMut(): Vector2 { + const len = this.length(); + if (len === 0) { + this.x = 0; + this.y = 0; + return this; + } + this.x /= len; + this.y /= len; + return this; + } + + /** + * Produit scalaire avec un autre vecteur + */ + dot(other: Vector2): number { + return this.x * other.x + this.y * other.y; + } + + /** + * Distance entre ce point et un autre + */ + distance(other: Vector2): number { + const dx = this.x - other.x; + const dy = this.y - other.y; + return Math.sqrt(dx * dx + dy * dy); + } + + /** + * Distance au carré (plus rapide) + */ + distanceSquared(other: Vector2): number { + const dx = this.x - other.x; + const dy = this.y - other.y; + return dx * dx + dy * dy; + } + + /** + * Interpolation linéaire vers un autre vecteur + * @param other Vecteur de destination + * @param t Facteur d'interpolation (0 = ce vecteur, 1 = other) + */ + lerp(other: Vector2, t: number): Vector2 { + const clampedT = Math.max(0, Math.min(1, t)); + return new Vector2( + this.x + (other.x - this.x) * clampedT, + this.y + (other.y - this.y) * clampedT + ); + } + + /** + * Rotation du vecteur (retourne un nouveau vecteur) + * @param angle Angle en radians + */ + rotate(angle: number): Vector2 { + const cos = Math.cos(angle); + const sin = Math.sin(angle); + return new Vector2( + this.x * cos - this.y * sin, + this.x * sin + this.y * cos + ); + } + + /** + * Rotation mutative (modifie ce vecteur) + * @param angle Angle en radians + */ + rotateMut(angle: number): Vector2 { + const cos = Math.cos(angle); + const sin = Math.sin(angle); + const newX = this.x * cos - this.y * sin; + const newY = this.x * sin + this.y * cos; + this.x = newX; + this.y = newY; + return this; + } + + /** + * Angle du vecteur en radians (de 0 à 2π) + */ + angle(): number { + return Math.atan2(this.y, this.x); + } + + /** + * Copie ce vecteur + */ + clone(): Vector2 { + return new Vector2(this.x, this.y); + } + + /** + * Copie les valeurs d'un autre vecteur + */ + copyFrom(other: Vector2): Vector2 { + this.x = other.x; + this.y = other.y; + return this; + } + + /** + * Définit les valeurs x et y + */ + set(x: number, y: number): Vector2 { + this.x = x; + this.y = y; + return this; + } + + /** + * Retourne un tableau [x, y] + */ + toArray(): [number, number] { + return [this.x, this.y]; + } + + /** + * Retourne une représentation string + */ + toString(): string { + return `Vector2(${this.x}, ${this.y})`; + } + + /** + * Constantes statiques + */ + static readonly zero = new Vector2(0, 0); + static readonly one = new Vector2(1, 1); + static readonly up = new Vector2(0, -1); // WebGL: Y vers le haut = négatif + static readonly down = new Vector2(0, 1); + static readonly left = new Vector2(-1, 0); + static readonly right = new Vector2(1, 0); +} + diff --git a/src/engine/physics/BoxCollider2D.ts b/src/engine/physics/BoxCollider2D.ts new file mode 100644 index 0000000..9b91120 --- /dev/null +++ b/src/engine/physics/BoxCollider2D.ts @@ -0,0 +1,115 @@ +/** + * BoxCollider2D - Collider rectangulaire (AABB - Axis-Aligned Bounding Box) + */ + +import { Collider2D } from './Collider2D'; +import { Vector2 } from '../math/Vector2'; +import { Rect } from '../math/Rect'; +import { CircleCollider2D } from './CircleCollider2D'; + +export class BoxCollider2D extends Collider2D { + private _size: Vector2 = new Vector2(1, 1); + + /** + * Taille du collider (largeur, hauteur) + */ + get size(): Vector2 { + return this._size; + } + + set size(value: Vector2) { + this._size = value.clone(); + } + + /** + * Largeur du collider + */ + get width(): number { + return this._size.x; + } + + set width(value: number) { + this._size.x = value; + } + + /** + * Hauteur du collider + */ + get height(): number { + return this._size.y; + } + + set height(value: number) { + this._size.y = value; + } + + /** + * Retourne le rectangle de collision dans l'espace monde + */ + getBounds(): { x: number; y: number; width: number; height: number } { + const pos = this.worldPosition; + // Centre le rectangle sur la position (comme Unity) + return { + x: pos.x - this._size.x / 2, + y: pos.y - this._size.y / 2, + width: this._size.x, + height: this._size.y + }; + } + + /** + * Retourne le rectangle de collision + */ + getRect(): Rect { + const bounds = this.getBounds(); + return new Rect(bounds.x, bounds.y, bounds.width, bounds.height); + } + + /** + * Teste la collision avec un autre collider + */ + intersects(other: Collider2D): boolean { + if (other instanceof BoxCollider2D) { + return this._intersectsBox(other); + } + if (other instanceof CircleCollider2D) { + return this._intersectsCircle(other); + } + return false; + } + + /** + * Teste la collision avec un autre BoxCollider2D (AABB) + */ + private _intersectsBox(other: BoxCollider2D): boolean { + const thisBounds = this.getBounds(); + const otherBounds = other.getBounds(); + + return thisBounds.x < otherBounds.x + otherBounds.width && + thisBounds.x + thisBounds.width > otherBounds.x && + thisBounds.y < otherBounds.y + otherBounds.height && + thisBounds.y + thisBounds.height > otherBounds.y; + } + + /** + * Teste la collision avec un CircleCollider2D + */ + private _intersectsCircle(other: CircleCollider2D): boolean { + const thisBounds = this.getBounds(); + const thisRect = new Rect(thisBounds.x, thisBounds.y, thisBounds.width, thisBounds.height); + const circleCenter = other.worldPosition; + const circleRadius = other.radius; + + // Trouve le point le plus proche du cercle dans le rectangle + const closestX = Math.max(thisRect.left, Math.min(circleCenter.x, thisRect.right)); + const closestY = Math.max(thisRect.top, Math.min(circleCenter.y, thisRect.bottom)); + + // Calcule la distance du point le plus proche au centre du cercle + const distanceX = circleCenter.x - closestX; + const distanceY = circleCenter.y - closestY; + const distanceSquared = distanceX * distanceX + distanceY * distanceY; + + return distanceSquared < circleRadius * circleRadius; + } +} + diff --git a/src/engine/physics/CircleCollider2D.ts b/src/engine/physics/CircleCollider2D.ts new file mode 100644 index 0000000..f991968 --- /dev/null +++ b/src/engine/physics/CircleCollider2D.ts @@ -0,0 +1,70 @@ +/** + * CircleCollider2D - Collider circulaire + */ + +import { Collider2D } from './Collider2D'; +import { BoxCollider2D } from './BoxCollider2D'; + +export class CircleCollider2D extends Collider2D { + private _radius: number = 0.5; + + /** + * Rayon du collider + */ + get radius(): number { + return this._radius; + } + + set radius(value: number) { + this._radius = Math.max(0, value); + } + + /** + * Diamètre du collider + */ + get diameter(): number { + return this._radius * 2; + } + + set diameter(value: number) { + this._radius = Math.max(0, value / 2); + } + + /** + * Retourne les bounds (rectangle englobant) du collider + */ + getBounds(): { x: number; y: number; width: number; height: number } { + const pos = this.worldPosition; + return { + x: pos.x - this._radius, + y: pos.y - this._radius, + width: this._radius * 2, + height: this._radius * 2 + }; + } + + /** + * Teste la collision avec un autre collider + */ + intersects(other: Collider2D): boolean { + if (other instanceof CircleCollider2D) { + return this._intersectsCircle(other); + } + // BoxCollider2D gère déjà Box-Circle dans son intersects() + if (other instanceof BoxCollider2D) { + return other.intersects(this); // Délègue à BoxCollider2D (qui gère Box-Circle) + } + return false; + } + + /** + * Teste la collision avec un autre CircleCollider2D + */ + private _intersectsCircle(other: CircleCollider2D): boolean { + const thisPos = this.worldPosition; + const otherPos = other.worldPosition; + const distance = thisPos.distance(otherPos); + return distance < (this._radius + other.radius); + } +} + diff --git a/src/engine/physics/Collider2D.ts b/src/engine/physics/Collider2D.ts new file mode 100644 index 0000000..9accf0b --- /dev/null +++ b/src/engine/physics/Collider2D.ts @@ -0,0 +1,207 @@ +/** + * Collider2D - Classe de base abstraite pour les colliders 2D + * Gère les collisions et les événements de trigger + */ + +import { Component } from '../entities/Component'; +import { Vector2 } from '../math/Vector2'; + +export type CollisionCallback = (other: Collider2D) => void; + +export abstract class Collider2D extends Component { + private _isTrigger: boolean = false; + private _offset: Vector2 = new Vector2(0, 0); + + // Événements de collision + private _onTriggerEnterCallbacks: CollisionCallback[] = []; + private _onTriggerExitCallbacks: CollisionCallback[] = []; + private _onCollisionEnterCallbacks: CollisionCallback[] = []; + private _onCollisionExitCallbacks: CollisionCallback[] = []; + + // Suivi des collisions actuelles (pour détecter exit) + private _currentTriggers: Set = new Set(); + private _currentCollisions: Set = new Set(); + + /** + * Si true, le collider est un trigger (pas de résolution physique) + */ + get isTrigger(): boolean { + return this._isTrigger; + } + + set isTrigger(value: boolean) { + this._isTrigger = value; + } + + /** + * Offset du collider par rapport au Transform + */ + get offset(): Vector2 { + return this._offset; + } + + set offset(value: Vector2) { + this._offset = value.clone(); + } + + /** + * Position du collider dans l'espace monde (position + offset) + */ + get worldPosition(): Vector2 { + const pos = this.transform.position; + if (this._offset.x === 0 && this._offset.y === 0) { + return pos; + } + return pos.add(this._offset); + } + + /** + * Ajoute un callback pour OnTriggerEnter + */ + onTriggerEnter(callback: CollisionCallback): void { + this._onTriggerEnterCallbacks.push(callback); + } + + /** + * Retire un callback pour OnTriggerEnter + */ + removeTriggerEnter(callback: CollisionCallback): void { + const index = this._onTriggerEnterCallbacks.indexOf(callback); + if (index !== -1) { + this._onTriggerEnterCallbacks.splice(index, 1); + } + } + + /** + * Ajoute un callback pour OnTriggerExit + */ + onTriggerExit(callback: CollisionCallback): void { + this._onTriggerExitCallbacks.push(callback); + } + + /** + * Retire un callback pour OnTriggerExit + */ + removeTriggerExit(callback: CollisionCallback): void { + const index = this._onTriggerExitCallbacks.indexOf(callback); + if (index !== -1) { + this._onTriggerExitCallbacks.splice(index, 1); + } + } + + /** + * Ajoute un callback pour OnCollisionEnter + */ + onCollisionEnter(callback: CollisionCallback): void { + this._onCollisionEnterCallbacks.push(callback); + } + + /** + * Retire un callback pour OnCollisionEnter + */ + removeCollisionEnter(callback: CollisionCallback): void { + const index = this._onCollisionEnterCallbacks.indexOf(callback); + if (index !== -1) { + this._onCollisionEnterCallbacks.splice(index, 1); + } + } + + /** + * Ajoute un callback pour OnCollisionExit + */ + onCollisionExit(callback: CollisionCallback): void { + this._onCollisionExitCallbacks.push(callback); + } + + /** + * Retire un callback pour OnCollisionExit + */ + removeCollisionExit(callback: CollisionCallback): void { + const index = this._onCollisionExitCallbacks.indexOf(callback); + if (index !== -1) { + this._onCollisionExitCallbacks.splice(index, 1); + } + } + + /** + * Méthode abstraite : Teste la collision avec un autre collider + */ + abstract intersects(other: Collider2D): boolean; + + /** + * Méthode abstraite : Calcule le bounds (Rectangle englobant) du collider + */ + abstract getBounds(): { x: number; y: number; width: number; height: number }; + + /** + * Appelé par PhysicsSystem quand une collision est détectée + * @internal + */ + _notifyCollision(other: Collider2D): void { + if (this._isTrigger || other._isTrigger) { + // Trigger event + if (!this._currentTriggers.has(other)) { + this._currentTriggers.add(other); + for (const callback of this._onTriggerEnterCallbacks) { + callback(other); + } + } + } else { + // Collision event + if (!this._currentCollisions.has(other)) { + this._currentCollisions.add(other); + for (const callback of this._onCollisionEnterCallbacks) { + callback(other); + } + } + } + } + + /** + * Appelé par PhysicsSystem après la détection de collisions + * Nettoie les collisions qui n'existent plus + * @internal + */ + _cleanupCollisions(currentCollisions: Set): void { + // Triggers + const triggersToRemove: Collider2D[] = []; + for (const trigger of this._currentTriggers) { + if (!currentCollisions.has(trigger)) { + triggersToRemove.push(trigger); + } + } + for (const trigger of triggersToRemove) { + this._currentTriggers.delete(trigger); + for (const callback of this._onTriggerExitCallbacks) { + callback(trigger); + } + } + + // Collisions + const collisionsToRemove: Collider2D[] = []; + for (const collision of this._currentCollisions) { + if (!currentCollisions.has(collision)) { + collisionsToRemove.push(collision); + } + } + for (const collision of collisionsToRemove) { + this._currentCollisions.delete(collision); + for (const callback of this._onCollisionExitCallbacks) { + callback(collision); + } + } + } + + /** + * Nettoie les ressources + */ + onDestroy(): void { + this._onTriggerEnterCallbacks.length = 0; + this._onTriggerExitCallbacks.length = 0; + this._onCollisionEnterCallbacks.length = 0; + this._onCollisionExitCallbacks.length = 0; + this._currentTriggers.clear(); + this._currentCollisions.clear(); + } +} + diff --git a/src/engine/physics/PhysicsSystem.ts b/src/engine/physics/PhysicsSystem.ts new file mode 100644 index 0000000..9b1c679 --- /dev/null +++ b/src/engine/physics/PhysicsSystem.ts @@ -0,0 +1,323 @@ +/** + * PhysicsSystem - Système de détection et résolution de collisions + * Gère la détection de collisions et l'intégration de la physique + */ + +import { Scene } from '../core/Scene'; +import { GameObject } from '../entities/GameObject'; +import { Collider2D } from './Collider2D'; +import { BoxCollider2D } from './BoxCollider2D'; +import { CircleCollider2D } from './CircleCollider2D'; +import { Rigidbody2D } from './Rigidbody2D'; +import { Vector2 } from '../math/Vector2'; +import { Time } from '../core/Time'; + +export class PhysicsSystem { + private _fixedTimeStep: number = 1 / 60; // 60 FPS fixe pour la physique + private _accumulator: number = 0; + + /** + * Timestep fixe pour la physique (en secondes) + */ + get fixedTimeStep(): number { + return this._fixedTimeStep; + } + + set fixedTimeStep(value: number) { + this._fixedTimeStep = Math.max(0.001, value); + } + + /** + * Update du système de physique (appelé chaque frame) + * Utilise un fixed timestep pour la stabilité + */ + update(scene: Scene): void { + // Fixed timestep accumulation + this._accumulator += Time.deltaTime; + + // Exécute plusieurs fixed updates si nécessaire + const maxIterations = 5; // Limite pour éviter le spiral of death + let iterations = 0; + + while (this._accumulator >= this._fixedTimeStep && iterations < maxIterations) { + this._fixedUpdate(scene, this._fixedTimeStep); + this._accumulator -= this._fixedTimeStep; + iterations++; + } + + // Si l'accumulateur devient trop grand, on le limite + if (this._accumulator > this._fixedTimeStep * 2) { + this._accumulator = this._fixedTimeStep; + } + } + + /** + * Fixed update (physique stable) + */ + private _fixedUpdate(scene: Scene, fixedDeltaTime: number): void { + // 1. Update tous les Rigidbody2D + this._updateRigidbodies(scene, fixedDeltaTime); + + // 2. Détecte les collisions + this._detectCollisions(scene); + } + + /** + * Update tous les Rigidbody2D de la scène + */ + private _updateRigidbodies(scene: Scene, fixedDeltaTime: number): void { + const gameObjects = scene.gameObjects; + + for (const obj of gameObjects) { + if (!obj.active || obj.isDestroyed) { + continue; + } + + // Update récursif des enfants + this._updateRigidbodyRecursive(obj, fixedDeltaTime); + } + } + + /** + * Update récursif des Rigidbody2D + */ + private _updateRigidbodyRecursive(obj: GameObject, fixedDeltaTime: number): void { + if (!obj.active || obj.isDestroyed) { + return; + } + + const rigidbody = obj.getComponent(Rigidbody2D); + if (rigidbody && rigidbody.enabled) { + rigidbody._fixedUpdate(fixedDeltaTime); + } + + // Parcourt les enfants + for (const child of obj.children) { + this._updateRigidbodyRecursive(child, fixedDeltaTime); + } + } + + /** + * Détecte toutes les collisions dans la scène + */ + private _detectCollisions(scene: Scene): void { + const colliders: Collider2D[] = []; + + // Collecte tous les colliders actifs + this._collectColliders(scene, colliders); + + // Suivi des collisions actuelles (pour cleanup des événements Exit) + const collisionMap = new Map>(); + + // Teste chaque paire de colliders + for (let i = 0; i < colliders.length; i++) { + const colliderA = colliders[i]!; + + if (!colliderA.enabled || !colliderA.gameObject.active) { + continue; + } + + const collisionsA = new Set(); + + for (let j = i + 1; j < colliders.length; j++) { + const colliderB = colliders[j]!; + + if (!colliderB.enabled || !colliderB.gameObject.active) { + continue; + } + + // Ignore les colliders du même GameObject + if (colliderA.gameObject === colliderB.gameObject) { + continue; + } + + // Teste la collision + if (colliderA.intersects(colliderB)) { + collisionsA.add(colliderB); + + // Notifie les deux colliders + colliderA._notifyCollision(colliderB); + colliderB._notifyCollision(colliderA); + + // Résolution physique (si pas de trigger) + if (!colliderA.isTrigger && !colliderB.isTrigger) { + this._resolveCollision(colliderA, colliderB); + } + } + } + + collisionMap.set(colliderA, collisionsA); + } + + // Cleanup des événements Exit + for (const [collider, currentCollisions] of collisionMap) { + collider._cleanupCollisions(currentCollisions); + } + } + + /** + * Collecte récursivement tous les colliders de la scène + */ + private _collectColliders(scene: Scene, colliders: Collider2D[]): void { + const gameObjects = scene.gameObjects; + + for (const obj of gameObjects) { + if (!obj.active || obj.isDestroyed) { + continue; + } + + this._collectCollidersRecursive(obj, colliders); + } + } + + /** + * Collecte récursivement les colliders d'un GameObject + */ + private _collectCollidersRecursive(obj: GameObject, colliders: Collider2D[]): void { + if (!obj.active || obj.isDestroyed) { + return; + } + + // Cherche BoxCollider2D + const boxCollider = obj.getComponent(BoxCollider2D); + if (boxCollider && boxCollider.enabled) { + colliders.push(boxCollider); + } + + // Cherche CircleCollider2D + const circleCollider = obj.getComponent(CircleCollider2D); + if (circleCollider && circleCollider.enabled) { + colliders.push(circleCollider); + } + + // Parcourt les enfants + for (const child of obj.children) { + this._collectCollidersRecursive(child, colliders); + } + } + + /** + * Résout une collision physique (sépare les objets) + */ + private _resolveCollision(colliderA: Collider2D, colliderB: Collider2D): void { + const rigidbodyA = colliderA.getComponent(Rigidbody2D); + const rigidbodyB = colliderB.getComponent(Rigidbody2D); + + // Si aucun des deux n'a de rigidbody, pas de résolution + if (!rigidbodyA && !rigidbodyB) { + return; + } + + // Calcul de la séparation (basique - pour BoxCollider2D) + if (colliderA instanceof BoxCollider2D && colliderB instanceof BoxCollider2D) { + this._resolveBoxBoxCollision(colliderA, colliderB, rigidbodyA, rigidbodyB); + } else { + // Pour CircleCollider2D ou Box-Circle, résolution simple + this._resolveSimpleCollision(colliderA, colliderB, rigidbodyA, rigidbodyB); + } + } + + /** + * Résout une collision Box-Box (AABB) + */ + private _resolveBoxBoxCollision( + colliderA: BoxCollider2D, + colliderB: BoxCollider2D, + rigidbodyA: Rigidbody2D | null, + rigidbodyB: Rigidbody2D | null + ): void { + const boundsA = colliderA.getBounds(); + const boundsB = colliderB.getBounds(); + + // Calcule la pénétration + const overlapX = Math.min( + boundsA.x + boundsA.width - boundsB.x, + boundsB.x + boundsB.width - boundsA.x + ); + const overlapY = Math.min( + boundsA.y + boundsA.height - boundsB.y, + boundsB.y + boundsB.height - boundsA.y + ); + + // Séparation dans la direction de la plus petite pénétration + if (overlapX < overlapY) { + const direction = boundsA.x < boundsB.x ? -1 : 1; + const separation = overlapX * direction; + + if (rigidbodyA && !rigidbodyB) { + colliderA.transform.position = colliderA.transform.position.add(new Vector2(separation, 0)); + } else if (rigidbodyB && !rigidbodyA) { + colliderB.transform.position = colliderB.transform.position.add(new Vector2(-separation, 0)); + } else if (rigidbodyA && rigidbodyB) { + // Les deux bougent : séparation proportionnelle à la masse + const totalMass = rigidbodyA.mass + rigidbodyB.mass; + const ratioA = rigidbodyB.mass / totalMass; + const ratioB = rigidbodyA.mass / totalMass; + colliderA.transform.position = colliderA.transform.position.add(new Vector2(separation * ratioA, 0)); + colliderB.transform.position = colliderB.transform.position.add(new Vector2(-separation * ratioB, 0)); + } + } else { + const direction = boundsA.y < boundsB.y ? -1 : 1; + const separation = overlapY * direction; + + if (rigidbodyA && !rigidbodyB) { + colliderA.transform.position = colliderA.transform.position.add(new Vector2(0, separation)); + } else if (rigidbodyB && !rigidbodyA) { + colliderB.transform.position = colliderB.transform.position.add(new Vector2(0, -separation)); + } else if (rigidbodyA && rigidbodyB) { + const totalMass = rigidbodyA.mass + rigidbodyB.mass; + const ratioA = rigidbodyB.mass / totalMass; + const ratioB = rigidbodyA.mass / totalMass; + colliderA.transform.position = colliderA.transform.position.add(new Vector2(0, separation * ratioA)); + colliderB.transform.position = colliderB.transform.position.add(new Vector2(0, -separation * ratioB)); + } + } + } + + /** + * Résout une collision simple (pour Circle ou Box-Circle) + */ + private _resolveSimpleCollision( + colliderA: Collider2D, + colliderB: Collider2D, + rigidbodyA: Rigidbody2D | null, + rigidbodyB: Rigidbody2D | null + ): void { + const posA = colliderA.worldPosition; + const posB = colliderB.worldPosition; + const direction = posB.subtract(posA); + const distance = direction.length(); + + if (distance === 0) { + return; // Évite la division par zéro + } + + const normalized = direction.multiply(1 / distance); + + // Séparation minimale (pour BoxCollider2D, utilise la moitié de la taille) + let separation = 0; + if (colliderA instanceof BoxCollider2D && colliderB instanceof BoxCollider2D) { + const sizeA = colliderA.size; + const sizeB = colliderB.size; + separation = (sizeA.x + sizeA.y + sizeB.x + sizeB.y) / 4; // Approximation + } else { + // Pour les cercles ou autres + separation = distance; + } + + const correction = normalized.multiply((separation - distance) / 2); + + if (rigidbodyA && !rigidbodyB) { + colliderA.transform.position = colliderA.transform.position.add(correction); + } else if (rigidbodyB && !rigidbodyA) { + colliderB.transform.position = colliderB.transform.position.add(correction.multiply(-1)); + } else if (rigidbodyA && rigidbodyB) { + const totalMass = rigidbodyA.mass + rigidbodyB.mass; + const ratioA = rigidbodyB.mass / totalMass; + const ratioB = rigidbodyA.mass / totalMass; + colliderA.transform.position = colliderA.transform.position.add(correction.multiply(ratioA)); + colliderB.transform.position = colliderB.transform.position.add(correction.multiply(-ratioB)); + } + } +} + diff --git a/src/engine/physics/Rigidbody2D.ts b/src/engine/physics/Rigidbody2D.ts new file mode 100644 index 0000000..0c03f2d --- /dev/null +++ b/src/engine/physics/Rigidbody2D.ts @@ -0,0 +1,184 @@ +/** + * Rigidbody2D - Physique de mouvement 2D + * Gère la vélocité, les forces et l'intégration du mouvement + */ + +import { Component } from '../entities/Component'; +import { Vector2 } from '../math/Vector2'; +import { Time } from '../core/Time'; + +export class Rigidbody2D extends Component { + private _velocity: Vector2 = new Vector2(0, 0); + private _angularVelocity: number = 0; // degrés par seconde + + private _mass: number = 1; + private _drag: number = 0; // Friction linéaire (0 = pas de friction) + private _angularDrag: number = 0; // Friction angulaire + + private _useGravity: boolean = false; + private _gravityScale: number = 1; + + // Constante de gravité (pixels par seconde²) + private static readonly GRAVITY: number = 980; // ~9.8 m/s² * 100 pixels/mètre + + /** + * Vélocité linéaire (pixels par seconde) + */ + get velocity(): Vector2 { + return this._velocity; + } + + set velocity(value: Vector2) { + this._velocity = value.clone(); + } + + /** + * Vélocité angulaire (degrés par seconde) + */ + get angularVelocity(): number { + return this._angularVelocity; + } + + set angularVelocity(value: number) { + this._angularVelocity = value; + } + + /** + * Masse de l'objet + */ + get mass(): number { + return this._mass; + } + + set mass(value: number) { + this._mass = Math.max(0.001, value); // Évite la division par zéro + } + + /** + * Friction linéaire (drag) + */ + get drag(): number { + return this._drag; + } + + set drag(value: number) { + this._drag = Math.max(0, value); + } + + /** + * Friction angulaire + */ + get angularDrag(): number { + return this._angularDrag; + } + + set angularDrag(value: number) { + this._angularDrag = Math.max(0, value); + } + + /** + * Si true, l'objet est affecté par la gravité + */ + get useGravity(): boolean { + return this._useGravity; + } + + set useGravity(value: boolean) { + this._useGravity = value; + } + + /** + * Multiplicateur de gravité + */ + get gravityScale(): number { + return this._gravityScale; + } + + set gravityScale(value: number) { + this._gravityScale = value; + } + + /** + * Ajoute une force à l'objet (modifie la vélocité) + * @param force Force en newtons (sera divisée par la masse) + */ + addForce(force: Vector2): void { + // F = ma => a = F/m + const acceleration = force.multiply(1 / this._mass); + this._velocity.addMut(acceleration.multiply(Time.deltaTime)); + } + + /** + * Ajoute une impulsion (modifie la vélocité instantanément) + * @param impulse Impulsion (sera divisée par la masse) + */ + addImpulse(impulse: Vector2): void { + const velocityChange = impulse.multiply(1 / this._mass); + this._velocity.addMut(velocityChange); + } + + /** + * Ajoute un couple (rotation) + * @param torque Couple en newtons-mètres + */ + addTorque(torque: number): void { + // Moment d'inertie approximatif pour un rectangle + const momentOfInertia = this._mass * 0.0833; // I = m * (w² + h²) / 12, approximé + const angularAcceleration = torque / momentOfInertia; + this._angularVelocity += angularAcceleration * Time.deltaTime; + } + + /** + * Définit la vélocité directement (ignorant les forces) + */ + setVelocity(velocity: Vector2): void { + this._velocity = velocity.clone(); + } + + /** + * Met la vélocité à zéro + */ + stop(): void { + this._velocity = new Vector2(0, 0); + this._angularVelocity = 0; + } + + /** + * Update du mouvement (appelé par PhysicsSystem) + * @internal + */ + _fixedUpdate(fixedDeltaTime: number): void { + // Gravité + if (this._useGravity) { + this._velocity.y -= Rigidbody2D.GRAVITY * this._gravityScale * fixedDeltaTime; + } + + // Drag (friction) + if (this._drag > 0) { + const dragForce = this._velocity.multiply(-this._drag * fixedDeltaTime); + this._velocity.addMut(dragForce); + // Arrête la vélocité si elle devient très petite + if (this._velocity.lengthSquared() < 0.01) { + this._velocity = new Vector2(0, 0); + } + } + + // Rotation avec drag + if (this._angularDrag > 0) { + this._angularVelocity *= (1 - this._angularDrag * fixedDeltaTime); + if (Math.abs(this._angularVelocity) < 0.01) { + this._angularVelocity = 0; + } + } + + // Intégration du mouvement + const deltaMovement = this._velocity.multiply(fixedDeltaTime); + this.transform.translate(deltaMovement); + + // Rotation + if (this._angularVelocity !== 0) { + this.transform.rotate(this._angularVelocity * fixedDeltaTime); + } + } +} + diff --git a/src/engine/physics/index.ts b/src/engine/physics/index.ts new file mode 100644 index 0000000..b3c47e3 --- /dev/null +++ b/src/engine/physics/index.ts @@ -0,0 +1,10 @@ +/** + * Exports du système de physique + */ + +export { Collider2D, type CollisionCallback } from './Collider2D'; +export { BoxCollider2D } from './BoxCollider2D'; +export { CircleCollider2D } from './CircleCollider2D'; +export { Rigidbody2D } from './Rigidbody2D'; +export { PhysicsSystem } from './PhysicsSystem'; + diff --git a/src/engine/rendering/Camera.ts b/src/engine/rendering/Camera.ts new file mode 100644 index 0000000..ef053af --- /dev/null +++ b/src/engine/rendering/Camera.ts @@ -0,0 +1,253 @@ +/** + * Camera - Viewport et transformations + * Définit quelle partie du monde est visible + */ + +import { Vector2 } from '../math/Vector2'; +import { Matrix3 } from '../math/Matrix3'; +import { Rect } from '../math/Rect'; + +export class Camera { + private _position: Vector2 = new Vector2(0, 0); + private _zoom: number = 1.0; + private _rotation: number = 0; // En radians + + private _viewportWidth: number = 800; + private _viewportHeight: number = 600; + + private _bounds: Rect | null = null; + + // Cache des matrices (dirty flags) + private _projectionMatrix: Matrix3 | null = null; + private _viewMatrix: Matrix3 | null = null; + private _viewProjMatrix: Matrix3 | null = null; + private _dirty: boolean = true; + + constructor(viewportWidth: number = 800, viewportHeight: number = 600) { + this._viewportWidth = viewportWidth; + this._viewportHeight = viewportHeight; + this._dirty = true; + } + + /** + * Position de la caméra dans le monde + */ + get position(): Vector2 { + return this._position; + } + + set position(value: Vector2) { + this._position = value; + this._markDirty(); + this._clampToBounds(); + } + + /** + * Zoom (1 = normal, 2 = 2× plus proche, 0.5 = 2× plus loin) + */ + get zoom(): number { + return this._zoom; + } + + set zoom(value: number) { + this._zoom = Math.max(0.01, value); // Évite le zoom négatif ou zéro + this._markDirty(); + } + + /** + * Rotation en radians + */ + get rotation(): number { + return this._rotation; + } + + set rotation(value: number) { + this._rotation = value; + this._markDirty(); + } + + /** + * Largeur du viewport + */ + get viewportWidth(): number { + return this._viewportWidth; + } + + set viewportWidth(value: number) { + this._viewportWidth = value; + this._markDirty(); + } + + /** + * Hauteur du viewport + */ + get viewportHeight(): number { + return this._viewportHeight; + } + + set viewportHeight(value: number) { + this._viewportHeight = value; + this._markDirty(); + } + + /** + * Limites de déplacement (optionnel) + */ + get bounds(): Rect | null { + return this._bounds; + } + + set bounds(value: Rect | null) { + this._bounds = value; + this._clampToBounds(); + } + + /** + * Marque les matrices comme "sales" (besoin de recalculer) + */ + private _markDirty(): void { + this._dirty = true; + } + + /** + * Clamp la position aux bounds si définis + */ + private _clampToBounds(): void { + if (!this._bounds) { + return; + } + + const halfWidth = (this._viewportWidth / this._zoom) / 2; + const halfHeight = (this._viewportHeight / this._zoom) / 2; + + this._position.x = Math.max( + this._bounds.left + halfWidth, + Math.min(this._bounds.right - halfWidth, this._position.x) + ); + this._position.y = Math.max( + this._bounds.top + halfHeight, + Math.min(this._bounds.bottom - halfHeight, this._position.y) + ); + } + + /** + * Calcule la matrice de projection orthographique + * Convertit coordonnées pixel en coordonnées NDC WebGL (-1 à 1) + * Centre l'écran sur (0, 0) pour que la caméra fonctionne correctement + */ + getProjectionMatrix(): Matrix3 { + if (this._projectionMatrix && !this._dirty) { + return this._projectionMatrix; + } + + // Projection orthographique centrée sur le viewport + // Centre de l'écran = (viewportWidth/2, viewportHeight/2) + const centerX = this._viewportWidth / 2; + const centerY = this._viewportHeight / 2; + + this._projectionMatrix = Matrix3.orthographic( + -centerX, // left + centerX, // right + centerY, // bottom (WebGL: Y positif = bas) + -centerY // top (WebGL: Y négatif = haut) + ); + + return this._projectionMatrix; + } + + /** + * Calcule la matrice de vue + * Applique position, rotation, zoom de la caméra + */ + getViewMatrix(): Matrix3 { + if (this._viewMatrix && !this._dirty) { + return this._viewMatrix; + } + + // Ordre : Translate → Rotate → Scale (zoom) + this._viewMatrix = Matrix3.identity() + .translate(-this._position.x, -this._position.y) + .rotate(-this._rotation) + .scale(this._zoom, this._zoom); + + return this._viewMatrix; + } + + /** + * Calcule la matrice View × Projection (combinaison) + */ + getViewProjectionMatrix(): Matrix3 { + if (this._viewProjMatrix && !this._dirty) { + return this._viewProjMatrix; + } + + const proj = this.getProjectionMatrix(); + const view = this.getViewMatrix(); + + this._viewProjMatrix = proj.clone().multiply(view); + this._dirty = false; // Marque comme propre maintenant + + return this._viewProjMatrix; + } + + /** + * Convertit coordonnées écran (pixels) → coordonnées monde + */ + screenToWorld(screenPos: Vector2): Vector2 { + // Normalise les coordonnées écran (0-1) + const normalizedX = (screenPos.x / this._viewportWidth) * 2 - 1; + const normalizedY = (screenPos.y / this._viewportHeight) * 2 - 1; + + // Inverse la projection + const worldX = normalizedX * (this._viewportWidth / this._zoom) + this._position.x; + const worldY = normalizedY * (this._viewportHeight / this._zoom) + this._position.y; + + return new Vector2(worldX, worldY); + } + + /** + * Convertit coordonnées monde → coordonnées écran (pixels) + */ + worldToScreen(worldPos: Vector2): Vector2 { + const viewProj = this.getViewProjectionMatrix(); + const transformed = viewProj.transformPoint(worldPos); + + // Convertit NDC (-1 à 1) en pixels + const screenX = ((transformed.x + 1) / 2) * this._viewportWidth; + const screenY = ((transformed.y + 1) / 2) * this._viewportHeight; + + return new Vector2(screenX, screenY); + } + + /** + * Redimensionne le viewport + */ + resize(width: number, height: number): void { + this._viewportWidth = width; + this._viewportHeight = height; + this._markDirty(); + } + + /** + * Centre la caméra sur une position + */ + lookAt(target: Vector2): void { + this.position = target; + } + + /** + * Récupère le rectangle visible dans le monde (pour culling) + */ + getVisibleWorldBounds(): Rect { + const halfWidth = (this._viewportWidth / this._zoom) / 2; + const halfHeight = (this._viewportHeight / this._zoom) / 2; + + return new Rect( + this._position.x - halfWidth, + this._position.y - halfHeight, + this._viewportWidth / this._zoom, + this._viewportHeight / this._zoom + ); + } +} + diff --git a/src/engine/rendering/Shader.ts b/src/engine/rendering/Shader.ts new file mode 100644 index 0000000..88af074 --- /dev/null +++ b/src/engine/rendering/Shader.ts @@ -0,0 +1,121 @@ +/** + * Shader - Gestion des programmes GPU + * Compile et gère les shaders vertex et fragment + */ + +export class Shader { + private _program: WebGLProgram | null = null; + private _vertexShader: WebGLShader | null = null; + private _fragmentShader: WebGLShader | null = null; + + /** + * Compile un shader depuis le code source + */ + compile(gl: WebGLRenderingContext, vertexSource: string, fragmentSource: string): boolean { + // Compile vertex shader + this._vertexShader = this.compileShader(gl, gl.VERTEX_SHADER, vertexSource); + if (!this._vertexShader) { + return false; + } + + // Compile fragment shader + this._fragmentShader = this.compileShader(gl, gl.FRAGMENT_SHADER, fragmentSource); + if (!this._fragmentShader) { + return false; + } + + // Crée et lie le programme + this._program = gl.createProgram(); + if (!this._program) { + console.error('Impossible de créer le programme WebGL'); + return false; + } + + gl.attachShader(this._program, this._vertexShader); + gl.attachShader(this._program, this._fragmentShader); + gl.linkProgram(this._program); + + // Vérifie les erreurs de linking + if (!gl.getProgramParameter(this._program, gl.LINK_STATUS)) { + const info = gl.getProgramInfoLog(this._program); + console.error('Erreur de linking du shader:', info); + gl.deleteProgram(this._program); + this._program = null; + return false; + } + + return true; + } + + /** + * Compile un shader individuel + */ + private compileShader(gl: WebGLRenderingContext, type: number, source: string): WebGLShader | null { + const shader = gl.createShader(type); + if (!shader) { + console.error('Impossible de créer le shader'); + return null; + } + + gl.shaderSource(shader, source); + gl.compileShader(shader); + + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + const info = gl.getShaderInfoLog(shader); + console.error(`Erreur de compilation du shader (${type === gl.VERTEX_SHADER ? 'vertex' : 'fragment'}):`, info); + gl.deleteShader(shader); + return null; + } + + return shader; + } + + /** + * Active ce shader pour le rendu + */ + use(gl: WebGLRenderingContext): void { + if (!this._program) { + throw new Error('Shader non compilé'); + } + gl.useProgram(this._program); + } + + /** + * Récupère l'emplacement d'un attribut + */ + getAttributeLocation(gl: WebGLRenderingContext, name: string): number { + if (!this._program) { + throw new Error('Shader non compilé'); + } + return gl.getAttribLocation(this._program, name); + } + + /** + * Récupère l'emplacement d'un uniform + */ + getUniformLocation(gl: WebGLRenderingContext, name: string): WebGLUniformLocation | null { + if (!this._program) { + throw new Error('Shader non compilé'); + } + return gl.getUniformLocation(this._program, name); + } + + /** + * Nettoie les ressources + */ + dispose(gl: WebGLRenderingContext): void { + if (this._program) { + gl.deleteProgram(this._program); + this._program = null; + } + if (this._vertexShader) { + gl.deleteShader(this._vertexShader); + this._vertexShader = null; + } + if (this._fragmentShader) { + gl.deleteShader(this._fragmentShader); + this._fragmentShader = null; + } + } +} + diff --git a/src/engine/rendering/SpriteBatch.ts b/src/engine/rendering/SpriteBatch.ts new file mode 100644 index 0000000..17c2d1c --- /dev/null +++ b/src/engine/rendering/SpriteBatch.ts @@ -0,0 +1,316 @@ +/** + * SpriteBatch - Optimisation batch rendering + * Accumule plusieurs sprites et les dessine en un seul draw call + */ + +import { GLContext } from '../webgl/GLContext'; +import { Buffer, BufferType, BufferUsage } from '../webgl/Buffer'; +import { Texture } from './Texture'; +import { Matrix3 } from '../math/Matrix3'; +import { Vector2 } from '../math/Vector2'; +import { Rect } from '../math/Rect'; +import { Color } from '../math/Color'; +import { Shader } from './Shader'; + +// Structure d'un vertex : [x, y, u, v, r, g, b, a] = 8 floats +const FLOATS_PER_VERTEX = 8; +const VERTICES_PER_SPRITE = 4; // Un quad = 4 vertices +const INDICES_PER_SPRITE = 6; // 2 triangles = 6 indices + +export class SpriteBatch { + private _glContext: GLContext; + private _maxSprites: number = 10000; + + // Buffers CPU + private _vertices: Float32Array; + private _indices: Uint16Array; + + // Buffers GPU + private _vertexBuffer: Buffer | null = null; + private _indexBuffer: Buffer | null = null; + + // État du batch + private _spriteCount: number = 0; + private _currentTexture: Texture | null = null; + private _isBatching: boolean = false; + + // Shader actif (pour configurer les attributs) + private _shader: Shader | null = null; + + // Matrices pour les uniforms (stockées depuis begin()) + private _projectionMatrix: Matrix3 | null = null; + private _viewMatrix: Matrix3 | null = null; + + constructor(glContext: GLContext, maxSprites: number = 10000) { + this._glContext = glContext; + this._maxSprites = maxSprites; + + // Alloue les buffers CPU + this._vertices = new Float32Array(maxSprites * VERTICES_PER_SPRITE * FLOATS_PER_VERTEX); + this._indices = new Uint16Array(maxSprites * INDICES_PER_SPRITE); + + // Crée les buffers GPU + this._vertexBuffer = new Buffer( + glContext.gl, + BufferType.ARRAY_BUFFER, + BufferUsage.DYNAMIC_DRAW + ); + + this._indexBuffer = new Buffer( + glContext.gl, + BufferType.ELEMENT_ARRAY_BUFFER, + BufferUsage.STATIC_DRAW + ); + + // Prépare les indices (fixe pour tous les sprites) + this._generateIndices(); + } + + /** + * Génère les indices pour tous les sprites (une seule fois) + */ + private _generateIndices(): void { + for (let i = 0; i < this._maxSprites; i++) { + const baseIndex = i * VERTICES_PER_SPRITE; + const indexOffset = i * INDICES_PER_SPRITE; + + // Premier triangle: 0, 1, 2 + this._indices[indexOffset + 0] = baseIndex + 0; + this._indices[indexOffset + 1] = baseIndex + 1; + this._indices[indexOffset + 2] = baseIndex + 2; + + // Second triangle: 2, 1, 3 + this._indices[indexOffset + 3] = baseIndex + 2; + this._indices[indexOffset + 4] = baseIndex + 1; + this._indices[indexOffset + 5] = baseIndex + 3; + } + + // Upload les indices (statique) + this._indexBuffer!.uploadData(this._indices); + } + + /** + * Commence un batch de rendu + * @param projection Matrice de projection (utilisée par le shader via uniform) + * @param view Matrice de vue (utilisée par le shader via uniform) + * @param shader Shader à utiliser pour configurer les attributs + */ + begin(projection: Matrix3, view: Matrix3, shader?: Shader): void { + if (this._isBatching) { + console.warn('SpriteBatch déjà en cours, appel end() d\'abord'); + return; + } + + this._shader = shader || null; + this._projectionMatrix = projection; + this._viewMatrix = view; + this._spriteCount = 0; + this._currentTexture = null; + this._isBatching = true; + } + + /** + * Ajoute un sprite au batch + */ + draw( + texture: Texture, + position: Vector2, + sourceRect: Rect | null = null, + tint: Color = Color.white, + rotation: number = 0, + scale: Vector2 = Vector2.one, + pivot: Vector2 = new Vector2(0.5, 0.5) + ): void { + if (!this._isBatching) { + console.warn('SpriteBatch non démarré, appel begin() d\'abord'); + return; + } + + // Flush si le batch est plein ou si la texture change + if (this._spriteCount >= this._maxSprites || + (this._currentTexture && texture !== this._currentTexture)) { + this.flush(); + } + + // Si flush a été appelé, réinitialise pour la nouvelle texture + if (this._spriteCount === 0) { + this._currentTexture = texture; + } + + // Calcule les UVs + const srcRect = sourceRect || new Rect(0, 0, texture.width, texture.height); + const u1 = srcRect.left / texture.width; + const v1 = srcRect.top / texture.height; + const u2 = srcRect.right / texture.width; + const v2 = srcRect.bottom / texture.height; + + // Calcule les 4 coins du sprite avec rotation et scale + const halfWidth = (srcRect.width * scale.x) / 2; + const halfHeight = (srcRect.height * scale.y) / 2; + + // Offset du pivot + const pivotOffsetX = (pivot.x - 0.5) * srcRect.width * scale.x; + const pivotOffsetY = (pivot.y - 0.5) * srcRect.height * scale.y; + + // Les 4 coins locaux (avant rotation) + const corners = [ + new Vector2(-halfWidth - pivotOffsetX, -halfHeight - pivotOffsetY), // Top-left + new Vector2( halfWidth - pivotOffsetX, -halfHeight - pivotOffsetY), // Top-right + new Vector2(-halfWidth - pivotOffsetX, halfHeight - pivotOffsetY), // Bottom-left + new Vector2( halfWidth - pivotOffsetX, halfHeight - pivotOffsetY) // Bottom-right + ]; + + // Applique la rotation si nécessaire + if (rotation !== 0) { + for (let i = 0; i < corners.length; i++) { + const corner = corners[i]; + if (corner) { + corners[i] = corner.rotate(rotation); + } + } + } + + // Ajoute la position et transforme les vertices + const vertexOffset = this._spriteCount * VERTICES_PER_SPRITE * FLOATS_PER_VERTEX; + const r = tint.r; + const g = tint.g; + const b = tint.b; + const a = tint.a; + + // Calcule les positions monde de chaque coin (le shader fera la transformation view/projection) + for (let i = 0; i < corners.length; i++) { + const corner = corners[i]; + if (!corner) continue; + + // Position monde = coin local + position du transform + const worldPos = corner.add(position); + + const vOffset = vertexOffset + i * FLOATS_PER_VERTEX; + const u = i < 2 ? u1 : u2; + const v = (i === 0 || i === 2) ? v1 : v2; + + // [x, y, u, v, r, g, b, a] + // x, y = position monde (le shader appliquera view/projection) + this._vertices[vOffset + 0] = worldPos.x; + this._vertices[vOffset + 1] = worldPos.y; + this._vertices[vOffset + 2] = u; + this._vertices[vOffset + 3] = v; + this._vertices[vOffset + 4] = r; + this._vertices[vOffset + 5] = g; + this._vertices[vOffset + 6] = b; + this._vertices[vOffset + 7] = a; + } + + this._spriteCount++; + } + + /** + * Envoie tout au GPU et dessine + */ + flush(): void { + if (this._spriteCount === 0 || !this._currentTexture) { + return; + } + + if (!this._shader) { + console.warn('SpriteBatch.flush() appelé sans shader actif'); + return; + } + + const gl = this._glContext.gl; + + // Active le shader AVANT de configurer les attributs et de dessiner + this._shader.use(gl); + + // Définit les uniforms des matrices (doit être fait après l'activation du shader) + if (this._projectionMatrix && this._viewMatrix) { + const projLoc = this._shader.getUniformLocation(gl, 'uProjection'); + const viewLoc = this._shader.getUniformLocation(gl, 'uView'); + + if (projLoc) { + gl.uniformMatrix3fv(projLoc, false, this._projectionMatrix.toArray()); + } + if (viewLoc) { + gl.uniformMatrix3fv(viewLoc, false, this._viewMatrix.toArray()); + } + } + + // Upload les vertices vers le GPU + const verticesUsed = this._spriteCount * VERTICES_PER_SPRITE * FLOATS_PER_VERTEX; + const verticesSubset = this._vertices.subarray(0, verticesUsed); + this._vertexBuffer!.uploadData(verticesSubset); + + // Active et bind les buffers + this._vertexBuffer!.bind(); + this._indexBuffer!.bind(); + + // Configure les attributs du shader + const positionLoc = this._shader.getAttributeLocation(gl, 'aPosition'); + const texCoordLoc = this._shader.getAttributeLocation(gl, 'aTexCoord'); + const colorLoc = this._shader.getAttributeLocation(gl, 'aColor'); + + const stride = FLOATS_PER_VERTEX * 4; // 32 bytes (8 floats * 4 bytes) + const BYTES_PER_FLOAT = 4; + + // aPosition: [x, y] - offset 0, 2 floats + if (positionLoc >= 0) { + gl.enableVertexAttribArray(positionLoc); + gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, stride, 0); + } + + // aTexCoord: [u, v] - offset 8 bytes, 2 floats + if (texCoordLoc >= 0) { + gl.enableVertexAttribArray(texCoordLoc); + gl.vertexAttribPointer(texCoordLoc, 2, gl.FLOAT, false, stride, 2 * BYTES_PER_FLOAT); + } + + // aColor: [r, g, b, a] - offset 16 bytes, 4 floats + if (colorLoc >= 0) { + gl.enableVertexAttribArray(colorLoc); + gl.vertexAttribPointer(colorLoc, 4, gl.FLOAT, false, stride, 4 * BYTES_PER_FLOAT); + } + + // Bind la texture + this._currentTexture.bind(0); + + // Dessine + const indicesUsed = this._spriteCount * INDICES_PER_SPRITE; + gl.drawElements( + gl.TRIANGLES, + indicesUsed, + gl.UNSIGNED_SHORT, + 0 + ); + + // Reset pour le prochain batch + this._spriteCount = 0; + this._currentTexture = null; + } + + /** + * Termine le batch (flush automatique) + */ + end(): void { + if (!this._isBatching) { + return; + } + + this.flush(); + this._isBatching = false; + } + + /** + * Nettoie les ressources + */ + dispose(): void { + if (this._vertexBuffer) { + this._vertexBuffer.dispose(); + this._vertexBuffer = null; + } + if (this._indexBuffer) { + this._indexBuffer.dispose(); + this._indexBuffer = null; + } + } +} + diff --git a/src/engine/rendering/Texture.ts b/src/engine/rendering/Texture.ts new file mode 100644 index 0000000..ed39f7c --- /dev/null +++ b/src/engine/rendering/Texture.ts @@ -0,0 +1,220 @@ +/** + * Texture - Gestion des images WebGL + * Encapsule une texture WebGL (image sur le GPU) + */ + +export enum FilterMode { + NEAREST = WebGLRenderingContext.NEAREST, + LINEAR = WebGLRenderingContext.LINEAR +} + +export enum WrapMode { + CLAMP_TO_EDGE = WebGLRenderingContext.CLAMP_TO_EDGE, + REPEAT = WebGLRenderingContext.REPEAT, + MIRRORED_REPEAT = WebGLRenderingContext.MIRRORED_REPEAT +} + +export class Texture { + private _gl: WebGLRenderingContext; + private _texture: WebGLTexture | null = null; + private _width: number = 0; + private _height: number = 0; + private _id: number; + + private _filterMode: FilterMode = FilterMode.LINEAR; + private _wrapMode: WrapMode = WrapMode.CLAMP_TO_EDGE; + + private static _nextId = 1; + + constructor(gl: WebGLRenderingContext) { + this._gl = gl; + this._id = Texture._nextId++; + + // Crée la texture WebGL + const texture = gl.createTexture(); + if (!texture) { + throw new Error('Impossible de créer la texture WebGL'); + } + this._texture = texture; + } + + /** + * Charge une texture depuis une image HTML + */ + loadFromImage(image: HTMLImageElement): void { + if (!this._texture) { + throw new Error('Texture détruite'); + } + + this._width = image.width; + this._height = image.height; + + this._gl.bindTexture(this._gl.TEXTURE_2D, this._texture); + + // Upload l'image vers le GPU + this._gl.texImage2D( + this._gl.TEXTURE_2D, + 0, + this._gl.RGBA, + this._gl.RGBA, + this._gl.UNSIGNED_BYTE, + image + ); + + // Configure le filtrage + this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_MIN_FILTER, this._filterMode); + this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_MAG_FILTER, this._filterMode); + + // Configure le wrapping + this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_WRAP_S, this._wrapMode); + this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_WRAP_T, this._wrapMode); + + this._gl.bindTexture(this._gl.TEXTURE_2D, null); + } + + /** + * Charge une texture depuis une URL (asynchrone) + */ + static async loadFromURL(gl: WebGLRenderingContext, url: string): Promise { + return new Promise((resolve, reject) => { + const image = new Image(); + image.crossOrigin = 'anonymous'; + + image.onload = () => { + const texture = new Texture(gl); + texture.loadFromImage(image); + resolve(texture); + }; + + image.onerror = () => { + reject(new Error(`Impossible de charger l'image: ${url}`)); + }; + + image.src = url; + }); + } + + /** + * Crée une texture vide (utile pour rendu vers texture) + */ + createEmpty(width: number, height: number): void { + if (!this._texture) { + throw new Error('Texture détruite'); + } + + this._width = width; + this._height = height; + + this._gl.bindTexture(this._gl.TEXTURE_2D, this._texture); + + this._gl.texImage2D( + this._gl.TEXTURE_2D, + 0, + this._gl.RGBA, + width, + height, + 0, + this._gl.RGBA, + this._gl.UNSIGNED_BYTE, + null + ); + + this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_MIN_FILTER, this._filterMode); + this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_MAG_FILTER, this._filterMode); + this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_WRAP_S, this._wrapMode); + this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_WRAP_T, this._wrapMode); + + this._gl.bindTexture(this._gl.TEXTURE_2D, null); + } + + /** + * Active la texture sur un slot (0-31) + */ + bind(slot: number = 0): void { + if (!this._texture) { + throw new Error('Texture détruite'); + } + + const gl = this._gl; + gl.activeTexture(gl.TEXTURE0 + slot); + gl.bindTexture(gl.TEXTURE_2D, this._texture); + } + + /** + * Désactive la texture + */ + unbind(): void { + this._gl.bindTexture(this._gl.TEXTURE_2D, null); + } + + /** + * Définit le mode de filtrage + */ + setFilterMode(mode: FilterMode): void { + if (!this._texture) { + throw new Error('Texture détruite'); + } + + this._filterMode = mode; + this._gl.bindTexture(this._gl.TEXTURE_2D, this._texture); + this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_MIN_FILTER, mode); + this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_MAG_FILTER, mode); + this._gl.bindTexture(this._gl.TEXTURE_2D, null); + } + + /** + * Définit le mode de wrapping + */ + setWrapMode(mode: WrapMode): void { + if (!this._texture) { + throw new Error('Texture détruite'); + } + + this._wrapMode = mode; + this._gl.bindTexture(this._gl.TEXTURE_2D, this._texture); + this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_WRAP_S, mode); + this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_WRAP_T, mode); + this._gl.bindTexture(this._gl.TEXTURE_2D, null); + } + + /** + * Largeur de la texture + */ + get width(): number { + return this._width; + } + + /** + * Hauteur de la texture + */ + get height(): number { + return this._height; + } + + /** + * Identifiant unique de la texture + */ + get id(): number { + return this._id; + } + + /** + * Texture WebGL native + */ + get glTexture(): WebGLTexture | null { + return this._texture; + } + + /** + * Libère la mémoire GPU + */ + dispose(): void { + if (this._texture) { + this._gl.deleteTexture(this._texture); + this._texture = null; + this._width = 0; + this._height = 0; + } + } +} + diff --git a/src/engine/rendering/WebGLRenderer.ts b/src/engine/rendering/WebGLRenderer.ts new file mode 100644 index 0000000..2727e3e --- /dev/null +++ b/src/engine/rendering/WebGLRenderer.ts @@ -0,0 +1,313 @@ +/** + * WebGLRenderer - Coordonne le rendu + * Chef d'orchestre du système de rendu + */ + +import { GLContext } from '../webgl/GLContext'; +import { Shader } from './Shader'; +import { Texture } from './Texture'; +import { Camera } from './Camera'; +import { Color } from '../math/Color'; +import { Scene } from '../core/Scene'; +import { SpriteBatch } from './SpriteBatch'; + +export class WebGLRenderer { + private _glContext: GLContext; + private _spriteBatch: SpriteBatch; + private _shaderLibrary: Map = new Map(); + private _textureCache: Map = new Map(); + private _defaultShader: Shader | null = null; + private _currentShader: Shader | null = null; + private _currentTexture: Texture | null = null; + private _viewportWidth: number = 800; + private _viewportHeight: number = 600; + + constructor(glContext: GLContext) { + this._glContext = glContext; + this._viewportWidth = glContext.canvas.width; + this._viewportHeight = glContext.canvas.height; + this._spriteBatch = new SpriteBatch(glContext); + } + + /** + * Initialise le renderer et crée le shader par défaut + */ + initialize(): boolean { + // Crée le shader sprite par défaut + const vertexShader = ` + attribute vec2 aPosition; + attribute vec2 aTexCoord; + attribute vec4 aColor; + + uniform mat3 uProjection; + uniform mat3 uView; + + varying vec2 vTexCoord; + varying vec4 vColor; + + void main() { + vec3 pos = uProjection * uView * vec3(aPosition, 1.0); + gl_Position = vec4(pos.xy, 0.0, 1.0); + vTexCoord = aTexCoord; + vColor = aColor; + } + `; + + const fragmentShader = ` + precision mediump float; + + uniform sampler2D uTexture; + + varying vec2 vTexCoord; + varying vec4 vColor; + + void main() { + vec4 texColor = texture2D(uTexture, vTexCoord); + gl_FragColor = texColor * vColor; + } + `; + + const shader = new Shader(); + if (!shader.compile(this._glContext.gl, vertexShader, fragmentShader)) { + console.error('Échec de la compilation du shader par défaut'); + return false; + } + + this._defaultShader = shader; + this._currentShader = shader; + this.loadShader('default', vertexShader, fragmentShader); + + return true; + } + + /** + * Charge et enregistre un shader + */ + loadShader(name: string, vertexSource: string, fragmentSource: string): Shader | null { + // Vérifie si le shader existe déjà + if (this._shaderLibrary.has(name)) { + console.warn(`Shader "${name}" déjà chargé`); + return this._shaderLibrary.get(name) || null; + } + + const shader = new Shader(); + if (!shader.compile(this._glContext.gl, vertexSource, fragmentSource)) { + console.error(`Échec de la compilation du shader "${name}"`); + return null; + } + + this._shaderLibrary.set(name, shader); + return shader; + } + + /** + * Récupère un shader par son nom + */ + getShader(name: string): Shader | null { + return this._shaderLibrary.get(name) || null; + } + + /** + * Charge une texture depuis une URL et la met en cache + */ + async loadTexture(name: string, url: string): Promise { + // Vérifie si la texture est déjà en cache + if (this._textureCache.has(name)) { + console.warn(`Texture "${name}" déjà chargée`); + return this._textureCache.get(name) || null; + } + + try { + const texture = await Texture.loadFromURL(this._glContext.gl, url); + this._textureCache.set(name, texture); + return texture; + } catch (error) { + console.error(`Échec du chargement de la texture "${name}":`, error); + return null; + } + } + + /** + * Récupère une texture par son nom + */ + getTexture(name: string): Texture | null { + return this._textureCache.get(name) || null; + } + + /** + * Efface le canvas avec une couleur + */ + clear(color?: Color): void { + if (color) { + this._glContext.gl.clearColor(color.r, color.g, color.b, color.a); + } + this._glContext.clear(); + } + + /** + * Active un shader + */ + useShader(shader: Shader | string | null): void { + let targetShader: Shader | null = null; + + if (shader === null) { + targetShader = this._defaultShader; + } else if (typeof shader === 'string') { + targetShader = this.getShader(shader); + if (!targetShader) { + console.warn(`Shader "${shader}" introuvable, utilisation du shader par défaut`); + targetShader = this._defaultShader; + } + } else { + targetShader = shader; + } + + if (targetShader && targetShader !== this._currentShader) { + targetShader.use(this._glContext.gl); + this._currentShader = targetShader; + } + } + + /** + * Active une texture sur le slot 0 + */ + useTexture(texture: Texture | null): void { + if (texture && texture !== this._currentTexture) { + texture.bind(0); + this._currentTexture = texture; + } else if (!texture) { + this._glContext.gl.bindTexture(this._glContext.gl.TEXTURE_2D, null); + this._currentTexture = null; + } + } + + /** + * Rend une scène avec une caméra + */ + render(scene: Scene, camera: Camera): void { + // Met à jour la taille du viewport si nécessaire + if (camera.viewportWidth !== this._viewportWidth || + camera.viewportHeight !== this._viewportHeight) { + this.resize(camera.viewportWidth, camera.viewportHeight); + } + + // Clear avec fond blanc + this.clear(Color.white); + + // Récupère les matrices de la caméra + const proj = camera.getProjectionMatrix(); + const view = camera.getViewMatrix(); + + // S'assure que le shader par défaut existe + if (!this._defaultShader) { + console.error('Aucun shader par défaut disponible'); + return; + } + + // Commence le batch avec les matrices de caméra et le shader actif + // Les uniforms seront définis dans SpriteBatch.flush() après l'activation du shader + this._spriteBatch.begin(proj, view, this._defaultShader); + + // Collecte et rend tous les SpriteRenderer de la scène + const renderables = scene.getAllRenderables(); + for (const renderable of renderables) { + renderable.render(this._spriteBatch); + } + + // Termine le batch (flush automatique) + this._spriteBatch.end(); + + // Rend l'UI en overlay (par-dessus la scène) + const uiCanvas = scene.uiCanvas; + if (uiCanvas) { + const uiCamera = uiCanvas.camera; + const uiProj = uiCamera.getProjectionMatrix(); + const uiView = uiCamera.getViewMatrix(); + + // Commence un nouveau batch pour l'UI + this._spriteBatch.begin(uiProj, uiView, this._defaultShader); + + // Rend tous les éléments UI + uiCanvas.render(this._spriteBatch); + + // Termine le batch UI + this._spriteBatch.end(); + } + } + + /** + * Redimensionne le viewport + */ + resize(width: number, height: number): void { + this._viewportWidth = width; + this._viewportHeight = height; + this._glContext.resize(width, height); + } + + /** + * Récupère le contexte WebGL + */ + get gl(): WebGLRenderingContext { + return this._glContext.gl; + } + + /** + * Récupère le GLContext + */ + get glContext(): GLContext { + return this._glContext; + } + + /** + * Largeur du viewport + */ + get viewportWidth(): number { + return this._viewportWidth; + } + + /** + * Hauteur du viewport + */ + get viewportHeight(): number { + return this._viewportHeight; + } + + /** + * Shader par défaut + */ + get defaultShader(): Shader | null { + return this._defaultShader; + } + + /** + * SpriteBatch (pour accès externe si nécessaire) + */ + get spriteBatch(): SpriteBatch { + return this._spriteBatch; + } + + /** + * Nettoie les ressources + */ + dispose(): void { + // Nettoie tous les shaders + for (const shader of this._shaderLibrary.values()) { + shader.dispose(this._glContext.gl); + } + this._shaderLibrary.clear(); + + // Nettoie toutes les textures + for (const texture of this._textureCache.values()) { + texture.dispose(); + } + this._textureCache.clear(); + + this._defaultShader = null; + this._currentShader = null; + this._currentTexture = null; + + // Nettoie le SpriteBatch + this._spriteBatch.dispose(); + } +} + diff --git a/src/engine/ui/RectTransform.ts b/src/engine/ui/RectTransform.ts new file mode 100644 index 0000000..216cfc9 --- /dev/null +++ b/src/engine/ui/RectTransform.ts @@ -0,0 +1,250 @@ +/** + * RectTransform - Gestion du positionnement UI avec anchors + * Similaire à Unity RectTransform mais simplifié + */ + +import { Vector2 } from '../math/Vector2'; +import { Rect } from '../math/Rect'; + +export enum AnchorPreset { + TopLeft, + TopCenter, + TopRight, + MiddleLeft, + MiddleCenter, + MiddleRight, + BottomLeft, + BottomCenter, + BottomRight, + StretchHorizontal, + StretchVertical, + StretchAll +} + +export class RectTransform { + private _anchoredPosition: Vector2 = new Vector2(0, 0); + private _size: Vector2 = new Vector2(100, 100); + private _anchorMin: Vector2 = new Vector2(0, 0); // 0-1 (coin bas-gauche de l'anchor) + private _anchorMax: Vector2 = new Vector2(0, 0); // 0-1 (coin haut-droite de l'anchor) + private _pivot: Vector2 = new Vector2(0.5, 0.5); // 0-1 (point de référence pour rotation/scale) + + // Référence au parent (null = écran) + private _parent: RectTransform | null = null; + private _canvasSize: Vector2 = new Vector2(800, 600); + + /** + * Position ancrée (offset depuis l'anchor) + */ + get anchoredPosition(): Vector2 { + return this._anchoredPosition; + } + + set anchoredPosition(value: Vector2) { + this._anchoredPosition = value.clone(); + } + + /** + * Taille du rectangle (width, height) + */ + get size(): Vector2 { + return this._size; + } + + set size(value: Vector2) { + this._size = value.clone(); + } + + /** + * Largeur + */ + get width(): number { + return this._size.x; + } + + set width(value: number) { + this._size.x = value; + } + + /** + * Hauteur + */ + get height(): number { + return this._size.y; + } + + set height(value: number) { + this._size.y = value; + } + + /** + * Anchor minimum (coin bas-gauche, 0-1) + */ + get anchorMin(): Vector2 { + return this._anchorMin; + } + + set anchorMin(value: Vector2) { + this._anchorMin = value.clone(); + } + + /** + * Anchor maximum (coin haut-droite, 0-1) + */ + get anchorMax(): Vector2 { + return this._anchorMax; + } + + set anchorMax(value: Vector2) { + this._anchorMax = value.clone(); + } + + /** + * Pivot (point de référence, 0-1) + */ + get pivot(): Vector2 { + return this._pivot; + } + + set pivot(value: Vector2) { + this._pivot = value.clone(); + } + + /** + * Parent RectTransform (null = écran/canvas) + */ + get parent(): RectTransform | null { + return this._parent; + } + + set parent(value: RectTransform | null) { + this._parent = value; + } + + /** + * Taille du canvas (pour calculer les positions) + */ + get canvasSize(): Vector2 { + return this._canvasSize; + } + + set canvasSize(value: Vector2) { + this._canvasSize = value.clone(); + } + + /** + * Retourne le rectangle écran final (position absolue à l'écran) + */ + getRect(): Rect { + // Calcule la position de l'anchor dans l'espace écran + let anchorX: number; + let anchorY: number; + + if (this._parent) { + const parentRect = this._parent.getRect(); + anchorX = parentRect.left + (this._anchorMin.x * parentRect.width); + anchorY = parentRect.top + (this._anchorMin.y * parentRect.height); + } else { + // Relatif au canvas + anchorX = this._anchorMin.x * this._canvasSize.x; + anchorY = this._anchorMin.y * this._canvasSize.y; + } + + // Position finale = anchor + offset + const x = anchorX + this._anchoredPosition.x; + const y = anchorY + this._anchoredPosition.y; + + // Pour WebGL: Y vers le haut = négatif, donc on inverse + // Si anchorMin.y = 1 (haut), on veut y proche de canvasSize.y + // Si anchorMin.y = 0 (bas), on veut y proche de 0 + // Mais dans notre système, Y+ = bas, donc on inverse + const screenY = this._canvasSize.y - y - this._size.y; + + return new Rect(x, screenY, this._size.x, this._size.y); + } + + /** + * Position absolue dans l'espace écran (coin haut-gauche pour WebGL) + */ + get screenPosition(): Vector2 { + const rect = this.getRect(); + return new Vector2(rect.left, rect.top); + } + + /** + * Centre du rectangle dans l'espace écran + */ + get center(): Vector2 { + const rect = this.getRect(); + return rect.center; + } + + /** + * Configure l'anchor avec un preset + * Note: Dans WebGL, Y=0 est en bas, donc on inverse par rapport à l'écran + */ + setAnchorPreset(preset: AnchorPreset): void { + switch (preset) { + case AnchorPreset.TopLeft: + this._anchorMin = new Vector2(0, 1); // 1 = haut (WebGL) + this._anchorMax = new Vector2(0, 1); + this._pivot = new Vector2(0, 1); + break; + case AnchorPreset.TopCenter: + this._anchorMin = new Vector2(0.5, 1); + this._anchorMax = new Vector2(0.5, 1); + this._pivot = new Vector2(0.5, 1); + break; + case AnchorPreset.TopRight: + this._anchorMin = new Vector2(1, 1); + this._anchorMax = new Vector2(1, 1); + this._pivot = new Vector2(1, 1); + break; + case AnchorPreset.MiddleLeft: + this._anchorMin = new Vector2(0, 0.5); + this._anchorMax = new Vector2(0, 0.5); + this._pivot = new Vector2(0, 0.5); + break; + case AnchorPreset.MiddleCenter: + this._anchorMin = new Vector2(0.5, 0.5); + this._anchorMax = new Vector2(0.5, 0.5); + this._pivot = new Vector2(0.5, 0.5); + break; + case AnchorPreset.MiddleRight: + this._anchorMin = new Vector2(1, 0.5); + this._anchorMax = new Vector2(1, 0.5); + this._pivot = new Vector2(1, 0.5); + break; + case AnchorPreset.BottomLeft: + this._anchorMin = new Vector2(0, 0); // 0 = bas (WebGL) + this._anchorMax = new Vector2(0, 0); + this._pivot = new Vector2(0, 0); + break; + case AnchorPreset.BottomCenter: + this._anchorMin = new Vector2(0.5, 0); + this._anchorMax = new Vector2(0.5, 0); + this._pivot = new Vector2(0.5, 0); + break; + case AnchorPreset.BottomRight: + this._anchorMin = new Vector2(1, 0); + this._anchorMax = new Vector2(1, 0); + this._pivot = new Vector2(1, 0); + break; + case AnchorPreset.StretchHorizontal: + this._anchorMin = new Vector2(0, 0.5); + this._anchorMax = new Vector2(1, 0.5); + this._pivot = new Vector2(0.5, 0.5); + break; + case AnchorPreset.StretchVertical: + this._anchorMin = new Vector2(0.5, 0); + this._anchorMax = new Vector2(0.5, 1); + this._pivot = new Vector2(0.5, 0.5); + break; + case AnchorPreset.StretchAll: + this._anchorMin = new Vector2(0, 0); + this._anchorMax = new Vector2(1, 1); + this._pivot = new Vector2(0.5, 0.5); + break; + } + } +} + diff --git a/src/engine/ui/UIButton.ts b/src/engine/ui/UIButton.ts new file mode 100644 index 0000000..f270df5 --- /dev/null +++ b/src/engine/ui/UIButton.ts @@ -0,0 +1,264 @@ +/** + * UIButton - Bouton cliquable avec états + */ + +import { UIComponent } from './UIComponent'; +import { SpriteBatch } from '../rendering/SpriteBatch'; +import { Texture } from '../rendering/Texture'; +import { Color } from '../math/Color'; +import { Vector2 } from '../math/Vector2'; +import { InputManager } from '../input/InputManager'; +// WebGLRenderingContext est un type global + +export enum ButtonState { + Normal, + Hover, + Pressed, + Disabled +} + +export type ButtonCallback = () => void; + +export class UIButton extends UIComponent { + private _backgroundTexture: Texture | null = null; + private _normalColor: Color = new Color(0.2, 0.4, 0.8, 1.0); + private _hoverColor: Color = new Color(0.3, 0.5, 0.9, 1.0); + private _pressedColor: Color = new Color(0.1, 0.3, 0.7, 1.0); + private _disabledColor: Color = new Color(0.5, 0.5, 0.5, 0.5); + + private _state: ButtonState = ButtonState.Normal; + private _onClick: ButtonCallback | null = null; + private _inputManager: InputManager | null = null; + + private _wasPressed: boolean = false; + private _isHovering: boolean = false; + + private _gl: WebGLRenderingContext | WebGL2RenderingContext | null = null; + private _solidColorTexture: Texture | null = null; + + /** + * Texture de fond (optionnel, sinon utilise une couleur unie) + */ + get backgroundTexture(): Texture | null { + return this._backgroundTexture; + } + + set backgroundTexture(value: Texture | null) { + this._backgroundTexture = value; + } + + /** + * Couleur normale + */ + get normalColor(): Color { + return this._normalColor; + } + + set normalColor(value: Color) { + this._normalColor = value; + } + + /** + * Couleur au survol + */ + get hoverColor(): Color { + return this._hoverColor; + } + + set hoverColor(value: Color) { + this._hoverColor = value; + } + + /** + * Couleur quand pressé + */ + get pressedColor(): Color { + return this._pressedColor; + } + + set pressedColor(value: Color) { + this._pressedColor = value; + } + + /** + * État actuel du bouton + */ + get state(): ButtonState { + return this._state; + } + + /** + * Callback appelé quand le bouton est cliqué + */ + set onClick(callback: ButtonCallback | null) { + this._onClick = callback; + } + + /** + * Configure l'InputManager (nécessaire pour détecter les clics) + */ + setInputManager(inputManager: InputManager): void { + this._inputManager = inputManager; + } + + /** + * Initialise avec le contexte WebGL (pour créer les textures de couleur unie) + */ + initialize(gl: WebGLRenderingContext | WebGL2RenderingContext): void { + this._gl = gl; + } + + /** + * Crée une texture de couleur unie pour le fond + */ + private _createSolidColorTexture(color: Color): Texture | null { + if (!this._gl) { + return null; + } + + // Crée un canvas temporaire + const canvas = document.createElement('canvas'); + canvas.width = 1; + canvas.height = 1; + const ctx = canvas.getContext('2d'); + if (!ctx) { + return null; + } + + ctx.fillStyle = color.toRGBA(); + ctx.fillRect(0, 0, 1, 1); + + // Crée ou met à jour la texture + if (!this._solidColorTexture) { + this._solidColorTexture = new Texture(this._gl); + } + + // Upload directement depuis le canvas + if (!this._solidColorTexture || !this._gl) { + return null; + } + const gl = this._gl; + const texture = (this._solidColorTexture as any)._texture; + if (!texture) { + return null; + } + + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.bindTexture(gl.TEXTURE_2D, null); + + // Met à jour la taille + (this._solidColorTexture as any)._width = 1; + (this._solidColorTexture as any)._height = 1; + return this._solidColorTexture; + } + + /** + * Update pour détecter les interactions + */ + update(_deltaTime: number): void { + if (!this._inputManager || !this.gameObject.active || !this.enabled) { + return; + } + + const mousePos = this._inputManager.getMousePosition(); + const isMouseDown = this._inputManager.isMouseButtonDown(0); // Bouton gauche + + // Teste si la souris est sur le bouton + this._isHovering = this.containsPoint(mousePos); + + // Gère les états + if (this._state === ButtonState.Disabled) { + return; + } + + if (this._isHovering) { + if (isMouseDown) { + this._state = ButtonState.Pressed; + this._wasPressed = true; + } else { + if (this._wasPressed && this._state === ButtonState.Pressed) { + // Relâchement après avoir été pressé = clic + if (this._onClick) { + this._onClick(); + } + this._wasPressed = false; + } + this._state = ButtonState.Hover; + } + } else { + this._state = ButtonState.Normal; + this._wasPressed = false; + } + } + + /** + * Rend le bouton UI + */ + renderUI(spriteBatch: SpriteBatch): void { + if (!this.gameObject.active || !this.enabled) { + return; + } + + const rect = this.rectTransform.getRect(); + const position = new Vector2(rect.center.x, rect.center.y); + + // Choisit la couleur selon l'état + let tint: Color; + switch (this._state) { + case ButtonState.Hover: + tint = this._hoverColor; + break; + case ButtonState.Pressed: + tint = this._pressedColor; + break; + case ButtonState.Disabled: + tint = this._disabledColor; + break; + default: + tint = this._normalColor; + } + + // Si une texture est définie, l'utilise + if (this._backgroundTexture) { + spriteBatch.draw( + this._backgroundTexture, + position, + null, + tint, + 0, + new Vector2(rect.width / this._backgroundTexture.width, rect.height / this._backgroundTexture.height), + new Vector2(0.5, 0.5) + ); + } else { + // Sinon, crée un rectangle de couleur unie avec une texture 1x1 + const colorTexture = this._createSolidColorTexture(tint); + if (colorTexture) { + spriteBatch.draw( + colorTexture, + position, + null, + Color.white, // La couleur est déjà dans la texture + 0, + new Vector2(rect.width, rect.height), + new Vector2(0.5, 0.5) + ); + } + } + } + + /** + * Active ou désactive le bouton + */ + setEnabled(enabled: boolean): void { + this.enabled = enabled; + if (!enabled) { + this._state = ButtonState.Disabled; + } else { + this._state = ButtonState.Normal; + } + } +} + diff --git a/src/engine/ui/UICanvas.ts b/src/engine/ui/UICanvas.ts new file mode 100644 index 0000000..5ec14a7 --- /dev/null +++ b/src/engine/ui/UICanvas.ts @@ -0,0 +1,168 @@ +/** + * UICanvas - Système de gestion de l'interface utilisateur + * Collecte et rend tous les éléments UI + */ + +import { GameObject } from '../entities/GameObject'; +import { UIComponent } from './UIComponent'; +import { UIImage } from './UIImage'; +import { UIText } from './UIText'; +import { UIButton } from './UIButton'; +import { SpriteBatch } from '../rendering/SpriteBatch'; +import { Vector2 } from '../math/Vector2'; +import { Camera } from '../rendering/Camera'; + +export class UICanvas { + private _gameObjects: GameObject[] = []; + private _canvasSize: Vector2 = new Vector2(800, 600); + private _camera: Camera; + + constructor(canvasWidth: number = 800, canvasHeight: number = 600) { + this._canvasSize = new Vector2(canvasWidth, canvasHeight); + // Crée une caméra spéciale pour l'UI (pas de transformation, rendu direct à l'écran) + this._camera = new Camera(canvasWidth, canvasHeight); + this._camera.position = new Vector2(canvasWidth / 2, canvasHeight / 2); + } + + /** + * Taille du canvas UI + */ + get canvasSize(): Vector2 { + return this._canvasSize; + } + + set canvasSize(value: Vector2) { + this._canvasSize = value.clone(); + this._camera.resize(value.x, value.y); + this._camera.position = new Vector2(value.x / 2, value.y / 2); + + // Met à jour tous les UIComponents + for (const obj of this._gameObjects) { + const uiComponent = this._findUIComponent(obj); + if (uiComponent) { + uiComponent.updateCanvasSize(value); + } + } + } + + /** + * Caméra UI (pour le rendu) + */ + get camera(): Camera { + return this._camera; + } + + /** + * Ajoute un GameObject avec UIComponent + */ + addGameObject(gameObject: GameObject): void { + if (this._gameObjects.indexOf(gameObject) !== -1) { + console.warn(`GameObject "${gameObject.name}" déjà dans le UICanvas`); + return; + } + this._gameObjects.push(gameObject); + + // Met à jour la taille du canvas pour le UIComponent + const uiComponent = this._findUIComponent(gameObject); + if (uiComponent) { + uiComponent.updateCanvasSize(this._canvasSize); + } + } + + /** + * Retire un GameObject + */ + removeGameObject(gameObject: GameObject): void { + const index = this._gameObjects.indexOf(gameObject); + if (index !== -1) { + this._gameObjects.splice(index, 1); + } + } + + /** + * Update tous les GameObjects UI (pour les boutons, etc.) + */ + update(deltaTime: number): void { + for (const obj of this._gameObjects) { + if (obj.active && !obj.isDestroyed) { + obj.update(deltaTime); + } + } + + // Nettoie les objets détruits + this._gameObjects = this._gameObjects.filter(obj => !obj.isDestroyed); + } + + /** + * Rend tous les éléments UI + */ + render(spriteBatch: SpriteBatch): void { + // Collecte tous les UIComponents actifs + const uiComponents: UIComponent[] = []; + + for (const obj of this._gameObjects) { + if (!obj.active || obj.isDestroyed) { + continue; + } + + // Cherche les UIComponents dans cet objet et ses enfants + this._collectUIComponentsRecursive(obj, uiComponents); + } + + // Trie par layer si nécessaire (pour l'instant on garde l'ordre d'ajout) + // Rend tous les éléments UI + for (const component of uiComponents) { + if (component.enabled) { + component.renderUI(spriteBatch); + } + } + } + + /** + * Trouve un UIComponent dans un GameObject + */ + private _findUIComponent(obj: GameObject): UIComponent | null { + const uiImage = obj.getComponent(UIImage); + if (uiImage) return uiImage; + + const uiText = obj.getComponent(UIText); + if (uiText) return uiText; + + const uiButton = obj.getComponent(UIButton); + if (uiButton) return uiButton; + + return null; + } + + /** + * Collecte récursivement tous les UIComponents + */ + private _collectUIComponentsRecursive(obj: GameObject, components: UIComponent[]): void { + if (!obj.active || obj.isDestroyed) { + return; + } + + // Cherche les UIComponents dans cet objet + const uiImage = obj.getComponent(UIImage); + if (uiImage && uiImage.enabled) components.push(uiImage); + + const uiText = obj.getComponent(UIText); + if (uiText && uiText.enabled) components.push(uiText); + + const uiButton = obj.getComponent(UIButton); + if (uiButton && uiButton.enabled) components.push(uiButton); + + // Parcourt les enfants + for (const child of obj.children) { + this._collectUIComponentsRecursive(child, components); + } + } + + /** + * Liste de tous les GameObjects UI + */ + get gameObjects(): readonly GameObject[] { + return this._gameObjects; + } +} + diff --git a/src/engine/ui/UIComponent.ts b/src/engine/ui/UIComponent.ts new file mode 100644 index 0000000..a86f53f --- /dev/null +++ b/src/engine/ui/UIComponent.ts @@ -0,0 +1,52 @@ +/** + * UIComponent - Classe de base pour tous les éléments UI + * Gère le RectTransform et le rendu UI + */ + +import { Component } from '../entities/Component'; +import { RectTransform } from './RectTransform'; +import { SpriteBatch } from '../rendering/SpriteBatch'; +import { Vector2 } from '../math/Vector2'; + +export abstract class UIComponent extends Component { + private _rectTransform: RectTransform; + + constructor() { + super(); + this._rectTransform = new RectTransform(); + } + + /** + * RectTransform pour le positionnement UI + */ + get rectTransform(): RectTransform { + return this._rectTransform; + } + + /** + * Méthode abstraite : Rend l'élément UI + */ + abstract renderUI(spriteBatch: SpriteBatch): void; + + /** + * Met à jour la taille du canvas + */ + updateCanvasSize(canvasSize: Vector2): void { + this._rectTransform.canvasSize = canvasSize; + } + + /** + * Teste si un point (en coordonnées écran) est dans cet élément UI + */ + containsPoint(screenPoint: Vector2): boolean { + const rect = this._rectTransform.getRect(); + // Convertit le point écran en coordonnées UI + // WebGL: Y vers le haut = négatif, donc on inverse + const uiY = this._rectTransform.canvasSize.y - screenPoint.y; + return screenPoint.x >= rect.left && + screenPoint.x <= rect.right && + uiY >= rect.top && + uiY <= rect.bottom; + } +} + diff --git a/src/engine/ui/UIImage.ts b/src/engine/ui/UIImage.ts new file mode 100644 index 0000000..f2bbf79 --- /dev/null +++ b/src/engine/ui/UIImage.ts @@ -0,0 +1,139 @@ +/** + * UIImage - Image d'interface utilisateur + */ + +import { UIComponent } from './UIComponent'; +import { SpriteBatch } from '../rendering/SpriteBatch'; +import { Texture } from '../rendering/Texture'; +import { Color } from '../math/Color'; +import { Rect } from '../math/Rect'; +import { Vector2 } from '../math/Vector2'; + +export class UIImage extends UIComponent { + private _texture: Texture | null = null; + private _tint: Color = Color.white; + private _sourceRect: Rect | null = null; + private _shadowEnabled: boolean = false; + private _shadowOffset: Vector2 = new Vector2(2, 2); + private _shadowColor: Color = new Color(0, 0, 0, 0.5); + + /** + * Texture à afficher + */ + get texture(): Texture | null { + return this._texture; + } + + set texture(value: Texture | null) { + this._texture = value; + if (value && !this._sourceRect) { + this._sourceRect = new Rect(0, 0, value.width, value.height); + } + } + + /** + * Couleur de teinte + */ + get tint(): Color { + return this._tint; + } + + set tint(value: Color) { + this._tint = value; + } + + /** + * Rectangle source (pour afficher une partie de la texture) + */ + get sourceRect(): Rect | null { + return this._sourceRect; + } + + set sourceRect(value: Rect | null) { + this._sourceRect = value; + } + + /** + * Active/désactive l'ombre + */ + get shadowEnabled(): boolean { + return this._shadowEnabled; + } + + set shadowEnabled(value: boolean) { + this._shadowEnabled = value; + } + + /** + * Décalage de l'ombre (en pixels) + */ + get shadowOffset(): Vector2 { + return this._shadowOffset; + } + + set shadowOffset(value: Vector2) { + this._shadowOffset = value; + } + + /** + * Couleur de l'ombre + */ + get shadowColor(): Color { + return this._shadowColor; + } + + set shadowColor(value: Color) { + this._shadowColor = value; + } + + /** + * Rend l'image UI + */ + renderUI(spriteBatch: SpriteBatch): void { + if (!this._texture || !this.gameObject.active || !this.enabled) { + return; + } + + const rect = this.rectTransform.getRect(); + const position = new Vector2(rect.center.x, rect.center.y); + const sourceRect = this._sourceRect || new Rect(0, 0, this._texture.width, this._texture.height); + + // Calcule le scale pour que l'image s'adapte à la taille du rectTransform + const scaleX = rect.width / sourceRect.width; + const scaleY = rect.height / sourceRect.height; + const scale = new Vector2(scaleX, scaleY); + + // Utilise la rotation du GameObject (converti en radians) + const rotation = (this.gameObject.transform.rotation * Math.PI) / 180; + + // Dessine l'ombre en premier si activée + if (this._shadowEnabled) { + const shadowPosition = new Vector2( + position.x + this._shadowOffset.x, + position.y + this._shadowOffset.y + ); + + spriteBatch.draw( + this._texture, + shadowPosition, + sourceRect, + this._shadowColor, + rotation, + scale, + new Vector2(0.5, 0.5) // Pivot au centre + ); + } + + // Dessine l'image normale par-dessus + spriteBatch.draw( + this._texture, + position, + sourceRect, + this._tint, + rotation, + scale, + new Vector2(0.5, 0.5) // Pivot au centre + ); + } +} + diff --git a/src/engine/ui/UIText.ts b/src/engine/ui/UIText.ts new file mode 100644 index 0000000..e8b8cd0 --- /dev/null +++ b/src/engine/ui/UIText.ts @@ -0,0 +1,224 @@ +/** + * UIText - Affichage de texte UI + * Génère une texture de texte à partir d'un canvas 2D + */ + +import { UIComponent } from './UIComponent'; +import { SpriteBatch } from '../rendering/SpriteBatch'; +import { Texture } from '../rendering/Texture'; +import { Color } from '../math/Color'; +import { Vector2 } from '../math/Vector2'; + +export class UIText extends UIComponent { + private _text: string = ''; + private _fontSize: number = 16; + private _fontFamily: string = 'Arial'; + private _color: Color = Color.white; + private _textAlign: CanvasTextAlign = 'left'; + private _textBaseline: CanvasTextBaseline = 'top'; + + private _texture: Texture | null = null; + private _gl: WebGLRenderingContext | WebGL2RenderingContext | null = null; + private _dirty: boolean = true; + + /** + * Texte à afficher + */ + get text(): string { + return this._text; + } + + set text(value: string) { + if (this._text !== value) { + this._text = value; + this._dirty = true; + } + } + + /** + * Taille de la police + */ + get fontSize(): number { + return this._fontSize; + } + + set fontSize(value: number) { + if (this._fontSize !== value) { + this._fontSize = value; + this._dirty = true; + } + } + + /** + * Famille de police + */ + get fontFamily(): string { + return this._fontFamily; + } + + set fontFamily(value: string) { + if (this._fontFamily !== value) { + this._fontFamily = value; + this._dirty = true; + } + } + + /** + * Couleur du texte + */ + get color(): Color { + return this._color; + } + + set color(value: Color) { + this._color = value; + this._dirty = true; + } + + /** + * Alignement du texte + */ + get textAlign(): CanvasTextAlign { + return this._textAlign; + } + + set textAlign(value: CanvasTextAlign) { + if (this._textAlign !== value) { + this._textAlign = value; + this._dirty = true; + } + } + + /** + * Initialise avec le contexte WebGL (nécessaire pour créer les textures) + */ + initialize(gl: WebGLRenderingContext | WebGL2RenderingContext): void { + this._gl = gl; + this._dirty = true; + } + + /** + * Génère la texture de texte depuis le canvas 2D + */ + private _generateTexture(): void { + if (!this._gl || this._text === '') { + return; + } + + // Crée un canvas temporaire pour mesurer et dessiner le texte + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + if (!ctx) { + return; + } + + // Configure la police + ctx.font = `${this._fontSize}px ${this._fontFamily}`; + ctx.textAlign = this._textAlign; + ctx.textBaseline = this._textBaseline; + ctx.fillStyle = this._color.toRGBA(); + + // Mesure le texte + const metrics = ctx.measureText(this._text); + const textWidth = Math.ceil(metrics.width); + const textHeight = this._fontSize; + + // Ajuste la taille du canvas + canvas.width = Math.max(1, textWidth); + canvas.height = Math.max(1, textHeight); + + // Réapplique les paramètres (perdus après resize) + ctx.font = `${this._fontSize}px ${this._fontFamily}`; + ctx.textAlign = this._textAlign; + ctx.textBaseline = this._textBaseline; + ctx.fillStyle = this._color.toRGBA(); + + // Dessine le texte + const x = this._textAlign === 'center' ? canvas.width / 2 : + this._textAlign === 'right' ? canvas.width : 0; + const y = this._textBaseline === 'middle' ? canvas.height / 2 : + this._textBaseline === 'bottom' || this._textBaseline === 'alphabetic' ? canvas.height : 0; + + ctx.fillText(this._text, x, y); + + // Crée ou met à jour la texture + if (!this._texture) { + this._texture = new Texture(this._gl); + } + + // Upload directement depuis le canvas (comme dans createSolidColorTexture) + if (!this._texture) { + throw new Error('Texture non créée'); + } + const gl = this._gl; + const texture = (this._texture as any)._texture; + if (!texture) { + throw new Error('Texture WebGL non créée'); + } + + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.bindTexture(gl.TEXTURE_2D, null); + + // Met à jour la taille + (this._texture as any)._width = canvas.width; + (this._texture as any)._height = canvas.height; + + // Met à jour la taille du rectTransform si nécessaire + if (this.rectTransform.width === 100 && this.rectTransform.height === 100) { + this.rectTransform.width = canvas.width; + this.rectTransform.height = canvas.height; + } + + this._dirty = false; + } + + /** + * Rend le texte UI + */ + renderUI(spriteBatch: SpriteBatch): void { + if (!this.gameObject.active || !this.enabled || this._text === '') { + return; + } + + // Régénère la texture si nécessaire + if (this._dirty || !this._texture) { + this._generateTexture(); + } + + if (!this._texture) { + return; + } + + const rect = this.rectTransform.getRect(); + const position = new Vector2(rect.center.x, rect.center.y); + + // Scale pour adapter au rectTransform si nécessaire + const scaleX = rect.width / this._texture.width; + const scaleY = rect.height / this._texture.height; + const scale = new Vector2(scaleX, scaleY); + + spriteBatch.draw( + this._texture, + position, + null, + Color.white, + 0, + scale, + new Vector2(0.5, 0.5) + ); + } + + /** + * Nettoie la texture + */ + onDestroy(): void { + if (this._texture) { + this._texture.dispose(); + this._texture = null; + } + } +} + diff --git a/src/engine/ui/index.ts b/src/engine/ui/index.ts new file mode 100644 index 0000000..87f82bb --- /dev/null +++ b/src/engine/ui/index.ts @@ -0,0 +1,11 @@ +/** + * Exports du système UI + */ + +export { UICanvas } from './UICanvas'; +export { UIComponent } from './UIComponent'; +export { RectTransform, AnchorPreset } from './RectTransform'; +export { UIImage } from './UIImage'; +export { UIText } from './UIText'; +export { UIButton, ButtonState, type ButtonCallback } from './UIButton'; + diff --git a/src/engine/webgl/Buffer.ts b/src/engine/webgl/Buffer.ts new file mode 100644 index 0000000..a6704dc --- /dev/null +++ b/src/engine/webgl/Buffer.ts @@ -0,0 +1,109 @@ +/** + * Buffer - Encapsule un buffer WebGL + * Gère les vertex buffers et index buffers + */ + +export enum BufferUsage { + STATIC_DRAW = WebGLRenderingContext.STATIC_DRAW, + DYNAMIC_DRAW = WebGLRenderingContext.DYNAMIC_DRAW, + STREAM_DRAW = WebGLRenderingContext.STREAM_DRAW +} + +export enum BufferType { + ARRAY_BUFFER = WebGLRenderingContext.ARRAY_BUFFER, + ELEMENT_ARRAY_BUFFER = WebGLRenderingContext.ELEMENT_ARRAY_BUFFER +} + +export class Buffer { + private _gl: WebGLRenderingContext; + private _buffer: WebGLBuffer | null = null; + private _type: number; + private _usage: number; + private _size: number = 0; + + constructor(gl: WebGLRenderingContext, type: BufferType = BufferType.ARRAY_BUFFER, usage: BufferUsage = BufferUsage.STATIC_DRAW) { + this._gl = gl; + this._type = type; + this._usage = usage; + this._buffer = gl.createBuffer(); + + if (!this._buffer) { + throw new Error('Impossible de créer le buffer WebGL'); + } + } + + /** + * Lie le buffer (active ce buffer) + */ + bind(): void { + if (!this._buffer) { + throw new Error('Buffer détruit'); + } + this._gl.bindBuffer(this._type, this._buffer); + } + + /** + * Délie le buffer + */ + unbind(): void { + this._gl.bindBuffer(this._type, null); + } + + /** + * Upload des données dans le buffer + */ + uploadData(data: Float32Array | Uint16Array | Uint8Array): void { + if (!this._buffer) { + throw new Error('Buffer détruit'); + } + + this.bind(); + this._gl.bufferData(this._type, data, this._usage); + this._size = data.length; + } + + /** + * Upload des données partielles (pour les buffers dynamiques) + */ + uploadSubData(data: Float32Array | Uint16Array | Uint8Array, offset: number = 0): void { + if (!this._buffer) { + throw new Error('Buffer détruit'); + } + + this.bind(); + this._gl.bufferSubData(this._type, offset, data); + } + + /** + * Récupère la taille du buffer (nombre d'éléments) + */ + get size(): number { + return this._size; + } + + /** + * Récupère le type du buffer + */ + get type(): number { + return this._type; + } + + /** + * Libère les ressources GPU + */ + dispose(): void { + if (this._buffer) { + this._gl.deleteBuffer(this._buffer); + this._buffer = null; + this._size = 0; + } + } + + /** + * Vérifie si le buffer est valide + */ + get isValid(): boolean { + return this._buffer !== null; + } +} + diff --git a/src/engine/webgl/GLContext.ts b/src/engine/webgl/GLContext.ts new file mode 100644 index 0000000..1953330 --- /dev/null +++ b/src/engine/webgl/GLContext.ts @@ -0,0 +1,80 @@ +/** + * GLContext - Abstraction WebGL + * Encapsule le contexte WebGL et simplifie l'API + */ + +export class GLContext { + private _gl: WebGLRenderingContext | null = null; + private _canvas: HTMLCanvasElement | null = null; + + /** + * Initialise le contexte WebGL depuis un canvas + */ + initialize(canvas: HTMLCanvasElement): boolean { + this._canvas = canvas; + + // Tentative de récupération du contexte WebGL + this._gl = canvas.getContext('webgl') || canvas.getContext('webgl2') as WebGLRenderingContext | null; + + if (!this._gl) { + console.error('Impossible d\'obtenir le contexte WebGL'); + return false; + } + + // Configuration WebGL de base + this.gl.enable(this.gl.BLEND); + this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA); + + // Fond noir par défaut + this.gl.clearColor(0.0, 0.0, 0.0, 1.0); + + return true; + } + + /** + * Récupère le contexte WebGL + */ + get gl(): WebGLRenderingContext { + if (!this._gl) { + throw new Error('GLContext non initialisé. Appelez initialize() d\'abord.'); + } + return this._gl; + } + + /** + * Récupère le canvas + */ + get canvas(): HTMLCanvasElement { + if (!this._canvas) { + throw new Error('Canvas non initialisé.'); + } + return this._canvas; + } + + /** + * Efface le buffer avec la couleur de fond + */ + clear(): void { + this.gl.clear(this.gl.COLOR_BUFFER_BIT); + } + + /** + * Redimensionne le viewport + */ + resize(width: number, height: number): void { + this.canvas.width = width; + this.canvas.height = height; + this.gl.viewport(0, 0, width, height); + } + + /** + * Vérifie les erreurs WebGL + */ + checkError(operation: string): void { + const error = this.gl.getError(); + if (error !== this.gl.NO_ERROR) { + console.error(`WebGL Error ${error} lors de: ${operation}`); + } + } +} + diff --git a/src/entities/classes/Archer.ts b/src/entities/classes/Archer.ts deleted file mode 100644 index a7ee791..0000000 --- a/src/entities/classes/Archer.ts +++ /dev/null @@ -1,74 +0,0 @@ -import Player from "../../models/Player"; -import {Arrow} from "../projectiles/Arrow"; -import {SpriteSheet} from "../../utils/SpriteSheet"; - -export default class Archer extends Player { - private loaded: boolean = false; - private ammunitions: number = 10; - private bullet: Arrow|null = null; - constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: any) { - super(x, y, context, canvas, state); - } - - public draw(): void { - super.draw(); - - let spriteSheet = new SpriteSheet(16, 16, 3, 3, this.weapon); - spriteSheet - .setFrame(1, 1, 1) - .setFrame(2, 2, 1) - .setFrame(3, 1, 2) - .setFrame(4, 2, 2) - ; - if (this.delay > this.limit) { - if (this.isFiring) { - this.loaded = false; - if (this.frame < spriteSheet.getFramesCount() - 1) { - this.frame++; - } - if (this.frame === spriteSheet.getFramesCount() - 1) { - this.loaded = true; - this.frame = spriteSheet.getFramesCount() - 1; - } - } else { - this.frame = 0; - } - this.delay = 0; - } else { - this.delay++; - } - this.context.save(); - this.context.translate(this.x, this.y); - this.context.rotate(this.angle); - spriteSheet.drawFrame(this.frame, this.context); - this.context.restore(); - } - - hookMouseUp() { - if (this.isFiring) { - if (this.loaded) { - this.ammunitions--; - this.bullet!.launch(); - this.bullet = null; - this.loaded = false; - this.isFiring = false; - this.frame = 0; - } - } - super.hookMouseUp(); - } - - hookMouseDown() { - if (!this.isFiring) { - this.isFiring = true; - if (!this.bullet) { - this.bullet = new Arrow(this.x, this.y, this.context, this.canvas, this.state); - this.state.addEntity(this.bullet); - } - } - super.hookMouseDown(); - } - - - -} \ No newline at end of file diff --git a/src/entities/classes/Warrior.ts b/src/entities/classes/Warrior.ts deleted file mode 100644 index f664092..0000000 --- a/src/entities/classes/Warrior.ts +++ /dev/null @@ -1,26 +0,0 @@ - -import State from "../../vendor/State"; -import Player from "../../models/Player"; -import {Slash} from "../projectiles/Slash"; - -export default class Warrior extends Player { - - constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { - super(x, y, context, canvas, state); - } - - public draw(): void { - this.context.beginPath(); - this.context.arc(this.x, this.y, this.radius, 0, Math.PI * 2); - this.context.strokeStyle = 'rgba(0, 0, 0, 1)'; - this.context.stroke(); - this.context.closePath(); - } - - hookMouseDown() { - this.state.addEntity(new Slash(this.x, this.y, this.context, this.canvas, this.state)); - super.hookMouseDown(); - } - - -} \ No newline at end of file diff --git a/src/entities/projectiles/Arrow.ts b/src/entities/projectiles/Arrow.ts deleted file mode 100644 index fa6f810..0000000 --- a/src/entities/projectiles/Arrow.ts +++ /dev/null @@ -1,76 +0,0 @@ -import State from "../../vendor/State"; -import {Projectile} from "../../models/Projectile"; - -export class Arrow extends Projectile { - constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { - super(x, y, 10, context, canvas, state); - this.initialize(); - } - - public initialize(): void { - this.loadSprite(); - } - - public draw(): void { - - const sx = 32; - const sy = 16 - const sw = 16; - const sh = 16; - const dx = -16; - const dy = -16; - const dh = 32; - const dw = 32; - // image de flèche - this.context.save(); - if (this.isLaunch) { - this.context.translate(this.x, this.y); - } else { - this.context.translate(this.state.player.x, this.state.player.y); - } - if (!this.isLaunch) { - this.context.rotate(this.angle); - } else { - this.context.rotate(this.launchAngle); - } - this.context.drawImage(this.sprite, sx, sy, sw, sh, dx, dy, dw, dh); - // rotation de la flèche - this.context.beginPath(); - this.context.restore(); - } - - public update(): void { - this.angle = Math.atan2(this.state.mouse.y - this.y, this.state.mouse.x - this.x); - this.draw(); - if (this.isLaunch) { - if (this.launchAngle === 0) { - this.launchAngle = this.angle; - this.angle = Math.atan2(this.velocity.y, this.velocity.x); - } - this.velocity.x = Math.cos(this.launchAngle) * 20; - this.velocity.y = Math.sin(this.launchAngle) * 20; - this.x += this.velocity.x; - this.y += this.velocity.y; - } else { - this.x = this.state.player.x; - this.y = this.state.player.y; - } - if (this.isOutOfBounds()) { - console.log("isOutOfBounds"); - this.state.removeEntity(this); - } - - } - - - - private loadSprite(): void { - this.sprite.src = "./src/sprites/bow.png"; - } - - launch(): void { - this.isLaunch = true; - } - - -} \ No newline at end of file diff --git a/src/entities/projectiles/Slash.ts b/src/entities/projectiles/Slash.ts deleted file mode 100644 index 24d0894..0000000 --- a/src/entities/projectiles/Slash.ts +++ /dev/null @@ -1,33 +0,0 @@ -import {Projectile} from "../../models/Projectile"; -import State from "../../vendor/State"; - -export class Slash extends Projectile { - constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { - super(x, y, 10, context, canvas, state); - this.angle = Math.atan2(this.state.mouse.y - this.state.player.y, this.state.mouse.x - this.state.player.x); - } - - public draw(): void { - this.context.fillStyle = 'rgb(0, 0, 255)'; - this.context.beginPath(); - // draw a shape with 4 lines connected by moveTo and lineTo - this.context.moveTo(this.x, this.y); - this.context.lineTo(this.x + 10, this.y + 10); - this.context.lineTo(this.x + 10, this.y - 10); - this.context.lineTo(this.x, this.y); - this.context.fill(); - - } - - public update(): void { - this.velocity.x = Math.cos(this.angle) * 20; - this.velocity.y = Math.sin(this.angle) * 20; - this.x += this.velocity.x; - this.y += this.velocity.y; - if (this.isOutOfBounds()) { - this.state.removeEntity(this); - } - this.draw(); - } - -} \ No newline at end of file diff --git a/src/game/Game.ts b/src/game/Game.ts new file mode 100644 index 0000000..26cf501 --- /dev/null +++ b/src/game/Game.ts @@ -0,0 +1,129 @@ +/** + * Game + * Point d'entrée principal du jeu CastleStorm + * Sépare la logique du jeu du moteur + */ + +import { Engine } from '../engine/core/Engine'; +import { Scene } from '../engine/core/Scene'; +import { Time } from '../engine/core/Time'; +import { GameScene } from './scenes/GameScene'; + +export class Game { + private engine: Engine; + private canvas: HTMLCanvasElement; + + constructor(canvas: HTMLCanvasElement) { + this.canvas = canvas; + + // Récupère la taille de la fenêtre + const width = window.innerWidth; + const height = window.innerHeight; + + console.log(`Initialisation du jeu : ${width}x${height}`); + + // Crée l'Engine + this.engine = new Engine(canvas, width, height); + + // Configure le gestionnaire de redimensionnement + this.setupWindowResize(); + } + + /** + * Initialise le jeu + */ + async initialize(): Promise { + console.log('=== Initialisation de CastleStorm ==='); + + // Initialise le moteur + if (!(await this.engine.initialize())) { + console.error('❌ Échec de l\'initialisation du moteur'); + return false; + } + + // Crée la scène de jeu + const gameScene = new GameScene(this.engine, window.innerWidth, window.innerHeight); + + // Charge les assets de la scène AVANT de l'enregistrer + await gameScene.onLoad(); + + // Enregistre et charge la scène + this.engine.registerScene('game', gameScene as unknown as Scene); + this.engine.loadScene('game'); + + console.log('✅ Jeu initialisé avec succès'); + return true; + } + + /** + * Démarre le jeu + */ + start(): void { + console.log('=== Démarrage du jeu ==='); + + // Configure l'affichage du FPS + this.setupFPSDisplay(); + + // Démarre le moteur + this.engine.start(); + + // Affiche les informations de contrôles + this.displayControls(); + } + + /** + * Configure le gestionnaire de redimensionnement automatique + */ + private setupWindowResize(): void { + window.addEventListener('resize', () => { + const newWidth = window.innerWidth; + const newHeight = window.innerHeight; + console.log(`Redimensionnement : ${newWidth}x${newHeight}`); + + // Redimensionne le canvas et le renderer + this.canvas.width = newWidth; + this.canvas.height = newHeight; + this.engine.renderer.resize(newWidth, newHeight); + + // Met à jour la caméra de la scène active + const activeScene = this.engine.getActiveScene(); + if (activeScene) { + activeScene.camera.resize(newWidth, newHeight); + } + }); + } + + /** + * Configure l'affichage du FPS + */ + private setupFPSDisplay(): void { + const fpsElement = document.getElementById('fps'); + if (fpsElement) { + setInterval(() => { + fpsElement.textContent = Math.round(Time.fps).toString(); + }, 200); + } + } + + /** + * Affiche les informations de contrôles dans la console + */ + private displayControls(): void { + console.log('===================================='); + console.log('🎮 CastleStorm - TopDownCharacter'); + console.log('===================================='); + console.log('✅ Sprites : 16x16 pixels, 4 frames par animation'); + console.log('✅ Animations : 8 walk, 8 roll, 4 slash (joueur + épée)'); + console.log('✅ Contrôles :'); + console.log(' 📍 ZQSD ou Flèches : Déplacement 8 directions'); + console.log(' Z+Q : Haut-Gauche | Z+D : Haut-Droite'); + console.log(' S+Q : Bas-Gauche | S+D : Bas-Droite'); + console.log(' ⚔️ Touche A : Attaque épée (slash)'); + console.log(' 🎯 Clic gauche : Tir projectile'); + console.log('✅ Physique : BoxCollider2D (16x16) + Rigidbody2D'); + console.log('✅ Épée : GameObject enfant, collider trigger (dégâts)'); + console.log('✅ Collisions : Ennemis, murs, pièces (triggers)'); + console.log('===================================='); + } +} + diff --git a/src/game/components/HealthComponent.ts b/src/game/components/HealthComponent.ts new file mode 100644 index 0000000..3d7476f --- /dev/null +++ b/src/game/components/HealthComponent.ts @@ -0,0 +1,112 @@ +/** + * HealthComponent - Gère les points de vie d'une entité + */ + +import { Component } from '../../engine/entities/Component'; + +export class HealthComponent extends Component { + private _maxHealth: number; + private _currentHealth: number; + private _onHealthChanged: ((current: number, max: number) => void) | null = null; + private _onDeath: (() => void) | null = null; + + constructor(maxHealth: number = 100) { + super(); + this._maxHealth = maxHealth; + this._currentHealth = maxHealth; + } + + /** + * Points de vie actuels + */ + get health(): number { + return this._currentHealth; + } + + set health(value: number) { + const oldHealth = this._currentHealth; + this._currentHealth = Math.max(0, Math.min(value, this._maxHealth)); + + if (oldHealth !== this._currentHealth) { + if (this._onHealthChanged) { + this._onHealthChanged(this._currentHealth, this._maxHealth); + } + + if (this._currentHealth <= 0 && this._onDeath) { + this._onDeath(); + } + } + } + + /** + * Points de vie maximum + */ + get maxHealth(): number { + return this._maxHealth; + } + + set maxHealth(value: number) { + this._maxHealth = Math.max(1, value); + // Ajuste la vie actuelle si elle dépasse le nouveau max + if (this._currentHealth > this._maxHealth) { + this.health = this._maxHealth; + } + } + + /** + * Pourcentage de vie (0-1) + */ + get healthPercent(): number { + return this._maxHealth > 0 ? this._currentHealth / this._maxHealth : 0; + } + + /** + * Est-ce que l'entité est morte ? + */ + get isDead(): boolean { + return this._currentHealth <= 0; + } + + /** + * Est-ce que l'entité est à pleine vie ? + */ + get isFullHealth(): boolean { + return this._currentHealth >= this._maxHealth; + } + + /** + * Inflige des dégâts + */ + damage(amount: number): void { + this.health -= amount; + } + + /** + * Soigne + */ + heal(amount: number): void { + this.health += amount; + } + + /** + * Remet à pleine vie + */ + fullHeal(): void { + this.health = this._maxHealth; + } + + /** + * Callback appelé quand la vie change + */ + onHealthChanged(callback: (current: number, max: number) => void): void { + this._onHealthChanged = callback; + } + + /** + * Callback appelé quand l'entité meurt + */ + onDeath(callback: () => void): void { + this._onDeath = callback; + } +} + diff --git a/src/game/components/PlayerController.ts b/src/game/components/PlayerController.ts new file mode 100644 index 0000000..489656e --- /dev/null +++ b/src/game/components/PlayerController.ts @@ -0,0 +1,491 @@ +/** + * PlayerController + * Component de contrôle du joueur - 8 DIRECTIONS + ATTAQUE + */ + +import { Component } from '../../engine/entities/Component'; +import { GameObject } from '../../engine/entities/GameObject'; +import { Vector2 } from '../../engine/math/Vector2'; +import { Color } from '../../engine/math/Color'; +import { Rect } from '../../engine/math/Rect'; +import { InputManager } from '../../engine/input/InputManager'; +import { Scene } from '../../engine/core/Scene'; +import { Camera } from '../../engine/rendering/Camera'; +import { Texture } from '../../engine/rendering/Texture'; +import { AssetLoader } from '../../engine/assets/AssetLoader'; +import { Rigidbody2D } from '../../engine/physics/Rigidbody2D'; +import { BoxCollider2D } from '../../engine/physics/BoxCollider2D'; +import { Animator } from '../../engine/components/Animator'; +import { SpriteRenderer } from '../../engine/components/SpriteRenderer'; +import { Projectile } from './Projectile'; +import { TextureGenerator } from '../utils/TextureGenerator'; + +export class PlayerController extends Component { + private speed: number = 200; // pixels par seconde + private inputManager: InputManager; + private assetLoader: AssetLoader | null = null; + private scene: Scene | null = null; + private lastShotTime: number = 0; + private shootCooldown: number = 0.3; // 0.3 secondes entre chaque tir + private projectileTexture: Texture | null = null; + private currentDirection: string = 'down'; // Direction actuelle pour les animations + + // === SYSTÈME D'ATTAQUE === + private isAttacking: boolean = false; + private attackDuration: number = 0.32; // Durée de l'animation slash (4 frames × 0.08s) + private attackTimer: number = 0; + private swordGameObject: GameObject | null = null; + + // === SYSTÈME DE DASH/ROLL === + private isDashing: boolean = false; + private dashDuration: number = 0.32; // Durée de l'animation roll (4 frames × 0.08s) + private dashTimer: number = 0; + private dashDistance: number = 150; // Distance du dash en pixels + private dashSpeed: number = 0; // Vitesse calculée du dash + private dashDirection: Vector2 = new Vector2(0, 0); // Direction du dash + + constructor(inputManager: InputManager, gl: WebGLRenderingContext | WebGL2RenderingContext, assetLoader: AssetLoader) { + super(); + this.inputManager = inputManager; + this.assetLoader = assetLoader; + // Crée la texture du projectile une seule fois + this.projectileTexture = TextureGenerator.createSolidColorTexture(gl, 8, 8, new Color(1.0, 1.0, 0.0, 1.0)); // Jaune + } + + // Getters publics pour le debug + get isPlayerAttacking(): boolean { return this.isAttacking; } + get isPlayerDashing(): boolean { return this.isDashing; } + get isPlayerMoving(): boolean { + const rb = this.getComponent(Rigidbody2D); + return rb ? rb.velocity.lengthSquared() > 0.1 : false; + } + get direction(): string { return this.currentDirection; } + + start(): void { + this.scene = this.gameObject.scene; + console.log('[PlayerController] start() appelé, scene:', this.scene ? 'ok' : 'null'); + console.log('[PlayerController] projectileTexture:', this.projectileTexture ? 'ok' : 'null'); + + // Récupère la référence à l'épée (enfant du joueur) + this.swordGameObject = this.gameObject.findChild('Sword'); + if (this.swordGameObject) { + console.log('✅ [PlayerController] Épée trouvée et liée'); + } else { + console.warn('⚠️ [PlayerController] Épée non trouvée !'); + } + } + + /** + * Calcule la direction en 8 points cardinaux selon l'angle vers le curseur + */ + private getDirectionFromMouse(camera: Camera): string { + const mouseWorldPos = this.inputManager.getMouseWorldPosition(camera); + const playerPos = this.transform.position; + + // Vecteur du joueur vers le curseur + const direction = mouseWorldPos.subtract(playerPos); + + // Si le curseur est trop proche, garde la direction actuelle + if (direction.lengthSquared() < 1) { + return this.currentDirection; + } + + // Calcule l'angle en radians (atan2 retourne [-π, π]) + const angle = Math.atan2(direction.y, direction.x); + + // Convertit l'angle en direction 8-way + return this.angleToDirection8Way(angle); + } + + /** + * Convertit un angle (radians) en direction 8-way + * Sans rotation du GameObject, le mapping est direct + */ + private angleToDirection8Way(angle: number): string { + // Normalise l'angle entre 0 et 2π + let normalizedAngle = angle; + if (normalizedAngle < 0) { + normalizedAngle += Math.PI * 2; + } + + // Convertit en degrés + const degrees = normalizedAngle * (180 / Math.PI); + + // En WebGL : Y+ = bas, Y- = haut, X+ = droite, X- = gauche + // atan2(y, x) retourne : 0° = droite, 90° = bas, 180° = gauche, 270° = haut + + // Divise le cercle en 8 secteurs de 45° chacun + // Inversion left/right pour correspondre aux sprites + if (degrees >= 337.5 || degrees < 22.5) { + return 'left'; // 0° = curseur à droite → sprite left + } else if (degrees >= 22.5 && degrees < 67.5) { + return 'downleft'; // 45° = curseur en bas-droite → sprite downleft + } else if (degrees >= 67.5 && degrees < 112.5) { + return 'down'; // 90° = curseur en bas + } else if (degrees >= 112.5 && degrees < 157.5) { + return 'downright'; // 135° = curseur en bas-gauche → sprite downright + } else if (degrees >= 157.5 && degrees < 202.5) { + return 'right'; // 180° = curseur à gauche → sprite right + } else if (degrees >= 202.5 && degrees < 247.5) { + return 'upright'; // 225° = curseur en haut-gauche → sprite upright + } else if (degrees >= 247.5 && degrees < 292.5) { + return 'up'; // 270° = curseur en haut + } else { + return 'upleft'; // 315° = curseur en haut-droite → sprite upleft + } + } + + /** + * Obtient la direction diagonale la plus proche pour l'attaque + * (Les attaques slash ne sont disponibles qu'en diagonale) + */ + private getAttackDirection(): string { + const dir = this.currentDirection; + + // Si déjà en diagonale, utilise directement + if (dir.includes('up') || dir.includes('down')) { + if (dir.includes('left') || dir.includes('right')) { + return dir; // Déjà diagonale (upleft, upright, downleft, downright) + } + } + + // Sinon, convertit les directions cardinales en diagonales par défaut + // (On choisit la diagonale droite par défaut, ajuste selon tes préférences) + if (dir === 'up') return 'upright'; + if (dir === 'down') return 'downright'; + if (dir === 'left') return 'downleft'; + if (dir === 'right') return 'downright'; + + return 'downright'; // Fallback + } + + update(deltaTime: number): void { + const rigidbody = this.getComponent(Rigidbody2D); + if (!rigidbody) { + return; // Pas de rigidbody, on ne peut pas bouger avec la physique + } + + // === GESTION DU TIMER D'ATTAQUE === + if (this.isAttacking) { + this.attackTimer -= deltaTime; + + // Fin de l'attaque + if (this.attackTimer <= 0) { + this.isAttacking = false; + // Désactive l'épée + if (this.swordGameObject) { + this.swordGameObject.active = false; + } + console.log('⚔️ Fin attaque'); + } + } + + // === GESTION DU TIMER DE DASH === + if (this.isDashing) { + this.dashTimer -= deltaTime; + + // Fin du dash + if (this.dashTimer <= 0) { + this.isDashing = false; + console.log('🏃 Fin dash'); + } + } + + // === DÉTECTION INPUT ATTAQUE (touche A uniquement) === + const attackInput = this.inputManager.isKeyDown('a') || this.inputManager.isKeyDown('A'); + + if (attackInput && !this.isAttacking && !this.isDashing) { + // Démarre l'attaque + this.startAttack(); + } + + // === DÉTECTION INPUT DASH (clic droit) === + const dashInput = this.inputManager.isMouseButtonDown(2); + + if (dashInput && !this.isDashing && !this.isAttacking) { + // Démarre le dash vers la direction du curseur + this.startDash(); + } + + // Calcule la direction du mouvement (BRUT, non normalisé pour détecter diagonales) + const movement = new Vector2(0, 0); + + // === BLOQUE LE MOUVEMENT PENDANT L'ATTAQUE OU LE DASH === + if (!this.isAttacking && !this.isDashing) { + // Déplacement horizontal + if (this.inputManager.isKeyHeld('ArrowLeft') || this.inputManager.isKeyHeld('q') || this.inputManager.isKeyHeld('Q')) { + movement.x -= 1; + } + if (this.inputManager.isKeyHeld('ArrowRight') || this.inputManager.isKeyHeld('d') || this.inputManager.isKeyHeld('D')) { + movement.x += 1; + } + + // Déplacement vertical (WebGL: Y positif = bas, Y négatif = haut) + if (this.inputManager.isKeyHeld('ArrowUp') || this.inputManager.isKeyHeld('z') || this.inputManager.isKeyHeld('Z')) { + movement.y -= 1; // Y- vers le haut (WebGL) + } + if (this.inputManager.isKeyHeld('ArrowDown') || this.inputManager.isKeyHeld('s') || this.inputManager.isKeyHeld('S')) { + movement.y += 1; // Y+ vers le bas (WebGL) + } + } + + const moved = movement.x !== 0 || movement.y !== 0; + + // Fait suivre la caméra au joueur (centrée) - on en a besoin plus tôt maintenant + const scene = this.gameObject.scene; + if (!scene) return; + + const camera = scene.camera; + camera.position = this.transform.position.clone(); + + // Détermine la direction selon la position du curseur (système twin-stick) + this.currentDirection = this.getDirectionFromMouse(camera); + + // === GESTION DE LA VÉLOCITÉ === + if (this.isDashing) { + // En dash : applique la vitesse de dash dans la direction stockée + rigidbody.velocity = this.dashDirection.multiply(this.dashSpeed); + } else if (moved && !this.isAttacking) { + // Mouvement normal + movement.normalizeMut(); + rigidbody.velocity = movement.multiply(this.speed); + } else { + // Immobile + rigidbody.velocity = new Vector2(0, 0); + } + + // Change l'animation selon le mouvement et la direction + const animator = this.getComponent(Animator); + const spriteRenderer = this.getComponent(SpriteRenderer); + + if (animator && spriteRenderer) { + if (this.isDashing) { + // === EN DASH : Animation roll (déjà jouée dans startDash) === + // Ne fait rien, laisse l'animation roll se terminer + } else if (this.isAttacking) { + // === EN ATTAQUE : Animation slash (déjà jouée dans startAttack) === + // Ne fait rien, laisse l'animation slash se terminer + } else { + // === PAS EN ATTAQUE NI DASH : Animation walk normale === + const animationName = `walk_${this.currentDirection}`; + + // Change la texture si nécessaire + if (this.assetLoader) { + const textureName = `walk_${this.currentDirection}`; + const newTexture = this.assetLoader.getTexture(textureName); + if (newTexture && spriteRenderer.texture !== newTexture) { + spriteRenderer.texture = newTexture; + } + } + + if (moved) { + // Si en mouvement, joue l'animation de marche + if (animator.currentAnimation?.name !== animationName || !animator.isPlaying) { + animator.play(animationName); + } + } else { + // Si immobile, arrête l'animation (reste sur première frame = idle) + if (animator.isPlaying) { + animator.stop(); + // Remet à la première frame de l'animation actuelle (pose idle) + spriteRenderer.sourceRect = animator.currentAnimation?.frames[0] || spriteRenderer.sourceRect; + } + // S'assure que l'animation courante est celle de la dernière direction + if (animator.currentAnimation?.name !== animationName) { + animator.play(animationName); + animator.stop(); // Joue puis arrête immédiatement pour avoir la première frame + spriteRenderer.sourceRect = animator.currentAnimation?.frames[0] || spriteRenderer.sourceRect; + } + } + } + } + + // Gestion du tir de projectile + this.lastShotTime += deltaTime; + + // Utilise Held pour détecter le clic (car Down est réinitialisé trop tôt) + const mouseHeld = this.inputManager.isMouseButtonHeld(0); + const canShoot = this.lastShotTime >= this.shootCooldown; + + if (mouseHeld && canShoot) { + console.log('[PlayerController] Tir de projectile déclenché !'); + this.lastShotTime = 0; + this.shootProjectile(scene, camera); + } + } + + private shootProjectile(scene: Scene, camera: Camera): void { + console.log('[PlayerController] shootProjectile() appelé'); + + if (!this.scene || !this.projectileTexture) { + console.error('[PlayerController] Impossible de tirer: scene ou texture manquante'); + return; + } + + // Récupère la position du curseur dans le monde + const mouseWorldPos = this.inputManager.getMouseWorldPosition(camera); + const playerPos = this.transform.position; + + console.log(`[PlayerController] Position joueur: (${playerPos.x.toFixed(1)}, ${playerPos.y.toFixed(1)})`); + console.log(`[PlayerController] Position curseur monde: (${mouseWorldPos.x.toFixed(1)}, ${mouseWorldPos.y.toFixed(1)})`); + + // Calcule la direction vers le curseur + const direction = mouseWorldPos.subtract(playerPos); + + // Si la direction est trop petite, ne tire pas + if (direction.lengthSquared() < 1) { + console.warn('[PlayerController] Direction trop petite, pas de tir'); + return; + } + + // Crée le projectile + const projectile = new GameObject('Projectile'); + projectile.transform.position = playerPos.clone(); + + // Sprite + const projectileSprite = projectile.addComponent(new SpriteRenderer()); + projectileSprite.texture = this.projectileTexture; + projectileSprite.sourceRect = new Rect(0, 0, 8, 8); + projectileSprite.tint = Color.white; + projectileSprite.layer = 1; + projectileSprite.pivot = new Vector2(0.5, 0.5); + + // Rigidbody pour le mouvement + const projectileRigidbody = projectile.addComponent(new Rigidbody2D()); + projectileRigidbody.mass = 0.1; // Masse très faible + projectileRigidbody.drag = 0; // Pas de friction + + // Collider + const projectileCollider = projectile.addComponent(new BoxCollider2D()); + projectileCollider.size = new Vector2(8, 8); + projectileCollider.isTrigger = true; // Trigger pour ne pas résoudre physiquement + + // Ajoute le composant Projectile + projectile.addComponent(new Projectile(direction)); + + scene.addGameObject(projectile); + console.log('[PlayerController] Projectile créé et ajouté à la scène !'); + } + + /** + * Démarre une attaque au corps-à-corps avec l'épée + */ + private startAttack(): void { + if (this.isAttacking) return; // Déjà en attaque + + console.log('⚔️ Début attaque !'); + + // Active l'état d'attaque + this.isAttacking = true; + this.attackTimer = this.attackDuration; + + // Obtient la direction d'attaque (diagonale) + const attackDir = this.getAttackDirection(); + console.log(`⚔️ Direction attaque: ${attackDir}`); + + // Joue l'animation slash du joueur + const animator = this.getComponent(Animator); + const spriteRenderer = this.getComponent(SpriteRenderer); + + if (animator && spriteRenderer && this.assetLoader) { + const slashAnimName = `slash_${attackDir}`; + const slashTextureName = `slash_${attackDir}`; + + // Change la texture du joueur + const slashTexture = this.assetLoader.getTexture(slashTextureName); + if (slashTexture) { + spriteRenderer.texture = slashTexture; + } + + // Joue l'animation slash + animator.play(slashAnimName); + console.log(`⚔️ Animation joueur: ${slashAnimName}`); + } + + // Active l'épée et joue son animation + if (this.swordGameObject) { + this.swordGameObject.active = true; + + const swordAnimator = this.swordGameObject.getComponent(Animator); + const swordSprite = this.swordGameObject.getComponent(SpriteRenderer); + + if (swordAnimator && swordSprite && this.assetLoader) { + const swordAnimName = `sword_${attackDir}`; + const swordTextureName = `sword_${attackDir}`; + + // Change la texture de l'épée + const swordTexture = this.assetLoader.getTexture(swordTextureName); + if (swordTexture) { + swordSprite.texture = swordTexture; + } + + // Joue l'animation sword + swordAnimator.play(swordAnimName); + console.log(`⚔️ Animation épée: ${swordAnimName}`); + } + } + } + + /** + * Démarre un dash/roll rapide pour esquiver + * Le dash se fait vers la direction du curseur + */ + private startDash(): void { + if (this.isDashing) return; // Déjà en dash + + console.log('🏃 Début dash !'); + + // Active l'état de dash + this.isDashing = true; + this.dashTimer = this.dashDuration; + + // Calcule la vitesse du dash (distance / durée) + this.dashSpeed = this.dashDistance / this.dashDuration; + + // Détermine la direction du dash vers le curseur (currentDirection est déjà mis à jour) + // Convertit la direction (string) en vecteur normalisé + this.dashDirection = this.getDirectionVector(this.currentDirection); + + console.log(`🏃 Direction dash: ${this.currentDirection} -> (${this.dashDirection.x.toFixed(2)}, ${this.dashDirection.y.toFixed(2)})`); + + // Joue l'animation roll du joueur + const animator = this.getComponent(Animator); + const spriteRenderer = this.getComponent(SpriteRenderer); + + if (animator && spriteRenderer && this.assetLoader) { + const rollAnimName = `roll_${this.currentDirection}`; + const rollTextureName = `roll_${this.currentDirection}`; + + // Change la texture du joueur + const rollTexture = this.assetLoader.getTexture(rollTextureName); + if (rollTexture) { + spriteRenderer.texture = rollTexture; + } + + // Joue l'animation roll + animator.play(rollAnimName); + console.log(`🏃 Animation: ${rollAnimName}`); + } + } + + /** + * Convertit une direction (string) en Vector2 normalisé + * Les sprites left/right sont inversés visuellement + */ + private getDirectionVector(direction: string): Vector2 { + // En WebGL : X+ = droite, X- = gauche, Y+ = bas, Y- = haut + // Avec inversion left/right pour correspondre aux sprites + switch (direction) { + case 'up': return new Vector2(0, -1); + case 'down': return new Vector2(0, 1); + case 'left': return new Vector2(1, 0); // sprite left va à droite visuellement + case 'right': return new Vector2(-1, 0); // sprite right va à gauche visuellement + case 'upleft': return new Vector2(1, -1).normalize(); // va haut-droite visuellement + case 'upright': return new Vector2(-1, -1).normalize(); // va haut-gauche visuellement + case 'downleft': return new Vector2(1, 1).normalize(); // va bas-droite visuellement + case 'downright': return new Vector2(-1, 1).normalize(); // va bas-gauche visuellement + default: return new Vector2(0, 1); // Par défaut: bas + } + } +} + diff --git a/src/game/components/Projectile.ts b/src/game/components/Projectile.ts new file mode 100644 index 0000000..be96ef0 --- /dev/null +++ b/src/game/components/Projectile.ts @@ -0,0 +1,73 @@ +/** + * Projectile + * Component pour gérer le comportement d'un projectile + */ + +import { Component } from '../../engine/entities/Component'; +import { Vector2 } from '../../engine/math/Vector2'; +import { Rigidbody2D } from '../../engine/physics/Rigidbody2D'; +import { BoxCollider2D } from '../../engine/physics/BoxCollider2D'; + +export class Projectile extends Component { + private speed: number = 400; // pixels par seconde + private direction: Vector2; + private lifetime: number = 5; // Se détruit après 5 secondes + + constructor(direction: Vector2) { + super(); + this.direction = direction.normalize(); + } + + update(deltaTime: number): void { + // Déplace le projectile + const rigidbody = this.getComponent(Rigidbody2D); + if (rigidbody) { + rigidbody.velocity = this.direction.multiply(this.speed); + } else { + // Si pas de rigidbody, utilise transform directement + this.transform.translate(this.direction.multiply(this.speed * deltaTime)); + } + + // Log position chaque seconde (pour debug) + if (Math.floor(this.lifetime) !== Math.floor(this.lifetime + deltaTime)) { + console.log(`[Projectile] Position: (${this.transform.position.x.toFixed(1)}, ${this.transform.position.y.toFixed(1)}), Lifetime: ${this.lifetime.toFixed(2)}s`); + } + + // Réduit la durée de vie + this.lifetime -= deltaTime; + if (this.lifetime <= 0) { + console.log('[Projectile] Durée de vie expirée, destruction'); + this.gameObject.destroy(); + } + } + + start(): void { + console.log('[Projectile] start() appelé'); + // Configure le collider pour détecter les collisions + const collider = this.getComponent(BoxCollider2D); + if (collider) { + console.log('[Projectile] Collider trouvé, configuration trigger'); + collider.isTrigger = true; // Trigger pour ne pas résoudre physiquement + collider.onTriggerEnter((other) => { + console.log(`[Projectile] Trigger avec ${other.gameObject.name} (tag: ${other.gameObject.tag})`); + // Si collision avec un mur ou un ennemi, détruit le projectile + if (other.gameObject.tag === 'Wall' || other.gameObject.tag === 'Enemy') { + console.log(`[Projectile] Collision détectée avec ${other.gameObject.tag}, destruction`); + if (other.gameObject.tag === 'Enemy') { + // Détruit l'ennemi aussi + console.log(`[Projectile] Destruction de l'ennemi ${other.gameObject.name}`); + other.gameObject.destroy(); + } + this.gameObject.destroy(); + } + }); + } else { + console.warn('[Projectile] Aucun collider trouvé !'); + } + } + + awake(): void { + console.log('[Projectile] awake() appelé'); + } +} + diff --git a/src/game/scenes/GameScene.ts b/src/game/scenes/GameScene.ts new file mode 100644 index 0000000..ddca30d --- /dev/null +++ b/src/game/scenes/GameScene.ts @@ -0,0 +1,515 @@ +/** + * GameScene + * Scène principale du jeu avec joueur, ennemis, pièces, et environnement + */ + +import { Scene } from '../../engine/core/Scene'; +import { GameObject } from '../../engine/entities/GameObject'; +import { Vector2 } from '../../engine/math/Vector2'; +import { Color } from '../../engine/math/Color'; +import { Rect } from '../../engine/math/Rect'; +import { Engine } from '../../engine/core/Engine'; +import { AssetLoader } from '../../engine/assets/AssetLoader'; +import { SpriteSheet } from '../../engine/assets/SpriteSheet'; +import { Animation } from '../../engine/animation/Animation'; +import { SpriteRenderer } from '../../engine/components/SpriteRenderer'; +import { Animator } from '../../engine/components/Animator'; +import { BoxCollider2D, CircleCollider2D } from '../../engine/physics'; +import { Rigidbody2D } from '../../engine/physics/Rigidbody2D'; +import { PlayerController } from '../components/PlayerController'; +import { HealthComponent } from '../components/HealthComponent'; +import { TextureGenerator } from '../utils/TextureGenerator'; +import { UIHealthBar } from '../ui/UIHealthBar'; + +export class GameScene extends Scene { + private engine: Engine; + private assetLoader: AssetLoader; + private gl: WebGLRenderingContext | WebGL2RenderingContext; + + // Configuration du monde de jeu + private readonly TILE_SIZE = 80; + private readonly GRID_WIDTH = 10 * this.TILE_SIZE; // 800px + private readonly GRID_HEIGHT = 10 * this.TILE_SIZE; // 800px + + constructor(engine: Engine, width: number, height: number) { + super('GameScene', width, height); + this.engine = engine; + this.assetLoader = engine.assets; + this.gl = engine.renderer.gl; + } + + /** + * Charge les assets nécessaires à la scène + */ + async loadAssets(): Promise { + console.log('=== Chargement des assets du jeu ==='); + + try { + // === WALK (8 directions) === + await this.assetLoader.loadTexture('walk_down', '/TopDownCharacter/Character/Character_Down.png'); + await this.assetLoader.loadTexture('walk_downleft', '/TopDownCharacter/Character/Character_DownLeft.png'); + await this.assetLoader.loadTexture('walk_downright', '/TopDownCharacter/Character/Character_DownRight.png'); + await this.assetLoader.loadTexture('walk_left', '/TopDownCharacter/Character/Character_Left.png'); + await this.assetLoader.loadTexture('walk_right', '/TopDownCharacter/Character/Character_Right.png'); + await this.assetLoader.loadTexture('walk_up', '/TopDownCharacter/Character/Character_Up.png'); + await this.assetLoader.loadTexture('walk_upleft', '/TopDownCharacter/Character/Character_UpLeft.png'); + await this.assetLoader.loadTexture('walk_upright', '/TopDownCharacter/Character/Character_UpRight.png'); + + // === ROLL (8 directions) === + await this.assetLoader.loadTexture('roll_down', '/TopDownCharacter/Character/Character_RollDown.png'); + await this.assetLoader.loadTexture('roll_downleft', '/TopDownCharacter/Character/Character_RollDownLeft.png'); + await this.assetLoader.loadTexture('roll_downright', '/TopDownCharacter/Character/Character_RollDownRight.png'); + await this.assetLoader.loadTexture('roll_left', '/TopDownCharacter/Character/Character_RollLeft.png'); + await this.assetLoader.loadTexture('roll_right', '/TopDownCharacter/Character/Character_RollRight.png'); + await this.assetLoader.loadTexture('roll_up', '/TopDownCharacter/Character/Character_RollUp.png'); + await this.assetLoader.loadTexture('roll_upleft', '/TopDownCharacter/Character/Character_RollUpLeft.png'); + await this.assetLoader.loadTexture('roll_upright', '/TopDownCharacter/Character/Character_RollUpRight.png'); + + // === SLASH (4 directions diagonales) === + await this.assetLoader.loadTexture('slash_downleft', '/TopDownCharacter/Character/Character_SlashDownLeft.png'); + await this.assetLoader.loadTexture('slash_downright', '/TopDownCharacter/Character/Character_SlashDownRight.png'); + await this.assetLoader.loadTexture('slash_upleft', '/TopDownCharacter/Character/Character_SlashUpLeft.png'); + await this.assetLoader.loadTexture('slash_upright', '/TopDownCharacter/Character/Character_SlashUpRight.png'); + + // === SWORD/WEAPON (4 directions diagonales) === + await this.assetLoader.loadTexture('sword_downleft', '/TopDownCharacter/Weapon/Sword_DownLeft.png'); + await this.assetLoader.loadTexture('sword_downright', '/TopDownCharacter/Weapon/Sword_DownRight.png'); + await this.assetLoader.loadTexture('sword_upleft', '/TopDownCharacter/Weapon/Sword_UpLeft.png'); + await this.assetLoader.loadTexture('sword_upright', '/TopDownCharacter/Weapon/Sword_UpRight.png'); + + // === HEARTS (UI de vie) === + // ATTENTION : Les fichiers sont dans l'ordre INVERSE ! + // Hearts_Red_1.png = PLEIN, Hearts_Red_5.png = VIDE + await this.assetLoader.loadTexture('heart_full', '/Retro Inventory/Retro Inventory/Original/Hearts_Red_1.png'); + await this.assetLoader.loadTexture('heart_three_quarters', '/Retro Inventory/Retro Inventory/Original/Hearts_Red_2.png'); + await this.assetLoader.loadTexture('heart_half', '/Retro Inventory/Retro Inventory/Original/Hearts_Red_3.png'); + await this.assetLoader.loadTexture('heart_quarter', '/Retro Inventory/Retro Inventory/Original/Hearts_Red_4.png'); + await this.assetLoader.loadTexture('heart_empty', '/Retro Inventory/Retro Inventory/Original/Hearts_Red_5.png'); + + console.log('✅ Tous les assets chargés'); + } catch (error) { + console.error('❌ Erreur lors du chargement des assets:', error); + } + } + + /** + * Initialise la scène (appelé par Engine après loadScene) + */ + async onLoad(): Promise { + // Évite de charger deux fois + if (this.isLoaded) { + console.log('⚠️ GameScene déjà chargée, skip'); + return; + } + + console.log('=== Initialisation de GameScene ==='); + + // Crée le UICanvas + this.createUICanvas(); + console.log('✅ UICanvas créé'); + + // Charge les assets + await this.loadAssets(); + + // Détermine la taille du sprite + const testTexture = this.assetLoader.getTexture('walk_down'); + let spriteWidth = 16; + let spriteHeight = 16; + + if (testTexture) { + spriteWidth = testTexture.width / 4; // 4 frames par animation + spriteHeight = testTexture.height; + console.log(`📐 Taille sprite détectée: ${spriteWidth}x${spriteHeight}`); + } + + // Crée les éléments de la scène + this.createGround(); + this.createWalls(); + const player = this.createPlayer(spriteWidth, spriteHeight); + this.createEnemies(); + this.createCoins(); + + // Crée l'UI de vie + this.createHealthUI(player); + + // Centre la caméra + this.camera.position = new Vector2(this.GRID_WIDTH / 2, this.GRID_HEIGHT / 2); + + // Appelle super.onLoad() pour marquer comme chargée + super.onLoad(); + + console.log('✅ GameScene initialisée'); + } + + /** + * Crée le sol avec tiles + */ + private createGround(): void { + const tileTextureSize = 80; + const groundFillColor = new Color(0.4, 0.3, 0.2, 1.0); // Marron + const groundBorderColor = new Color(0.2, 0.15, 0.1, 1.0); // Marron foncé + const groundTexture = TextureGenerator.createTileTextureWithBorder( + this.gl, + tileTextureSize, + groundFillColor, + groundBorderColor, + 2 + ); + + for (let x = 0; x < 10; x++) { + for (let y = 0; y < 10; y++) { + const ground = new GameObject(`Ground_${x}_${y}`); + ground.transform.position = new Vector2(x * this.TILE_SIZE, y * this.TILE_SIZE); + + const groundSprite = ground.addComponent(new SpriteRenderer()); + groundSprite.texture = groundTexture; + groundSprite.sourceRect = new Rect(0, 0, tileTextureSize, tileTextureSize); + groundSprite.tint = Color.white; + groundSprite.layer = -1; // Arrière-plan + groundSprite.pivot = new Vector2(0, 0); // Pivot en haut-gauche + + this.addGameObject(ground); + } + } + } + + /** + * Crée les murs autour de la grille + */ + private createWalls(): void { + const wallThickness = 60; + const wallColor = new Color(0.3, 0.3, 0.3, 1.0); // Gris foncé + + // Mur du haut + const wallTop = this.createWall( + 'Wall_Top', + new Vector2(this.GRID_WIDTH / 2, -wallThickness / 2), + this.GRID_WIDTH, + wallThickness, + wallColor + ); + this.addGameObject(wallTop); + + // Mur du bas + const wallBottom = this.createWall( + 'Wall_Bottom', + new Vector2(this.GRID_WIDTH / 2, this.GRID_HEIGHT + wallThickness / 2), + this.GRID_WIDTH, + wallThickness, + wallColor + ); + this.addGameObject(wallBottom); + + // Mur de gauche + const wallLeft = this.createWall( + 'Wall_Left', + new Vector2(-wallThickness / 2, this.GRID_HEIGHT / 2), + wallThickness, + this.GRID_HEIGHT, + wallColor + ); + this.addGameObject(wallLeft); + + // Mur de droite + const wallRight = this.createWall( + 'Wall_Right', + new Vector2(this.GRID_WIDTH + wallThickness / 2, this.GRID_HEIGHT / 2), + wallThickness, + this.GRID_HEIGHT, + wallColor + ); + this.addGameObject(wallRight); + } + + /** + * Helper pour créer un mur + */ + private createWall(name: string, position: Vector2, width: number, height: number, color: Color): GameObject { + const wall = new GameObject(name); + wall.transform.position = position; + wall.tag = 'Wall'; + + const wallTexture = TextureGenerator.createSolidColorTexture(this.gl, width, height, color); + const wallSprite = wall.addComponent(new SpriteRenderer()); + wallSprite.texture = wallTexture; + wallSprite.sourceRect = new Rect(0, 0, width, height); + wallSprite.tint = Color.white; + wallSprite.layer = 0; + wallSprite.pivot = new Vector2(0.5, 0.5); + + const wallCollider = wall.addComponent(new BoxCollider2D()); + wallCollider.size = new Vector2(width, height); + + return wall; + } + + /** + * Crée le joueur avec animations + */ + private createPlayer(spriteWidth: number, spriteHeight: number): GameObject { + const player = new GameObject('Player'); + player.transform.position = new Vector2(this.GRID_WIDTH / 2, this.GRID_HEIGHT / 2); + player.transform.scale = new Vector2(3, 3); // 3x plus grand + player.transform.rotation = 90; // Rotation 90° horaire pour orienter les sprites correctement + + // Sprite + const playerSprite = player.addComponent(new SpriteRenderer()); + const walkTexture = this.assetLoader.getTexture('walk_down'); + if (walkTexture) { + playerSprite.texture = walkTexture; + } + playerSprite.sourceRect = new Rect(0, 0, spriteWidth, spriteHeight); + playerSprite.tint = Color.white; + playerSprite.layer = 1; + + // Animator avec toutes les animations + const playerAnimator = player.addComponent(new Animator()); + this.createPlayerAnimations(playerAnimator, spriteWidth, spriteHeight); + + // Joue l'animation walk_down par défaut + playerAnimator.play('walk_down'); + + // Controller + const playerController = new PlayerController(this.engine.input, this.gl, this.assetLoader); + player.addComponent(playerController); + + // Physique + const playerCollider = player.addComponent(new BoxCollider2D()); + playerCollider.size = new Vector2(16, 16); + playerCollider.onCollisionEnter((other) => { + console.log(`Player collision avec ${other.gameObject.name}`); + }); + + const playerRigidbody = player.addComponent(new Rigidbody2D()); + playerRigidbody.drag = 15; + + // Système de vie + const playerHealth = player.addComponent(new HealthComponent(500)); // 500 points de vie = 5 cœurs + playerHealth.onDeath(() => { + console.log('💀 Le joueur est mort !'); + // TODO: Game Over + }); + + // Épée (enfant du joueur) + this.createSword(player, spriteWidth, spriteHeight); + + this.addGameObject(player); + return player; + } + + /** + * Crée toutes les animations du joueur + */ + private createPlayerAnimations(animator: Animator, spriteWidth: number, spriteHeight: number): void { + const directions = ['down', 'downleft', 'downright', 'left', 'right', 'up', 'upleft', 'upright']; + + // Walk animations + for (const dir of directions) { + const anim = this.createAnimationFromTexture(`walk_${dir}`, `walk_${dir}`, spriteWidth, spriteHeight, 0.12, true); + if (anim) animator.addAnimation(`walk_${dir}`, anim); + } + + // Roll animations + for (const dir of directions) { + const anim = this.createAnimationFromTexture(`roll_${dir}`, `roll_${dir}`, spriteWidth, spriteHeight, 0.08, false); + if (anim) animator.addAnimation(`roll_${dir}`, anim); + } + + // Slash animations (diagonales seulement) + const slashDirs = ['downleft', 'downright', 'upleft', 'upright']; + for (const dir of slashDirs) { + const anim = this.createAnimationFromTexture(`slash_${dir}`, `slash_${dir}`, spriteWidth, spriteHeight, 0.08, false); + if (anim) animator.addAnimation(`slash_${dir}`, anim); + } + + console.log('✅ Animations joueur créées: 8 walk + 8 roll + 4 slash'); + } + + /** + * Crée l'épée en tant qu'enfant du joueur + */ + private createSword(player: GameObject, spriteWidth: number, spriteHeight: number): void { + const sword = new GameObject('Sword'); + sword.transform.position = new Vector2(0, 0); + sword.active = false; // Désactivée par défaut + + // Sprite + const swordSprite = sword.addComponent(new SpriteRenderer()); + const swordTexture = this.assetLoader.getTexture('sword_downright'); + if (swordTexture) { + swordSprite.texture = swordTexture; + } + swordSprite.sourceRect = new Rect(0, 0, spriteWidth, spriteHeight); + swordSprite.tint = Color.white; + swordSprite.layer = 2; + + // Animator + const swordAnimator = sword.addComponent(new Animator()); + const swordDirs = ['downleft', 'downright', 'upleft', 'upright']; + for (const dir of swordDirs) { + const anim = this.createAnimationFromTexture(`sword_${dir}`, `sword_${dir}`, spriteWidth, spriteHeight, 0.08, false); + if (anim) swordAnimator.addAnimation(`sword_${dir}`, anim); + } + + // Collider + const swordCollider = sword.addComponent(new BoxCollider2D()); + swordCollider.size = new Vector2(24, 24); + swordCollider.isTrigger = true; + swordCollider.onTriggerEnter((other) => { + if (other.gameObject.tag === 'Enemy') { + console.log(`⚔️ Épée touche ${other.gameObject.name} !`); + other.gameObject.destroy(); + } + }); + + player.addChild(sword); + console.log('✅ Épée créée'); + } + + /** + * Helper pour créer une animation depuis une texture + */ + private createAnimationFromTexture( + textureName: string, + animationName: string, + spriteWidth: number, + spriteHeight: number, + frameDuration: number, + loop: boolean + ): Animation | null { + const texture = this.assetLoader.getTexture(textureName); + if (!texture) { + console.warn(`Texture "${textureName}" non trouvée`); + return null; + } + + const spriteSheet = new SpriteSheet(texture, spriteWidth, spriteHeight); + const numFrames = Math.min(spriteSheet.columns, 4); // Maximum 4 frames + + const frames: Rect[] = []; + for (let i = 0; i < numFrames; i++) { + const spriteRect = spriteSheet.getSpriteByIndex(i); + if (spriteRect) { + frames.push(spriteRect); + } + } + + if (frames.length === 0) { + console.warn(`Aucune frame trouvée pour "${textureName}"`); + return null; + } + + return new Animation(animationName, frames, frameDuration, loop); + } + + /** + * Crée les ennemis + */ + private createEnemies(): void { + const enemyTexture = TextureGenerator.createSolidColorTexture(this.gl, 48, 48, new Color(1.0, 0.2, 0.2, 1.0)); + + for (let i = 0; i < 5; i++) { + const enemy = new GameObject(`Enemy_${i}`); + enemy.transform.position = new Vector2(100 + i * 150, 100 + (i % 2) * 200); + enemy.tag = 'Enemy'; + + const enemySprite = enemy.addComponent(new SpriteRenderer()); + enemySprite.texture = enemyTexture; + enemySprite.sourceRect = new Rect(0, 0, 48, 48); + enemySprite.tint = Color.white; + enemySprite.layer = 0; + + const enemyCollider = enemy.addComponent(new BoxCollider2D()); + enemyCollider.size = new Vector2(48, 48); + enemyCollider.onCollisionEnter((other) => { + if (other.gameObject.name === 'Player') { + console.log(`Enemy ${i} touché par le joueur !`); + } + }); + + this.addGameObject(enemy); + } + } + + /** + * Crée l'UI de vie du joueur + */ + private createHealthUI(player: GameObject): void { + // Récupère le HealthComponent du joueur + const healthComponent = player.getComponents().find(c => c.constructor.name === 'HealthComponent') as HealthComponent; + + if (!healthComponent) { + console.error('❌ HealthComponent introuvable sur le joueur !'); + return; + } + + if (!this.uiCanvas) { + console.error('❌ UICanvas introuvable dans la scène !'); + return; + } + + // Crée un GameObject UI pour la barre de vie + const healthBarObject = new GameObject('HealthBar'); + const healthBar = healthBarObject.addComponent(new UIHealthBar(this.assetLoader, this.uiCanvas)); + + // Lie la barre de vie au composant de vie du joueur + healthBar.bindToHealth(healthComponent); + + // Ajoute au canvas UI (le HealthBar n'a pas besoin d'être ajouté, les cœurs seront ajoutés directement) + // this.uiCanvas.addGameObject(healthBarObject); + + console.log('✅ UI de vie créée'); + } + + /** + * Crée les pièces collectables + */ + private createCoins(): void { + const coinTexture = TextureGenerator.createSolidColorTexture(this.gl, 32, 32, new Color(1.0, 0.8, 0.0, 1.0)); + + for (let i = 0; i < 3; i++) { + const coin = new GameObject(`Coin_${i}`); + coin.transform.position = new Vector2(200 + i * 200, 400); + coin.tag = 'Coin'; + + const coinSprite = coin.addComponent(new SpriteRenderer()); + coinSprite.texture = coinTexture; + coinSprite.sourceRect = new Rect(0, 0, 32, 32); + coinSprite.tint = Color.white; + coinSprite.layer = 0; + + // Animation de rotation + const coinAnimator = coin.addComponent(new Animator()); + const coinAnim = new Animation('spin', [ + new Rect(0, 0, 32, 32), + new Rect(0, 0, 32, 64), + new Rect(0, 0, 32, 32), + ], 0.1, true); + coinAnimator.addAnimation('spin', coinAnim); + coinAnimator.play('spin'); + coinAnimator.speed = 2.0; + + // Collider + const coinCollider = coin.addComponent(new CircleCollider2D()); + coinCollider.radius = 16; + coinCollider.isTrigger = true; + coinCollider.onTriggerEnter((other) => { + if (other.gameObject.name === 'Player') { + console.log(`Pièce ${i} collectée !`); + + // Inflige 25 dégâts au joueur + const healthComponent = other.gameObject.getComponents().find(c => c.constructor.name === 'HealthComponent') as HealthComponent; + if (healthComponent) { + healthComponent.damage(25); + console.log(`💔 Joueur blessé ! Vie restante: ${healthComponent.health}/${healthComponent.maxHealth}`); + } + + coin.active = false; + } + }); + + this.addGameObject(coin); + } + } +} + diff --git a/src/game/ui/UIHealthBar.ts b/src/game/ui/UIHealthBar.ts new file mode 100644 index 0000000..ca6a087 --- /dev/null +++ b/src/game/ui/UIHealthBar.ts @@ -0,0 +1,165 @@ +/** + * UIHealthBar - Affiche la barre de vie avec des cœurs + */ + +import { UIComponent } from '../../engine/ui/UIComponent'; +import { UIImage } from '../../engine/ui/UIImage'; +import { UICanvas } from '../../engine/ui/UICanvas'; +import { GameObject } from '../../engine/entities/GameObject'; +import { Vector2 } from '../../engine/math/Vector2'; +import { Color } from '../../engine/math/Color'; +import { SpriteBatch } from '../../engine/rendering/SpriteBatch'; +import { AssetLoader } from '../../engine/assets/AssetLoader'; +import { HealthComponent } from '../components/HealthComponent'; + +export class UIHealthBar extends UIComponent { + private hearts: UIImage[] = []; + private assetLoader: AssetLoader; + private uiCanvas: UICanvas | null = null; + private lastHealthValue: number = -1; + private heartSize: number = 20; // Taille d'un cœur en pixels (réduit à 20px) + private heartSpacing: number = 2; // Espacement entre les cœurs (réduit à 2px) + + constructor(assetLoader: AssetLoader, uiCanvas: UICanvas) { + super(); + this.assetLoader = assetLoader; + this.uiCanvas = uiCanvas; + } + + /** + * Lie la barre de vie à un HealthComponent + */ + bindToHealth(healthComponent: HealthComponent): void { + // Callback pour mettre à jour l'UI quand la vie change + healthComponent.onHealthChanged((current, max) => { + this.updateHearts(current, max); + }); + + // Initialise l'affichage + this.updateHearts(healthComponent.health, healthComponent.maxHealth); + } + + /** + * Met à jour l'affichage des cœurs selon la vie actuelle + */ + private updateHearts(currentHealth: number, maxHealth: number): void { + // Évite de redessiner si la vie n'a pas changé + if (currentHealth === this.lastHealthValue) { + return; + } + + this.lastHealthValue = currentHealth; + + // Calcule le nombre de cœurs nécessaires (1 cœur = 100 points de vie) + const maxHearts = Math.ceil(maxHealth / 100); + + // Crée ou supprime des cœurs si nécessaire + while (this.hearts.length < maxHearts) { + this.addHeart(this.hearts.length); + } + while (this.hearts.length > maxHearts) { + const heart = this.hearts.pop(); + if (heart && this.uiCanvas) { + this.uiCanvas.removeGameObject(heart.gameObject); + } + } + + // Met à jour chaque cœur + let remainingHealth = currentHealth; + for (let i = 0; i < this.hearts.length; i++) { + const heart = this.hearts[i]; + if (!heart) continue; // Sécurité + + const heartHealth = Math.min(100, remainingHealth); + this.updateHeartSprite(heart, heartHealth); + remainingHealth -= 100; + } + } + + /** + * Ajoute un nouveau cœur à l'UI + */ + private addHeart(index: number): void { + // Crée un GameObject pour le cœur + const heartObject = new GameObject(`Heart_${index}`); + + // Ajoute le composant UIImage + const heart = heartObject.addComponent(new UIImage()); + + // Position du cœur (horizontalement de droite à gauche) + // On inverse l'ordre : le dernier cœur (index le plus élevé) est le plus à droite + const totalHearts = 5; // Max 5 cœurs pour 500 HP + const reverseIndex = totalHearts - 1 - index; // Inverse l'ordre + const xOffset = reverseIndex * (this.heartSize + this.heartSpacing); + + heart.rectTransform.anchorMin = new Vector2(1, 1); // Ancre en haut à droite + heart.rectTransform.anchorMax = new Vector2(1, 1); + heart.rectTransform.pivot = new Vector2(1, 1); + heart.rectTransform.anchoredPosition = new Vector2(-40 - xOffset, -40); // 40px de marge (plus vers la gauche et plus bas) + heart.rectTransform.size = new Vector2(this.heartSize, this.heartSize); + + // Rotation pour orienter le cœur correctement + // Valeurs à tester : 0, 90, 180, 270, -90 + heartObject.transform.rotation = 90; // Tourne de 90° (essaie d'abord celle-ci) + + // Texture par défaut (cœur plein) + const texture = this.assetLoader.getTexture('heart_full'); + if (texture) { + heart.texture = texture; + } + + // Active l'ombre légère autour du cœur + heart.shadowEnabled = true; + heart.shadowOffset = new Vector2(1, 1); // Décalage léger de 1px en bas à droite + heart.shadowColor = new Color(0, 0, 0, 0.4); // Noir avec 40% d'opacité + + this.hearts.push(heart); + + // Ajoute le GameObject au UICanvas + if (this.uiCanvas) { + this.uiCanvas.addGameObject(heartObject); + } + } + + /** + * Met à jour le sprite d'un cœur selon sa vie + */ + private updateHeartSprite(heart: UIImage, health: number): void { + let textureName: string; + + if (health <= 0) { + textureName = 'heart_empty'; // Hearts_Red_1.png + } else if (health <= 25) { + textureName = 'heart_quarter'; // Hearts_Red_2.png (1 quart) + } else if (health <= 50) { + textureName = 'heart_half'; // Hearts_Red_3.png (2 quarts) + } else if (health <= 75) { + textureName = 'heart_three_quarters'; // Hearts_Red_4.png (3 quarts) + } else { + textureName = 'heart_full'; // Hearts_Red_5.png (plein) + } + + const texture = this.assetLoader.getTexture(textureName); + if (texture) { + heart.texture = texture; + } + } + + /** + * Rend l'UI (méthode abstraite de UIComponent) + * Les cœurs sont déjà rendus par le UICanvas, donc cette méthode est vide + */ + renderUI(_spriteBatch: SpriteBatch): void { + // Les cœurs sont des GameObject UI séparés, rendus directement par le UICanvas + // Cette méthode est vide mais doit être implémentée + } + + awake(): void { + super.awake(); + } + + update(deltaTime: number): void { + super.update(deltaTime); + } +} + diff --git a/src/game/utils/TextureGenerator.ts b/src/game/utils/TextureGenerator.ts new file mode 100644 index 0000000..e25c381 --- /dev/null +++ b/src/game/utils/TextureGenerator.ts @@ -0,0 +1,118 @@ +/** + * TextureGenerator + * Utilitaires pour générer des textures procédurales (pour tests sans images externes) + */ + +import { Texture } from '../../engine/rendering/Texture'; +import { Color } from '../../engine/math/Color'; + +export class TextureGenerator { + /** + * Crée une texture de couleur solide + */ + static createSolidColorTexture( + gl: WebGLRenderingContext | WebGL2RenderingContext, + width: number, + height: number, + color: Color + ): Texture { + const texture = new Texture(gl); + + // Crée un canvas temporaire pour générer l'image + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + if (!ctx) { + throw new Error('Impossible de créer le contexte 2D'); + } + + // Remplit avec la couleur + ctx.fillStyle = color.toRGBA(); + ctx.fillRect(0, 0, width, height); + + // Upload directement depuis le canvas + const webglTexture = (texture as any)._texture; + if (!webglTexture) { + throw new Error('Texture WebGL non créée'); + } + + gl.bindTexture(gl.TEXTURE_2D, webglTexture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.bindTexture(gl.TEXTURE_2D, null); + + // Met à jour la taille + (texture as any)._width = width; + (texture as any)._height = height; + + console.log(`Texture créée: ${width}x${height}, couleur: ${color.toRGBA()}`); + return texture; + } + + /** + * Crée une texture de tile avec bordure pour délimitation + */ + static createTileTextureWithBorder( + gl: WebGLRenderingContext | WebGL2RenderingContext, + size: number, + fillColor: Color, + borderColor: Color, + borderWidth: number = 1 + ): Texture { + const texture = new Texture(gl); + + // Crée un canvas temporaire + const canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext('2d'); + if (!ctx) { + throw new Error('Impossible de créer le contexte 2D'); + } + + // Remplit avec la couleur de fond + ctx.fillStyle = fillColor.toRGBA(); + ctx.fillRect(0, 0, size, size); + + // Dessine la bordure (on ne dessine que les bords droits et bas pour éviter les doubles bordures entre tiles) + ctx.strokeStyle = borderColor.toRGBA(); + ctx.lineWidth = borderWidth; + + // Bordure droite + ctx.beginPath(); + ctx.moveTo(size - borderWidth / 2, 0); + ctx.lineTo(size - borderWidth / 2, size); + ctx.stroke(); + + // Bordure bas + ctx.beginPath(); + ctx.moveTo(0, size - borderWidth / 2); + ctx.lineTo(size, size - borderWidth / 2); + ctx.stroke(); + + // Upload directement depuis le canvas + const webglTexture = (texture as any)._texture; + if (!webglTexture) { + throw new Error('Texture WebGL non créée'); + } + + gl.bindTexture(gl.TEXTURE_2D, webglTexture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.bindTexture(gl.TEXTURE_2D, null); + + // Met à jour la taille + (texture as any)._width = size; + (texture as any)._height = size; + + return texture; + } +} + diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..e947eb5 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,39 @@ +/** + * CastleStorm Engine + * Point d'entrée principal + */ + +import { Game } from './game/Game'; + +console.log('CastleStorm Engine v0.1.0'); +console.log('Initialisation...'); + +/** + * Initialisation du jeu + */ +async function init() { + const canvas = document.getElementById('game-canvas') as HTMLCanvasElement; + if (!canvas) { + console.error('Canvas introuvable'); + return; + } + + // Crée le jeu + const game = new Game(canvas); + + // Initialise le jeu + if (!(await game.initialize())) { + console.error('Échec de l\'initialisation du jeu'); + return; + } + + // Démarre le jeu + game.start(); +} + +// Lance l'initialisation quand le DOM est prêt +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); +} else { + init(); +} diff --git a/src/main.ts b/src/main.ts deleted file mode 100644 index 33ced38..0000000 --- a/src/main.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {Game} from "./vendor/Game"; - -window.onload = () => { - const canvas = document.createElement('canvas'); - document.body.appendChild(canvas) - const game = new Game(canvas); - game.loop() -} \ No newline at end of file diff --git a/src/models/Entity.ts b/src/models/Entity.ts deleted file mode 100644 index 90a94b3..0000000 --- a/src/models/Entity.ts +++ /dev/null @@ -1,54 +0,0 @@ -import State from "../vendor/State"; - -export default class Entity { - - public x: number; - public y: number; - public frame: number; - public sprite: HTMLImageElement = new Image(); - public state: State; - public angle: number; - public scale: number; - // v for velocity - public velocity: { - x: number, - y: number - } - // t for target - public target: { - x: number, - y: number - } - public radius: number; - context: CanvasRenderingContext2D; - canvas: HTMLCanvasElement; - - constructor(x: number, y: number, radius: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { - this.x = x; - this.y = y; - this.angle = 0; - this.frame = 0; - this.radius = radius; - this.context = context; - this.canvas = canvas; - this.state = state; - this.scale = 1; - this.velocity = { - x: 0, - y: 0 - } - this.target = { - x: this.x, - y: this.y - } - } - - public draw(): void { - throw new Error("Method not implemented."); - } - - public update(): void { - throw new Error("Method not implemented."); - } - -} \ No newline at end of file diff --git a/src/models/Player.ts b/src/models/Player.ts deleted file mode 100644 index 12ae456..0000000 --- a/src/models/Player.ts +++ /dev/null @@ -1,168 +0,0 @@ -import Entity from "./Entity"; -import State from "../vendor/State"; - -export default class Player extends Entity { - public isMoving: boolean = false; - public isFiring: boolean = false; - public weapon: HTMLImageElement = new Image(); - public dashLocation: { - x: number, - y: number - } - private inputs: any = { - 'z': false, - 'q': false, - 's': false, - 'd': false, - ' ': false - }; - delay: number = 0; - limit: number = 20; - private speed: number = 10; - - - constructor(x: number, y: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { - super(x, y, 10, context, canvas, state); - this.angle = 0; - this.dashLocation = { - x: this.x, - y: this.y - } - this.loadSprite(); - this.keyEvent(); - this.clickEvent(); - } - - public draw(): void { - // draw outlined circle for the player - this.context.beginPath(); - this.context.arc(this.x, this.y, this.radius, 0, Math.PI * 2); - this.context.strokeStyle = 'rgba(0, 0, 0, 1)'; - this.context.stroke(); - this.context.closePath(); - // draw a circle that represent the range of the dash ability which is 7 times the radius of the player - this.context.beginPath(); - this.context.arc(this.x, this.y, this.radius * 7, 0, Math.PI * 2); - this.context.strokeStyle = 'rgba(0, 0, 0, 0.5)'; - this.context.stroke(); - this.context.closePath(); - // draw a line from the player to the point that it cross the circle of the dash ability and draw a point at the end of the line - this.context.beginPath(); - this.context.moveTo(this.x, this.y); - this.context.lineTo(this.x + Math.cos(this.angle) * this.radius * 7, this.y + Math.sin(this.angle) * this.radius * 7); - this.context.strokeStyle = 'rgba(0, 0, 0, 0.5)'; - this.context.stroke(); - this.context.closePath(); - this.context.beginPath(); - this.context.arc(this.x + Math.cos(this.angle) * this.radius * 7, this.y + Math.sin(this.angle) * this.radius * 7, 5, 0, Math.PI * 2); - this.context.fillStyle = 'rgba(0, 0, 0, 0.5)'; - this.context.fill(); - this.context.closePath(); - - } - - private loadSprite(): void { - this.sprite.src = './src/sprites/player.png'; - this.weapon.src = './src/sprites/bow.png'; - } - - private keyEvent(): void { - document.body.addEventListener('keydown', (e: KeyboardEvent) => { - this.inputs[e.key] = true; - this.hookKeyDown(); - }); - document.body.addEventListener('keyup', (e: KeyboardEvent) => { - for (let i = 0; i < Object.keys(this.inputs).length; i++) { - if (Object.keys(this.inputs)[i] === e.key) { - this.inputs[e.key] = false; - } - } - this.target.x = this.x; - this.target.y = this.y; - this.hookKeyUp(); - }); - } - - public update(): void { - this.draw(); - this.state.player.x = this.x; - this.state.player.y = this.y; - this.angle = Math.atan2(this.state.mouse.y - this.y, this.state.mouse.x - this.x); - if (this.angle < 0) { - this.angle += Math.PI * 2; - } - for (const key in this.inputs) { - if (this.inputs[key]) { - if (key === 'z') { - if (this.target.y > 0) { - this.target.y -= this.speed; - } else { - this.target.y = 0; - } - } else if (key === 'q') { - if (this.target.x > 0) { - this.target.x -= this.speed; - } else { - this.target.x = 0; - } - } else if (key === 's') { - if (this.target.y < this.canvas.height) { - this.target.y += this.speed; - } else { - this.target.y = this.canvas.height; - } - } else if (key === 'd') { - if (this.target.x < this.canvas.width) { - this.target.x += this.speed; - - } else { - this.target.x = this.canvas.width; - } - } - - if (key === ' ') { - - } - - } else { - this.isMoving = false; - - } - } - const dx = this.target.x - this.x; - const dy = this.target.y - this.y; - if (dx !== 0 || dy !== 0) { - const angle = Math.atan2(dy, dx); - this.velocity.x = Math.cos(angle) * this.speed; - this.velocity.y = Math.sin(angle) * this.speed; - if (Math.abs(dx) < Math.abs(this.velocity.x)) { - this.x = this.target.x; - } else { - this.x += this.velocity.x; - } - if (Math.abs(dy) < Math.abs(this.velocity.y)) { - this.y = this.target.y; - } else { - this.y += this.velocity.y; - } - } - } - - clickEvent(): void { - this.canvas.addEventListener('mousedown', () => { - this.hookMouseDown(); - this.isFiring = true; - }); - this.canvas.addEventListener('mouseup', () => { - this.hookMouseUp(); - this.isFiring = false; - }); - } - - hookMouseDown(): void {} - hookMouseUp(): void {} - hookKeyDown(): void {} - hookKeyUp(): void {} - - -} \ No newline at end of file diff --git a/src/models/Projectile.ts b/src/models/Projectile.ts deleted file mode 100644 index e5036bf..0000000 --- a/src/models/Projectile.ts +++ /dev/null @@ -1,20 +0,0 @@ -import Entity from "./Entity"; -import State from "../vendor/State"; - -export class Projectile extends Entity { - launchAngle: number = 0; - isLaunch: boolean = false; - velocity = { - x: 0, - y: 0 - } - - constructor(x: number, y: number, radius: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: State) { - super(x, y, radius, context, canvas, state); - } - - public isOutOfBounds(): boolean { - return this.x < 0 || this.x > this.canvas.width || this.y < 0 || this.y > this.canvas.height; - } - -} \ No newline at end of file diff --git a/src/models/Prop.ts b/src/models/Prop.ts deleted file mode 100644 index 4875770..0000000 --- a/src/models/Prop.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Entity from "./Entity"; - -export default class Prop extends Entity { - - constructor(x: number, y: number, radius: number, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, state: any) { - super(x, y, radius, context, canvas, state); - this.sprite.src = 'assets/prop.png'; - } - - public draw(): void { - this.context.save(); - this.context.translate(this.x, this.y); - this.context.rotate(this.angle); - this.context.scale(this.scale, this.scale); - this.context.drawImage(this.sprite, this.frame * 16, 0, 16, 16, -this.radius, -this.radius, this.radius * 2, this.radius * 2); - this.context.restore(); - } - - public update(): void { - this.draw(); - } -} \ No newline at end of file diff --git a/src/sprites/PngItem_2102882.png b/src/sprites/PngItem_2102882.png deleted file mode 100644 index 376cc2ecd95d9d9fb46d9ee67ae8a77a4ae5c7f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47521 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 diff --git a/src/sprites/player.png b/src/sprites/player.png deleted file mode 100644 index 02d6fc0e0d77b80c0f9dd958f92e94e322338a89..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16491 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 diff --git a/src/utils/Event.ts b/src/utils/Event.ts deleted file mode 100644 index f11d90a..0000000 --- a/src/utils/Event.ts +++ /dev/null @@ -1,19 +0,0 @@ -export class Event { - - public static debounce(func: Function, wait: number, immediate: boolean): Function { - let timeout: any; - return function () { - // @ts-ignore - const context = this as Function, args = arguments; - const later = function () { - timeout = null; - if (!immediate) func.apply(context, args); - }; - const callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) func.apply(context, args); - }; - } - -} diff --git a/src/utils/SpriteSheet.ts b/src/utils/SpriteSheet.ts deleted file mode 100644 index 8e3db36..0000000 --- a/src/utils/SpriteSheet.ts +++ /dev/null @@ -1,58 +0,0 @@ -export class SpriteSheet { - private matrix: number[][] = []; - rows: number = 0; - cols: number = 0; - private frames: number[] = []; - public sprite: HTMLImageElement; - public frameWidth: number; - public frameHeight: number; - - constructor(frameWidth: number, frameHeight: number, rows: number, cols: number, sprite: HTMLImageElement) { - this.frameWidth = frameWidth; - this.frameHeight = frameHeight; - this.sprite = sprite; - this.rows = rows; - this.cols = cols; - this.create(rows, cols); - } - - public create(rows: number, cols:number ): number[][] { - const matrix: number[][] = []; - for (let i = 0; i < rows; i++) { - matrix[i] = []; - for (let j = 0; j < cols; j++) { - matrix[i][j] = 0; - } - } - this.matrix = matrix; - this.rows = rows; - this.cols = cols; - return matrix; - } - - public setFrame(id: any, row: number, col: number): SpriteSheet { - this.matrix[row - 1][col - 1] = id; - this.frames.push(this.frames.length + 1); - return this; - } - - public getFramesCount(): number { - return this.frames.length; - } - - public drawFrame(id: number, context: CanvasRenderingContext2D): void { - let row: number = 0; - let col: number = 0; - for (let i = 0; i < this.matrix.length; i++) { - for (let j = 0; j < this.matrix[i].length; j++) { - if (this.matrix[i][j] === id + 1) { - row = i; - col = j; - } - } - - } - context.drawImage(this.sprite, col * this.frameWidth, row * this.frameHeight, this.frameWidth, this.frameHeight, -16, -16, this.frameWidth * 2, this.frameHeight * 2); - } - -} \ No newline at end of file diff --git a/src/vendor/Game.ts b/src/vendor/Game.ts deleted file mode 100644 index a8dec88..0000000 --- a/src/vendor/Game.ts +++ /dev/null @@ -1,54 +0,0 @@ -import Renderer from "./Renderer"; -import State from "./State"; - -export class Game { - - requestId: number | null; - isRunning : boolean; - fps: number; - then: number; - elapsed: number; - renderer: Renderer; - canvas: HTMLCanvasElement; - - constructor(canvas: HTMLCanvasElement) { - this.renderer = new Renderer(canvas, new State()) - this.then = 0; - this.elapsed = 0; - this.fps = 60; - this.requestId = 0; - this.isRunning = false; - this.canvas = canvas; - this.autoResize(); - } - - public loop(): void { - this.requestId = requestAnimationFrame(this.loop.bind(this)) - const now = performance.now(); - const delta = now - this.then - const frameInterval = 1000 / this.fps; - if (delta > frameInterval) { - this.then = now - (delta / frameInterval); - this.elapsed++ - this.renderer.render() - } - } - - public setFps(fps: number) { - this.fps = fps; - } - - public getFps() { - return this.fps; - } - - private autoResize(): void { - const resizeObserver = new ResizeObserver((entries) => { - const { width, height } = entries[0].contentRect; - this.canvas.width = width; - this.canvas.height = height; - this.renderer.render(); - }) - resizeObserver.observe(this.canvas); - } -} diff --git a/src/vendor/Renderer.ts b/src/vendor/Renderer.ts deleted file mode 100644 index 7b92476..0000000 --- a/src/vendor/Renderer.ts +++ /dev/null @@ -1,47 +0,0 @@ -import Ui from "./Ui"; -import Entity from "../models/Entity"; -import State from "./State"; -import Player from "../models/Player"; -// import Archer from "../entities/classes/Archer"; -import Warrior from "../entities/classes/Warrior"; -import Archer from "../entities/classes/Archer"; - - -export default class Renderer { - context: CanvasRenderingContext2D; - canvas: HTMLCanvasElement; - state: State; - entities: Entity[] = []; - - constructor(canvas: HTMLCanvasElement, state: State) { - const context = canvas.getContext('2d'); - if (context === null) { - throw new Error("Canvas not supported") - } - context.imageSmoothingEnabled = false; - this.context = context; - this.canvas = canvas; - this.state = state; - } - - - public render(): void { - Ui.draw(this.canvas, this.context); - let playerInstance = false; - for (let i = 0; i < this.state.entities.length; i++) { - if (this.state.entities[i] instanceof Player) { - playerInstance = true; - } - this.state.entities[i].update(); - } - - if (!playerInstance) { - // spawn player in middle of screen - this.state.addEntity(new Archer(this.canvas.width / 2, this.canvas.height / 2, this.context, this.canvas, this.state)); - } - } - -} - - - diff --git a/src/vendor/State.ts b/src/vendor/State.ts deleted file mode 100644 index cd55771..0000000 --- a/src/vendor/State.ts +++ /dev/null @@ -1,49 +0,0 @@ -import Entity from "../models/Entity"; - - -export default class State { - entities: Entity[] = []; - public mouse = { - x: 0, - y: 0 - } - - public player = { - x: 0, - y: 0, - } - - constructor() { - this.mouseMoveEvent(); - } - - - public addEntity(entity: Entity): void { - this.entities.push(entity); - } - - public removeEntity(entity: Entity): void { - for (let i = 0; i < this.entities.length; i++) { - if (this.entities[i] === entity) { - this.entities.splice(i, 1); - } - } - } - - public clear(): void { - this.entities = []; - } - - mouseMoveEvent(): void { - document.addEventListener('mousemove', (e) => { - this.mouse = { - x: e.pageX, - y: e.pageY - } - }); - } - - - - -} \ No newline at end of file diff --git a/src/vendor/Ui.ts b/src/vendor/Ui.ts deleted file mode 100644 index 5a3c3e7..0000000 --- a/src/vendor/Ui.ts +++ /dev/null @@ -1,16 +0,0 @@ - -export default class Ui { - static bg: string = '#709775'; - - public static draw(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D): void { - this.clear(canvas, context); - context.fillStyle = this.bg; - context.fillRect(0, 0, canvas.width, canvas.height); - } - - public static clear(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D): void { - context.clearRect(0, 0, canvas.width, canvas.height); - } - - -} \ No newline at end of file diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts deleted file mode 100644 index 11f02fe..0000000 --- a/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/tsconfig.json b/tsconfig.json index 1a2f4df..af4af45 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,25 +1,51 @@ { "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "module": "ESNext", - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - - /* Linting */ + /* Langage et émission */ + "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + + /* Type Checking - STRICT MODE */ "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "noImplicitThis": true, + "alwaysStrict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + + /* Émission */ + "outDir": "./dist", + "rootDir": "./src", + "sourceMap": true, + "declaration": true, + "declarationMap": true, + "removeComments": false, + + /* Autres options */ + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true }, "include": [ - "./src" + "src/**/*", + "*.ts" + ], + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts", + "vite.config.ts" ] } + diff --git a/vite.config.d.ts b/vite.config.d.ts new file mode 100644 index 0000000..b8460a1 --- /dev/null +++ b/vite.config.d.ts @@ -0,0 +1,3 @@ +declare const _default: import("vite").UserConfig; +export default _default; +//# sourceMappingURL=vite.config.d.ts.map \ No newline at end of file diff --git a/vite.config.d.ts.map b/vite.config.d.ts.map new file mode 100644 index 0000000..d4f6972 --- /dev/null +++ b/vite.config.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"vite.config.d.ts","sourceRoot":"","sources":["vite.config.ts"],"names":[],"mappings":";AAEA,wBAaG"} \ No newline at end of file diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..e1eb933 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,16 @@ +import { defineConfig } from 'vite'; +export default defineConfig({ + server: { + port: 3000, + open: true + }, + build: { + outDir: 'dist', + rollupOptions: { + input: { + main: './index.html' + } + } + } +}); +//# sourceMappingURL=vite.config.js.map \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..14668fe --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + server: { + port: 3000, + open: true + }, + build: { + outDir: 'dist', + rollupOptions: { + input: { + main: './index.html' + } + }, + // Inclut les assets dans le build + assetsInclude: ['**/*.png', '**/*.jpg', '**/*.jpeg', '**/*.gif'] + }, + // Public directory pour les assets statiques + publicDir: 'assets' +}); +