From 5c4d588a79ecd2cc10b5ce7388e763862590608f Mon Sep 17 00:00:00 2001 From: Richard Leek Date: Thu, 2 Apr 2026 10:13:57 -0400 Subject: [PATCH 001/103] Add preact and delete old UI --- .vscode/settings.json | 20 + biome.json | 3 +- index.html | 576 +----------- package.json | 4 +- pnpm-lock.yaml | 623 +++++++++++++ src/client/client.ts | 2 +- src/client/events.ts | 2 +- src/controllers/chat-controller.ts | 2 +- src/controllers/spell-controller.ts | 2 +- src/handlers/arena.ts | 2 +- src/handlers/cast.ts | 2 +- src/handlers/init.ts | 2 +- src/handlers/item.ts | 2 +- src/handlers/message.ts | 2 +- src/handlers/npc.ts | 2 +- src/handlers/party.ts | 2 +- src/handlers/talk.ts | 2 +- src/handlers/warp.ts | 2 +- src/main.ts | 415 --------- src/main.tsx | 68 ++ src/ui/bank-dialog/bank-dialog.css | 30 - src/ui/bank-dialog/bank-dialog.ts | 108 --- src/ui/bank-dialog/index.ts | 1 - src/ui/barber-dialog/barber-dialog.css | 169 ---- src/ui/barber-dialog/barber-dialog.ts | 304 ------- src/ui/barber-dialog/index.ts | 1 - src/ui/base-dialog-md.ts | 104 --- src/ui/base-ui.ts | 19 - src/ui/board-dialog/board-dialog.css | 198 ---- src/ui/board-dialog/board-dialog.ts | 314 ------- src/ui/board-dialog/index.ts | 1 - src/ui/change-password/change-password.css | 56 -- src/ui/change-password/change-password.ts | 124 --- src/ui/change-password/index.ts | 1 - src/ui/character-select/character-select.css | 86 -- src/ui/character-select/character-select.ts | 292 ------ src/ui/character-select/index.ts | 1 - src/ui/chat/chat.css | 110 --- src/ui/chat/chat.ts | 310 ------- src/ui/chat/index.ts | 1 - src/ui/chest-dialog/chest-dialog.css | 83 -- src/ui/chest-dialog/chest-dialog.ts | 113 --- src/ui/chest-dialog/index.ts | 1 - src/ui/create-account/create-account.css | 25 - src/ui/create-account/create-account.ts | 168 ---- src/ui/create-account/index.ts | 1 - src/ui/create-character/create-character.css | 45 - src/ui/create-character/create-character.ts | 316 ------- src/ui/create-character/index.ts | 1 - src/ui/exit-game/exit-game.css | 6 - src/ui/exit-game/exit-game.ts | 30 - src/ui/exit-game/index.ts | 1 - src/ui/guild-dialog/guild-dialog.css | 238 ----- src/ui/guild-dialog/guild-dialog.ts | 678 -------------- src/ui/guild-dialog/index.ts | 1 - src/ui/hotbar/hotbar.css | 75 -- src/ui/hotbar/hotbar.ts | 113 --- src/ui/hotbar/index.ts | 1 - src/ui/hud/hud.css | 89 -- src/ui/hud/hud.ts | 96 -- src/ui/hud/index.ts | 1 - src/ui/in-game-menu/in-game-menu.css | 9 - src/ui/in-game-menu/in-game-menu.ts | 70 -- src/ui/in-game-menu/index.ts | 1 - src/ui/{ui-types.ts => index.ts} | 2 + src/ui/inventory/index.ts | 1 - src/ui/inventory/inventory.css | 161 ---- src/ui/inventory/inventory.ts | 594 ------------ src/ui/item-amount-dialog/index.ts | 1 - .../item-amount-dialog/item-amount-dialog.css | 75 -- .../item-amount-dialog/item-amount-dialog.ts | 152 ---- src/ui/large-alert-small-header/index.ts | 1 - .../large-alert-small-header.css | 30 - .../large-alert-small-header.ts | 37 - src/ui/large-confirm-small-header/index.ts | 1 - .../large-confirm-small-header.css | 32 - .../large-confirm-small-header.ts | 55 -- src/ui/locker-dialog/index.ts | 1 - src/ui/locker-dialog/locker-dialog.css | 109 --- src/ui/locker-dialog/locker-dialog.ts | 153 ---- src/ui/login/index.ts | 1 - src/ui/login/login.css | 49 - src/ui/login/login.ts | 90 -- src/ui/main-menu/index.ts | 1 - src/ui/main-menu/main-menu.css | 25 - src/ui/main-menu/main-menu.ts | 54 -- src/ui/mobile-controls/index.ts | 1 - src/ui/mobile-controls/mobile-controls.css | 56 -- src/ui/mobile-controls/mobile-controls.ts | 7 - src/ui/online-list/index.ts | 1 - src/ui/online-list/online-list.css | 5 - src/ui/online-list/online-list.ts | 86 -- src/ui/paperdoll/index.ts | 1 - src/ui/paperdoll/paperdoll.css | 208 ----- src/ui/paperdoll/paperdoll.ts | 361 -------- src/ui/party-dialog/index.ts | 1 - src/ui/party-dialog/party-dialog.css | 106 --- src/ui/party-dialog/party-dialog.ts | 175 ---- src/ui/quest-dialog/index.ts | 1 - src/ui/quest-dialog/quest-dialog.css | 71 -- src/ui/quest-dialog/quest-dialog.ts | 223 ----- src/ui/shop-dialog/index.ts | 1 - src/ui/shop-dialog/shop-dialog.css | 0 src/ui/shop-dialog/shop-dialog.ts | 330 ------- src/ui/skill-master-dialog/index.ts | 1 - .../skill-master-dialog.css | 56 -- .../skill-master-dialog.ts | 550 ----------- src/ui/small-alert-large-header/index.ts | 1 - .../small-alert-large-header.css | 30 - .../small-alert-large-header.ts | 37 - src/ui/small-alert-small-header/index.ts | 1 - .../small-alert-small-header.css | 30 - .../small-alert-small-header.ts | 37 - src/ui/small-confirm/index.ts | 1 - src/ui/small-confirm/small-confirm.css | 32 - src/ui/small-confirm/small-confirm.ts | 59 -- src/ui/spell-book/index.ts | 1 - src/ui/spell-book/spell-book.css | 3 - src/ui/spell-book/spell-book.ts | 178 ---- src/ui/stats/index.ts | 1 - src/ui/stats/stats.css | 197 ---- src/ui/stats/stats.ts | 231 ----- src/ui/trade-dialog/index.ts | 1 - src/ui/trade-dialog/trade-dialog.css | 308 ------- src/ui/trade-dialog/trade-dialog.ts | 373 -------- src/ui/ui.tsx | 5 + src/ui/utils/character-icon-to-chat-icon.ts | 19 - src/ui/utils/create-menu-item.ts | 120 --- src/ui/utils/gfx-resource.ts | 175 ---- src/ui/utils/index.ts | 12 - src/wiring/client-events.ts | 285 ------ src/wiring/index.ts | 7 - src/wiring/ui-events.ts | 860 ------------------ tsconfig.json | 5 +- vite.config.ts | 2 + 135 files changed, 744 insertions(+), 12260 deletions(-) create mode 100644 .vscode/settings.json delete mode 100644 src/main.ts create mode 100644 src/main.tsx delete mode 100644 src/ui/bank-dialog/bank-dialog.css delete mode 100644 src/ui/bank-dialog/bank-dialog.ts delete mode 100644 src/ui/bank-dialog/index.ts delete mode 100644 src/ui/barber-dialog/barber-dialog.css delete mode 100644 src/ui/barber-dialog/barber-dialog.ts delete mode 100644 src/ui/barber-dialog/index.ts delete mode 100644 src/ui/base-dialog-md.ts delete mode 100644 src/ui/base-ui.ts delete mode 100644 src/ui/board-dialog/board-dialog.css delete mode 100644 src/ui/board-dialog/board-dialog.ts delete mode 100644 src/ui/board-dialog/index.ts delete mode 100644 src/ui/change-password/change-password.css delete mode 100644 src/ui/change-password/change-password.ts delete mode 100644 src/ui/change-password/index.ts delete mode 100644 src/ui/character-select/character-select.css delete mode 100644 src/ui/character-select/character-select.ts delete mode 100644 src/ui/character-select/index.ts delete mode 100644 src/ui/chat/chat.css delete mode 100644 src/ui/chat/chat.ts delete mode 100644 src/ui/chat/index.ts delete mode 100644 src/ui/chest-dialog/chest-dialog.css delete mode 100644 src/ui/chest-dialog/chest-dialog.ts delete mode 100644 src/ui/chest-dialog/index.ts delete mode 100644 src/ui/create-account/create-account.css delete mode 100644 src/ui/create-account/create-account.ts delete mode 100644 src/ui/create-account/index.ts delete mode 100644 src/ui/create-character/create-character.css delete mode 100644 src/ui/create-character/create-character.ts delete mode 100644 src/ui/create-character/index.ts delete mode 100644 src/ui/exit-game/exit-game.css delete mode 100644 src/ui/exit-game/exit-game.ts delete mode 100644 src/ui/exit-game/index.ts delete mode 100644 src/ui/guild-dialog/guild-dialog.css delete mode 100644 src/ui/guild-dialog/guild-dialog.ts delete mode 100644 src/ui/guild-dialog/index.ts delete mode 100644 src/ui/hotbar/hotbar.css delete mode 100644 src/ui/hotbar/hotbar.ts delete mode 100644 src/ui/hotbar/index.ts delete mode 100644 src/ui/hud/hud.css delete mode 100644 src/ui/hud/hud.ts delete mode 100644 src/ui/hud/index.ts delete mode 100644 src/ui/in-game-menu/in-game-menu.css delete mode 100644 src/ui/in-game-menu/in-game-menu.ts delete mode 100644 src/ui/in-game-menu/index.ts rename src/ui/{ui-types.ts => index.ts} (97%) delete mode 100644 src/ui/inventory/index.ts delete mode 100644 src/ui/inventory/inventory.css delete mode 100644 src/ui/inventory/inventory.ts delete mode 100644 src/ui/item-amount-dialog/index.ts delete mode 100644 src/ui/item-amount-dialog/item-amount-dialog.css delete mode 100644 src/ui/item-amount-dialog/item-amount-dialog.ts delete mode 100644 src/ui/large-alert-small-header/index.ts delete mode 100644 src/ui/large-alert-small-header/large-alert-small-header.css delete mode 100644 src/ui/large-alert-small-header/large-alert-small-header.ts delete mode 100644 src/ui/large-confirm-small-header/index.ts delete mode 100644 src/ui/large-confirm-small-header/large-confirm-small-header.css delete mode 100644 src/ui/large-confirm-small-header/large-confirm-small-header.ts delete mode 100644 src/ui/locker-dialog/index.ts delete mode 100644 src/ui/locker-dialog/locker-dialog.css delete mode 100644 src/ui/locker-dialog/locker-dialog.ts delete mode 100644 src/ui/login/index.ts delete mode 100644 src/ui/login/login.css delete mode 100644 src/ui/login/login.ts delete mode 100644 src/ui/main-menu/index.ts delete mode 100644 src/ui/main-menu/main-menu.css delete mode 100644 src/ui/main-menu/main-menu.ts delete mode 100644 src/ui/mobile-controls/index.ts delete mode 100644 src/ui/mobile-controls/mobile-controls.css delete mode 100644 src/ui/mobile-controls/mobile-controls.ts delete mode 100644 src/ui/online-list/index.ts delete mode 100644 src/ui/online-list/online-list.css delete mode 100644 src/ui/online-list/online-list.ts delete mode 100644 src/ui/paperdoll/index.ts delete mode 100644 src/ui/paperdoll/paperdoll.css delete mode 100644 src/ui/paperdoll/paperdoll.ts delete mode 100644 src/ui/party-dialog/index.ts delete mode 100644 src/ui/party-dialog/party-dialog.css delete mode 100644 src/ui/party-dialog/party-dialog.ts delete mode 100644 src/ui/quest-dialog/index.ts delete mode 100644 src/ui/quest-dialog/quest-dialog.css delete mode 100644 src/ui/quest-dialog/quest-dialog.ts delete mode 100644 src/ui/shop-dialog/index.ts delete mode 100644 src/ui/shop-dialog/shop-dialog.css delete mode 100644 src/ui/shop-dialog/shop-dialog.ts delete mode 100644 src/ui/skill-master-dialog/index.ts delete mode 100644 src/ui/skill-master-dialog/skill-master-dialog.css delete mode 100644 src/ui/skill-master-dialog/skill-master-dialog.ts delete mode 100644 src/ui/small-alert-large-header/index.ts delete mode 100644 src/ui/small-alert-large-header/small-alert-large-header.css delete mode 100644 src/ui/small-alert-large-header/small-alert-large-header.ts delete mode 100644 src/ui/small-alert-small-header/index.ts delete mode 100644 src/ui/small-alert-small-header/small-alert-small-header.css delete mode 100644 src/ui/small-alert-small-header/small-alert-small-header.ts delete mode 100644 src/ui/small-confirm/index.ts delete mode 100644 src/ui/small-confirm/small-confirm.css delete mode 100644 src/ui/small-confirm/small-confirm.ts delete mode 100644 src/ui/spell-book/index.ts delete mode 100644 src/ui/spell-book/spell-book.css delete mode 100644 src/ui/spell-book/spell-book.ts delete mode 100644 src/ui/stats/index.ts delete mode 100644 src/ui/stats/stats.css delete mode 100644 src/ui/stats/stats.ts delete mode 100644 src/ui/trade-dialog/index.ts delete mode 100644 src/ui/trade-dialog/trade-dialog.css delete mode 100644 src/ui/trade-dialog/trade-dialog.ts create mode 100644 src/ui/ui.tsx delete mode 100644 src/ui/utils/character-icon-to-chat-icon.ts delete mode 100644 src/ui/utils/create-menu-item.ts delete mode 100644 src/ui/utils/gfx-resource.ts delete mode 100644 src/ui/utils/index.ts delete mode 100644 src/wiring/client-events.ts delete mode 100644 src/wiring/index.ts delete mode 100644 src/wiring/ui-events.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..b3d7ba0a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,20 @@ +{ + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[json]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[css]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[html]": { + "editor.defaultFormatter": "vscode.html-language-features" + }, + "editor.formatOnSave": true, + "editor.tabSize": 2, + "editor.insertSpaces": true +} diff --git a/biome.json b/biome.json index 95adf5dd..9d95d857 100644 --- a/biome.json +++ b/biome.json @@ -84,7 +84,8 @@ }, "javascript": { "formatter": { - "quoteStyle": "single" + "quoteStyle": "single", + "jsxQuoteStyle": "single" } } } diff --git a/index.html b/index.html index 8fe8732e..9d4b52e3 100644 --- a/index.html +++ b/index.html @@ -16,581 +16,9 @@
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
- + \ No newline at end of file diff --git a/package.json b/package.json index d56859c9..40d5ed8e 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "devDependencies": { "@biomejs/biome": "2.4.10", + "@preact/preset-vite": "^2.10.5", "@types/node": "^25.5.0", "knip": "^6.2.0", "lefthook": "^2.1.4", @@ -24,7 +25,8 @@ "idb": "^8.0.3", "mitt": "^3.0.1", "notyf": "^3.10.0", - "pixi.js": "^8.17.1" + "pixi.js": "^8.17.1", + "preact": "^10.29.0" }, "packageManager": "pnpm@10.33.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 42ad59d1..29f49ee9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,10 +23,16 @@ importers: pixi.js: specifier: ^8.17.1 version: 8.17.1 + preact: + specifier: ^10.29.0 + version: 10.29.0 devDependencies: '@biomejs/biome': specifier: 2.4.10 version: 2.4.10 + '@preact/preset-vite': + specifier: ^2.10.5 + version: 2.10.5(@babel/core@7.29.0)(preact@10.29.0)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(yaml@2.8.3)) '@types/node': specifier: ^25.5.0 version: 25.5.0 @@ -45,6 +51,99 @@ importers: packages: + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-development@7.27.1': + resolution: {integrity: sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx@7.28.6': + resolution: {integrity: sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + '@biomejs/biome@2.4.10': resolution: {integrity: sha512-xxA3AphFQ1geij4JTHXv4EeSTda1IFn22ye9LdyVPoJU19fNVl0uzfEuhsfQ4Yue/0FaLs2/ccVi4UDiE7R30w==} engines: {node: '>=14.21.3'} @@ -270,6 +369,22 @@ packages: cpu: [x64] os: [win32] + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@napi-rs/wasm-runtime@1.1.1': resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} @@ -617,6 +732,29 @@ packages: '@pixi/colord@2.9.6': resolution: {integrity: sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA==} + '@preact/preset-vite@2.10.5': + resolution: {integrity: sha512-p0vJpxiVO7KWWazWny3LUZ+saXyZKWv6Ju0bYMWNJRp2YveufRPgSUB1C4MTqGJfz07EehMgfN+AJNwQy+w6Iw==} + peerDependencies: + '@babel/core': 7.x + vite: 2.x || 3.x || 4.x || 5.x || 6.x || 7.x || 8.x + + '@prefresh/babel-plugin@0.5.3': + resolution: {integrity: sha512-57LX2SHs4BX2s1IwCjNzTE2OJeEepRCNf1VTEpbNcUyHfMO68eeOWGDIt4ob9aYlW6PEWZ1SuwNikuoIXANDtQ==} + + '@prefresh/core@1.5.9': + resolution: {integrity: sha512-IKBKCPaz34OFVC+adiQ2qaTF5qdztO2/4ZPf4KsRTgjKosWqxVXmEbxCiUydYZRY8GVie+DQlKzQr9gt6HQ+EQ==} + peerDependencies: + preact: ^10.0.0 || ^11.0.0-0 + + '@prefresh/utils@1.2.1': + resolution: {integrity: sha512-vq/sIuN5nYfYzvyayXI4C2QkprfNaHUQ9ZX+3xLD8nL3rWyzpxOm1+K7RtMbhd+66QcaISViK7amjnheQ/4WZw==} + + '@prefresh/vite@2.4.12': + resolution: {integrity: sha512-FY1fzXpUjiuosznMV0YM7XAOPZjB5FIdWS0W24+XnlxYkt9hNAwwsiKYn+cuTEoMtD/ZVazS5QVssBr9YhpCQA==} + peerDependencies: + preact: ^10.4.0 || ^11.0.0-0 + vite: '>=2.0.0' + '@rolldown/binding-android-arm64@1.0.0-rc.12': resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -715,12 +853,28 @@ packages: '@rolldown/pluginutils@1.0.0-rc.12': resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==} + '@rollup/pluginutils@4.2.1': + resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} + engines: {node: '>= 8.0.0'} + + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} '@types/earcut@3.0.0': resolution: {integrity: sha512-k/9fOUGO39yd2sCjrbAJvGDEQvRwRnQIZlBz43roGwUZo5SHAmyVvSFyaVVZkicRVCaDXPKlbxrUcBuJoSWunQ==} + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/node@25.5.0': resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} @@ -731,13 +885,34 @@ packages: resolution: {integrity: sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==} engines: {node: '>=10.0.0'} + babel-plugin-transform-hook-names@1.0.2: + resolution: {integrity: sha512-5gafyjyyBTTdX/tQQ0hRgu4AhNHG/hqWi0ZZmg2xvs2FgRkJXzDNKBZCyoYqgFkovfDrgM8OoKg8karoUvWeCw==} + peerDependencies: + '@babel/core': ^7.12.10 + + baseline-browser-mapping@2.10.13: + resolution: {integrity: sha512-BL2sTuHOdy0YT1lYieUxTw/QMtPBC3pmlJC6xk8BBYVv6vcw3SGdKemQ+Xsx9ik2F/lYDO9tqsFQH1r9PFuHKw==} + engines: {node: '>=6.0.0'} + hasBin: true + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + buffer-builder@0.2.0: resolution: {integrity: sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==} + caniuse-lite@1.0.30001784: + resolution: {integrity: sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw==} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -745,13 +920,52 @@ packages: colorjs.io@0.5.2: resolution: {integrity: sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + earcut@3.0.2: resolution: {integrity: sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==} + electron-to-chromium@1.5.331: + resolution: {integrity: sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + eolib@2.0.1: resolution: {integrity: sha512-AcAhkzhado0JXxPa7q8n+KXEscQEioy/edxCHeOJIMqKgAl9Ks8+AyX3uOwwrhOhIyqAySqlftLiM3HoA862Vg==} @@ -760,6 +974,13 @@ packages: engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} @@ -796,6 +1017,10 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + get-tsconfig@4.13.7: resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} @@ -810,6 +1035,10 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + idb@8.0.3: resolution: {integrity: sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==} @@ -838,11 +1067,27 @@ packages: js-binary-schema-parser@2.0.3: resolution: {integrity: sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg==} + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + knip@6.2.0: resolution: {integrity: sha512-4OMUMJARvNble8e8TeFv12flp4fKzAITrQec1eKO4g2eA4HnNqEa8CXy2UOPLjuYuAETpe0N0r25jF9yY9FLig==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + lefthook-darwin-arm64@2.1.4: resolution: {integrity: sha512-BUAAE9+rUrjr39a+wH/1zHmGrDdwUQ2Yq/z6BQbM/yUb9qtXBRcQ5eOXxApqWW177VhGBpX31aqIlfAZ5Q7wzw==} cpu: [arm64] @@ -971,6 +1216,12 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -985,6 +1236,9 @@ packages: mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -993,9 +1247,18 @@ packages: node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-html-parser@6.1.13: + resolution: {integrity: sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==} + + node-releases@2.0.37: + resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} + notyf@3.10.0: resolution: {integrity: sha512-Mtnp+0qiZxgrH+TzVlzhWyZceHdAZ/UWK0/ju9U0HQeDpap1mZ8cC7H5wSI5mwgni6yeAjaxsTw0sbMK+aSuHw==} + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + oxc-parser@0.121.0: resolution: {integrity: sha512-ek9o58+SCv6AV7nchiAcUJy1DNE2CC5WRdBcO0mF+W4oRjNQfPO7b3pLjTHSFECpHkKGOZSQxx3hk8viIL5YCg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1024,6 +1287,9 @@ packages: resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} engines: {node: ^10 || ^12 || >=14} + preact@10.29.0: + resolution: {integrity: sha512-wSAGyk2bYR1c7t3SZ3jHcM6xy0lcBcDel6lODcs9ME6Th++Dx2KU+6D3HD8wMMKGA8Wpw7OMd3/4RGzYRpzwRg==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -1171,6 +1437,13 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + simple-code-frame@1.3.0: + resolution: {integrity: sha512-MB4pQmETUBlNs62BBeRjIFGeuy/x6gGKh7+eRUemn1rCFhqo7K+4slPqsyizCbcbYLnaYqaoZ2FWsZ/jN06D8w==} + smol-toml@1.6.1: resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} engines: {node: '>= 18'} @@ -1179,6 +1452,14 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + stack-trace@1.0.0-pre2: + resolution: {integrity: sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==} + engines: {node: '>=16'} + strip-json-comments@5.0.3: resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} engines: {node: '>=14.16'} @@ -1222,9 +1503,20 @@ packages: undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + varint@6.0.0: resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==} + vite-prerender-plugin@0.5.13: + resolution: {integrity: sha512-IKSpYkzDBsKAxa05naRbj7GvNVMSdww/Z/E89oO3xndz+gWnOBOKOAbEXv7qDhktY/j3vHgJmoV1pPzqU2tx9g==} + peerDependencies: + vite: 5.x || 6.x || 7.x || 8.x + vite@8.0.3: resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1272,16 +1564,151 @@ packages: resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} engines: {node: 20 || >=22} + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.8.3: resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} engines: {node: '>= 14.6'} hasBin: true + zimmerframe@1.1.4: + resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} + zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} snapshots: + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.2': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@biomejs/biome@2.4.10': optionalDependencies: '@biomejs/cli-darwin-arm64': 2.4.10 @@ -1414,6 +1841,25 @@ snapshots: '@esbuild/win32-x64@0.27.2': optional: true + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@napi-rs/wasm-runtime@1.1.1': dependencies: '@emnapi/core': 1.9.0 @@ -1624,6 +2070,45 @@ snapshots: '@pixi/colord@2.9.6': {} + '@preact/preset-vite@2.10.5(@babel/core@7.29.0)(preact@10.29.0)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(yaml@2.8.3))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.29.0) + '@prefresh/vite': 2.4.12(preact@10.29.0)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(yaml@2.8.3)) + '@rollup/pluginutils': 5.3.0 + babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.29.0) + debug: 4.4.3 + magic-string: 0.30.21 + picocolors: 1.1.1 + vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(yaml@2.8.3) + vite-prerender-plugin: 0.5.13(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(yaml@2.8.3)) + zimmerframe: 1.1.4 + transitivePeerDependencies: + - preact + - rollup + - supports-color + + '@prefresh/babel-plugin@0.5.3': {} + + '@prefresh/core@1.5.9(preact@10.29.0)': + dependencies: + preact: 10.29.0 + + '@prefresh/utils@1.2.1': {} + + '@prefresh/vite@2.4.12(preact@10.29.0)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(yaml@2.8.3))': + dependencies: + '@babel/core': 7.29.0 + '@prefresh/babel-plugin': 0.5.3 + '@prefresh/core': 1.5.9(preact@10.29.0) + '@prefresh/utils': 1.2.1 + '@rollup/pluginutils': 4.2.1 + preact: 10.29.0 + vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(yaml@2.8.3) + transitivePeerDependencies: + - supports-color + '@rolldown/binding-android-arm64@1.0.0-rc.12': optional: true @@ -1673,6 +2158,17 @@ snapshots: '@rolldown/pluginutils@1.0.0-rc.12': {} + '@rollup/pluginutils@4.2.1': + dependencies: + estree-walker: 2.0.2 + picomatch: 2.3.2 + + '@rollup/pluginutils@5.3.0': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.4 + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -1680,6 +2176,8 @@ snapshots: '@types/earcut@3.0.0': {} + '@types/estree@1.0.8': {} + '@types/node@25.5.0': dependencies: undici-types: 7.18.2 @@ -1688,13 +2186,31 @@ snapshots: '@xmldom/xmldom@0.8.12': {} + babel-plugin-transform-hook-names@1.0.2(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + + baseline-browser-mapping@2.10.13: {} + + boolbase@1.0.0: {} + braces@3.0.3: dependencies: fill-range: 7.1.1 + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.13 + caniuse-lite: 1.0.30001784 + electron-to-chromium: 1.5.331 + node-releases: 2.0.37 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + buffer-builder@0.2.0: optional: true + caniuse-lite@1.0.30001784: {} + chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -1703,10 +2219,48 @@ snapshots: colorjs.io@0.5.2: optional: true + convert-source-map@2.0.0: {} + + css-select@5.2.2: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-what@6.2.2: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + detect-libc@2.1.2: {} + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + earcut@3.0.2: {} + electron-to-chromium@1.5.331: {} + + entities@4.5.0: {} + eolib@2.0.1: {} esbuild@0.27.2: @@ -1739,6 +2293,10 @@ snapshots: '@esbuild/win32-x64': 0.27.2 optional: true + escalade@3.2.0: {} + + estree-walker@2.0.2: {} + eventemitter3@5.0.4: {} fast-glob@3.3.3: @@ -1772,6 +2330,8 @@ snapshots: fsevents@2.3.3: optional: true + gensync@1.0.0-beta.2: {} + get-tsconfig@4.13.7: dependencies: resolve-pkg-maps: 1.0.0 @@ -1787,6 +2347,8 @@ snapshots: has-flag@4.0.0: optional: true + he@1.2.0: {} + idb@8.0.3: {} immutable@5.1.5: @@ -1806,6 +2368,12 @@ snapshots: js-binary-schema-parser@2.0.3: {} + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json5@2.2.3: {} + knip@6.2.0: dependencies: '@nodelib/fs.walk': 1.2.8 @@ -1824,6 +2392,8 @@ snapshots: yaml: 2.8.3 zod: 4.3.6 + kolorist@1.8.0: {} + lefthook-darwin-arm64@2.1.4: optional: true @@ -1916,6 +2486,14 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + merge2@1.4.1: {} micromatch@4.0.8: @@ -1927,13 +2505,26 @@ snapshots: mitt@3.0.1: {} + ms@2.1.3: {} + nanoid@3.3.11: {} node-addon-api@7.1.1: optional: true + node-html-parser@6.1.13: + dependencies: + css-select: 5.2.2 + he: 1.2.0 + + node-releases@2.0.37: {} + notyf@3.10.0: {} + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + oxc-parser@0.121.0: dependencies: '@oxc-project/types': 0.121.0 @@ -2009,6 +2600,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + preact@10.29.0: {} + queue-microtask@1.2.3: {} readdirp@4.1.2: @@ -2146,10 +2739,20 @@ snapshots: '@parcel/watcher': 2.5.6 optional: true + semver@6.3.1: {} + + simple-code-frame@1.3.0: + dependencies: + kolorist: 1.8.0 + smol-toml@1.6.1: {} source-map-js@1.2.1: {} + source-map@0.7.6: {} + + stack-trace@1.0.0-pre2: {} + strip-json-comments@5.0.3: {} supports-color@8.1.1: @@ -2185,9 +2788,25 @@ snapshots: undici-types@7.18.2: {} + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + varint@6.0.0: optional: true + vite-prerender-plugin@0.5.13(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(yaml@2.8.3)): + dependencies: + kolorist: 1.8.0 + magic-string: 0.30.21 + node-html-parser: 6.1.13 + simple-code-frame: 1.3.0 + source-map: 0.7.6 + stack-trace: 1.0.0-pre2 + vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(yaml@2.8.3) + vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 @@ -2206,6 +2825,10 @@ snapshots: walk-up-path@4.0.0: {} + yallist@3.1.1: {} + yaml@2.8.3: {} + zimmerframe@1.1.4: {} + zod@4.3.6: {} diff --git a/src/client/client.ts b/src/client/client.ts index 5f43818e..c703a82a 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -76,7 +76,7 @@ import { MapRenderer } from '@/map'; import { MinimapRenderer } from '@/minimap'; import { CharacterDeathAnimation, NpcDeathAnimation } from '@/render'; import { playSfxById, SfxId } from '@/sfx'; -import type { ISlot } from '@/ui/ui-types'; +import type { ISlot } from '@/ui'; import type { EffectMetadata, NPCMetadata, diff --git a/src/client/events.ts b/src/client/events.ts index 6bab63df..add37d93 100644 --- a/src/client/events.ts +++ b/src/client/events.ts @@ -13,7 +13,7 @@ import type { ThreeItem, } from 'eolib'; import type { SfxId } from '@/sfx'; -import type { ChatIcon, ChatTab } from '@/ui/ui-types'; +import type { ChatIcon, ChatTab } from '@/ui'; export type ClientEvents = { error: { title: string; message: string }; diff --git a/src/controllers/chat-controller.ts b/src/controllers/chat-controller.ts index 8ced0e6f..22c6fdaf 100644 --- a/src/controllers/chat-controller.ts +++ b/src/controllers/chat-controller.ts @@ -11,7 +11,7 @@ import type { Client } from '@/client'; import { COLORS, MAX_CHAT_LENGTH } from '@/consts'; import { ChatBubble } from '@/render'; import { playSfxById, SfxId } from '@/sfx'; -import { ChatIcon, ChatTab } from '@/ui/ui-types'; +import { ChatIcon, ChatTab } from '@/ui'; import { capitalize, makeDrunk } from '@/utils'; export class ChatController { diff --git a/src/controllers/spell-controller.ts b/src/controllers/spell-controller.ts index d4842446..f6faafa6 100644 --- a/src/controllers/spell-controller.ts +++ b/src/controllers/spell-controller.ts @@ -21,7 +21,7 @@ import { NpcDeathAnimation, } from '@/render'; import { playSfxById, SfxId } from '@/sfx'; -import { SlotType } from '@/ui/ui-types'; +import { SlotType } from '@/ui'; import { getTimestamp } from './movement-controller'; enum SpellState { diff --git a/src/handlers/arena.ts b/src/handlers/arena.ts index 15231dc6..73543be8 100644 --- a/src/handlers/arena.ts +++ b/src/handlers/arena.ts @@ -8,7 +8,7 @@ import { } from 'eolib'; import type { Client } from '@/client'; import { playSfxById, SfxId } from '@/sfx'; -import { ChatIcon } from '@/ui/ui-types'; +import { ChatIcon } from '@/ui'; function handleArenaUse(client: Client, reader: EoReader) { const packet = ArenaUseServerPacket.deserialize(reader); diff --git a/src/handlers/cast.ts b/src/handlers/cast.ts index e1780738..9831f9f7 100644 --- a/src/handlers/cast.ts +++ b/src/handlers/cast.ts @@ -13,7 +13,7 @@ import { ITEM_PROTECT_TICKS_NPC } from '@/consts'; import { EOResourceID } from '@/edf'; import { EffectTargetNpc, Emote, HealthBar } from '@/render'; import { playSfxById, SfxId } from '@/sfx'; -import { ChatIcon, ChatTab } from '@/ui/ui-types'; +import { ChatIcon, ChatTab } from '@/ui'; function handleCastReply(client: Client, reader: EoReader) { const packet = CastReplyServerPacket.deserialize(reader); diff --git a/src/handlers/init.ts b/src/handlers/init.ts index 3ed4a0d0..87acb2b4 100644 --- a/src/handlers/init.ts +++ b/src/handlers/init.ts @@ -20,7 +20,7 @@ import { saveEcf, saveEif, saveEmf, saveEnf, saveEsf } from '@/db'; import { DialogResourceID, EOResourceID } from '@/edf'; import { GameState } from '@/game-state'; import { playSfxById, SfxId } from '@/sfx'; -import { ChatIcon, ChatTab } from '@/ui/ui-types'; +import { ChatIcon, ChatTab } from '@/ui'; function handleInitInit(client: Client, reader: EoReader) { const packet = InitInitServerPacket.deserialize(reader); diff --git a/src/handlers/item.ts b/src/handlers/item.ts index 0a5f9ec2..fbd74609 100644 --- a/src/handlers/item.ts +++ b/src/handlers/item.ts @@ -27,7 +27,7 @@ import { HealthBar, } from '@/render'; import { playSfxById, SfxId } from '@/sfx'; -import { ChatIcon, ChatTab } from '@/ui/ui-types'; +import { ChatIcon, ChatTab } from '@/ui'; function handleItemAdd(client: Client, reader: EoReader) { const packet = ItemAddServerPacket.deserialize(reader); diff --git a/src/handlers/message.ts b/src/handlers/message.ts index 42cb60e1..bcd3963b 100644 --- a/src/handlers/message.ts +++ b/src/handlers/message.ts @@ -7,7 +7,7 @@ import { import type { Client } from '@/client'; import { EOResourceID } from '@/edf'; import { playSfxById, SfxId } from '@/sfx'; -import { ChatIcon, ChatTab } from '@/ui/ui-types'; +import { ChatIcon, ChatTab } from '@/ui'; function handleMessagePing(client: Client) { const delta = Date.now() - client.commandController.pingStart; diff --git a/src/handlers/npc.ts b/src/handlers/npc.ts index 089f3503..04f1ac3a 100644 --- a/src/handlers/npc.ts +++ b/src/handlers/npc.ts @@ -25,7 +25,7 @@ import { NpcWalkAnimation, } from '@/render'; import { playSfxById, SfxId } from '@/sfx'; -import { ChatIcon, ChatTab } from '@/ui/ui-types'; +import { ChatIcon, ChatTab } from '@/ui'; import { capitalize, getPrevCoords } from '@/utils'; function handleNpcPlayer(client: Client, reader: EoReader) { diff --git a/src/handlers/party.ts b/src/handlers/party.ts index 4e05b235..46d7eaaa 100644 --- a/src/handlers/party.ts +++ b/src/handlers/party.ts @@ -18,7 +18,7 @@ import type { Client } from '@/client'; import { DialogResourceID, EOResourceID } from '@/edf'; import { Emote } from '@/render'; import { playSfxById, SfxId } from '@/sfx'; -import { ChatIcon, ChatTab } from '@/ui/ui-types'; +import { ChatIcon, ChatTab } from '@/ui'; import { capitalize } from '@/utils'; function handlePartyReply(client: Client, reader: EoReader) { diff --git a/src/handlers/talk.ts b/src/handlers/talk.ts index 0b039412..6a12d677 100644 --- a/src/handlers/talk.ts +++ b/src/handlers/talk.ts @@ -18,7 +18,7 @@ import { COLORS } from '@/consts'; import { EOResourceID } from '@/edf'; import { ChatBubble } from '@/render'; import { playSfxById, SfxId } from '@/sfx'; -import { ChatIcon, ChatTab } from '@/ui/ui-types'; +import { ChatIcon, ChatTab } from '@/ui'; import { capitalize } from '@/utils'; function handleTalkPlayer(client: Client, reader: EoReader) { diff --git a/src/handlers/warp.ts b/src/handlers/warp.ts index e7a0c4b6..5af8e109 100644 --- a/src/handlers/warp.ts +++ b/src/handlers/warp.ts @@ -12,7 +12,7 @@ import { getEmf } from '@/db'; import { EOResourceID } from '@/edf'; import { EffectAnimation, EffectTargetCharacter } from '@/render'; import { playSfxById, SfxId } from '@/sfx'; -import { ChatIcon, ChatTab } from '@/ui/ui-types'; +import { ChatIcon, ChatTab } from '@/ui'; function handleWarpRequest(client: Client, reader: EoReader) { const packet = WarpRequestServerPacket.deserialize(reader); diff --git a/src/main.ts b/src/main.ts deleted file mode 100644 index a9406d31..00000000 --- a/src/main.ts +++ /dev/null @@ -1,415 +0,0 @@ -import { - BigCoords, - CharacterMapInfo, - Coords, - Direction, - Emf, - EoReader, - EquipmentMapInfo, - Gender, - InitInitClientPacket, - ItemMapInfo, - ItemType, - NpcMapInfo, - SitState, -} from 'eolib'; -import './css/style.css'; -import 'notyf/notyf.min.css'; -import { PacketBus } from '@/bus'; -import { Client } from '@/client'; -import { MAX_CHALLENGE } from '@/consts'; -import { DialogResourceID } from '@/edf'; -import { GameState } from '@/game-state'; -import { BankDialog } from '@/ui/bank-dialog'; -import { BarberDialog } from '@/ui/barber-dialog'; -import { BoardDialog } from '@/ui/board-dialog'; -import { ChangePasswordForm } from '@/ui/change-password'; -import { CharacterSelect } from '@/ui/character-select'; -import { Chat } from '@/ui/chat'; -import { ChestDialog } from '@/ui/chest-dialog'; -import { CreateAccountForm } from '@/ui/create-account'; -import { CreateCharacterForm } from '@/ui/create-character'; -import { ExitGame } from '@/ui/exit-game'; -import { GuildDialog } from '@/ui/guild-dialog'; -import { Hotbar } from '@/ui/hotbar'; -import { HUD } from '@/ui/hud'; -import { InGameMenu } from '@/ui/in-game-menu'; -import { Inventory } from '@/ui/inventory'; -import { ItemAmountDialog } from '@/ui/item-amount-dialog'; -import { LargeAlertSmallHeader } from '@/ui/large-alert-small-header'; -import { LargeConfirmSmallHeader } from '@/ui/large-confirm-small-header'; -import { LockerDialog } from '@/ui/locker-dialog'; -import { LoginForm } from '@/ui/login'; -import { MainMenu } from '@/ui/main-menu'; -import { MobileControls } from '@/ui/mobile-controls'; -import { OnlineList } from '@/ui/online-list'; -import { Paperdoll } from '@/ui/paperdoll'; -import { PartyDialog } from '@/ui/party-dialog'; -import { QuestDialog } from '@/ui/quest-dialog'; -import { ShopDialog } from '@/ui/shop-dialog'; -import { SkillMasterDialog } from '@/ui/skill-master-dialog'; -import { SmallAlertLargeHeader } from '@/ui/small-alert-large-header'; -import { SmallAlertSmallHeader } from '@/ui/small-alert-small-header'; -import { SmallConfirm } from '@/ui/small-confirm'; -import { SpellBook } from '@/ui/spell-book'; -import { Stats } from '@/ui/stats'; -import { TradeDialog } from '@/ui/trade-dialog'; -import { randomRange } from '@/utils'; -import { - getReconnectAttempts, - incrementReconnectAttempts, - resetReconnectAttempts, - wireClientEvents, - wireUiEvents, -} from '@/wiring'; - -// ── Client & Mobile ────────────────────────────────────────────────────── - -const client = new Client(); -const mobileControls = new MobileControls(); - -// ── Render Loop ────────────────────────────────────────────────────────── - -let accumulator = 0; -const TICK = 120; - -// ── UI Component Instantiation ─────────────────────────────────────────── - -const mainMenu = new MainMenu(); -const loginForm = new LoginForm(); -const createAccountForm = new CreateAccountForm(client); -const characterSelect = new CharacterSelect(client); -const createCharacterForm = new CreateCharacterForm(client); -const changePasswordForm = new ChangePasswordForm(client); -const smallAlertLargeHeader = new SmallAlertLargeHeader(); -const exitGame = new ExitGame(); -const smallConfirm = new SmallConfirm(); -const chat = new Chat(client); -const inGameMenu = new InGameMenu(); -const inventory = new Inventory(client); -const stats = new Stats(client); -const onlineList = new OnlineList(client); -const paperdoll = new Paperdoll(client); -const hud = new HUD(); -const itemAmountDialog = new ItemAmountDialog(); -const questDialog = new QuestDialog(client); -const chestDialog = new ChestDialog(client); -const shopDialog = new ShopDialog(client); -const boardDialog = new BoardDialog(client); -const bankDialog = new BankDialog(client); -const barberDialog = new BarberDialog(client); -const lockerDialog = new LockerDialog(client); -const skillMasterDialog = new SkillMasterDialog(client); -const smallAlert = new SmallAlertSmallHeader(); -const largeAlertSmallHeader = new LargeAlertSmallHeader(); -const largeConfirmSmallHeader = new LargeConfirmSmallHeader(); -const hotbar = new Hotbar(client); -const spellBook = new SpellBook(client); -const partyDialog = new PartyDialog(client); -new TradeDialog(client); -new GuildDialog(client); - -// ── Helpers ────────────────────────────────────────────────────────────── - -const hideAllUi = () => { - const uiElements = document.querySelectorAll('#ui>div')!; - for (const el of uiElements) { - el.classList.add('hidden'); - } - - const dialogs = document.querySelectorAll('#dialogs>div')!; - for (const el of dialogs) { - el.classList.add('hidden'); - } -}; - -// ── Socket / Reconnect ─────────────────────────────────────────────────── - -const reconnectOverlay = document.getElementById('reconnect-overlay')!; -const MAX_RECONNECT_ATTEMPTS = 5; -let _reconnectTimer: ReturnType | null = null; - -const initializeSocket = (next: 'login' | 'create' | '' = '') => { - const socket = new WebSocket(client.config.host); - socket.addEventListener('open', () => { - if (next === 'create') { - mainMenu.hide(); - createAccountForm.show(); - } else if (next === 'login') { - if (!client.loginToken) { - mainMenu.hide(); - loginForm.show(); - } - } - - client.setBus(new PacketBus(socket)); - client.challenge = randomRange(1, MAX_CHALLENGE); - - const init = new InitInitClientPacket(); - init.challenge = client.challenge; - init.hdid = String(Math.floor(Math.random() * 2147483647)); - init.version = client.version; - client.bus.send(init); - }); - - socket.addEventListener('close', () => { - const wasInGame = client.state === GameState.InGame; - const canReconnect = - wasInGame && - client.loginToken && - client.lastCharacterId && - getReconnectAttempts() < MAX_RECONNECT_ATTEMPTS; - - client.clearBus(); - - if (canReconnect) { - client.reconnecting = true; - const attempts = incrementReconnectAttempts(); - const delay = Math.min(1000 * 2 ** (attempts - 1), 8000); - - reconnectOverlay.querySelector('.reconnect-text')!.textContent = - `Reconnecting... (${attempts}/${MAX_RECONNECT_ATTEMPTS})`; - reconnectOverlay.classList.remove('hidden'); - - console.log( - `WebSocket closed while in-game. Reconnect attempt ${attempts}/${MAX_RECONNECT_ATTEMPTS} in ${delay}ms`, - ); - - _reconnectTimer = setTimeout(() => { - initializeSocket('login'); - }, delay); - } else { - client.reconnecting = false; - resetReconnectAttempts(); - reconnectOverlay.classList.add('hidden'); - - hideAllUi(); - mainMenu.show(); - if (wasInGame || client.state !== GameState.Initial) { - client.setState(GameState.Initial); - const text = client.getDialogStrings( - DialogResourceID.CONNECTION_LOST_CONNECTION, - ); - smallAlertLargeHeader.setContent(text[1], text[0]); - smallAlertLargeHeader.show(); - } - } - }); - - socket.addEventListener('error', (e) => { - console.error('Websocket Error', e); - }); -}; - -// ── Wire Events ────────────────────────────────────────────────────────── - -wireClientEvents({ - client, - smallAlertLargeHeader, - smallConfirm, - smallAlert, - createAccountForm, - mainMenu, - loginForm, - characterSelect, - createCharacterForm, - changePasswordForm, - chat, - hud, - hotbar, - inGameMenu, - exitGame, - inventory, - stats, - questDialog, - paperdoll, - chestDialog, - shopDialog, - bankDialog, - barberDialog, - boardDialog, - lockerDialog, - skillMasterDialog, - partyDialog, - mobileControls, - initializeSocket, -}); - -wireUiEvents({ - client, - mainMenu, - loginForm, - createAccountForm, - characterSelect, - createCharacterForm, - changePasswordForm, - exitGame, - chat, - smallConfirm, - smallAlertLargeHeader, - smallAlert, - largeAlertSmallHeader, - largeConfirmSmallHeader, - inventory, - stats, - spellBook, - onlineList, - inGameMenu, - questDialog, - shopDialog, - bankDialog, - lockerDialog, - hotbar, - itemAmountDialog, - partyDialog, - hideAllUi, - initializeSocket, -}); - -// ── DOM Init ───────────────────────────────────────────────────────────── - -function loadInventoryGrid() { - const img = new Image(); - img.src = '/gfx/gfx002/144.png'; - img.onload = () => { - const canvas: HTMLCanvasElement = document.createElement('canvas'); - canvas.width = 23; - canvas.height = 23; - const ctx = canvas.getContext('2d'); - ctx!.fillStyle! = '#000'; - ctx!.fillRect!(0, 0, 23, 23); - ctx!.drawImage!(img, 12, 10, 23, 23, 0, 0, 23, 23); - - const dataUrl = canvas.toDataURL(); - const grid = document.querySelector('#inventory .grid')!; - grid.style.background = `url(${dataUrl})`; - }; -} - -const MAX_ACCUMULATOR = TICK * 10; -window.addEventListener('DOMContentLoaded', async () => { - await client.initPixi(); - - client.app.ticker.add((ticker) => { - accumulator = Math.min(accumulator + ticker.deltaMS, MAX_ACCUMULATOR); - while (accumulator >= TICK) { - client.tick(); - accumulator -= TICK; - } - const interpolation = accumulator / TICK; - client.render(interpolation); - }); - - const response = await fetch('/maps/00005.emf'); - const map = await response.arrayBuffer(); - const reader = new EoReader(new Uint8Array(map)); - const emf = Emf.deserialize(reader); - client.setMap(emf); - - client.playerId = 0; - const character = new CharacterMapInfo(); - character.playerId = 0; - character.coords = new BigCoords(); - character.coords.x = 35; - character.coords.y = 38; - character.gender = Gender.Female; - character.sitState = SitState.Floor; - character.skin = 0; - character.hairStyle = 1; - character.hairColor = 0; - character.name = 'debug'; - character.guildTag = ' '; - character.direction = Direction.Down; - character.equipment = new EquipmentMapInfo(); - character.equipment.armor = 0; - character.equipment.weapon = 0; - character.equipment.boots = 0; - character.equipment.shield = 0; - character.equipment.hat = 0; - client.nearby.characters = [character]; - client.atlas.refresh(); - - //setTimeout(setDebugData, 300); - - loadInventoryGrid(); -}); - -function _setDebugData() { - const numCharacters = 100; - const numNpcs = 200; - const numItems = 100; - - const weapons = client.eif.items - .filter((i) => i.type === ItemType.Weapon) - .map((i) => i.spec1); - const armors = client.eif.items - .filter((i) => i.type === ItemType.Armor) - .map((i) => ({ gender: i.spec2, graphic: i.spec1 })); - const boots = client.eif.items - .filter((i) => i.type === ItemType.Boots) - .map((i) => i.spec1); - const hats = client.eif.items - .filter((i) => i.type === ItemType.Hat) - .map((i) => i.spec1); - const shields = client.eif.items - .filter((i) => i.type === ItemType.Shield) - .map((i) => i.spec1); - - for (let i = 1; i <= numCharacters; ++i) { - const character = new CharacterMapInfo(); - character.playerId = i; - character.coords = new BigCoords(); - character.name = `character${i}`; - character.guildTag = ' '; - character.coords.x = 1; - character.coords.y = 1; - character.direction = Direction.Down; - character.gender = i % 2 === 0 ? Gender.Male : Gender.Female; - character.sitState = SitState.Floor; - character.skin = randomRange(0, 6); - character.hairStyle = randomRange(0, 20); - character.hairColor = randomRange(0, 9); - character.equipment = new EquipmentMapInfo(); - - const wearableArmor = armors - .filter((a) => a.gender === character.gender) - .map((a) => a.graphic); - character.equipment.armor = - wearableArmor[Math.floor(Math.random() * wearableArmor.length)]; - - character.equipment.weapon = - weapons[Math.floor(Math.random() * weapons.length)]; - character.equipment.boots = boots[Math.floor(Math.random() * boots.length)]; - - character.equipment.hat = hats[Math.floor(Math.random() * hats.length)]; - character.equipment.shield = - shields[Math.floor(Math.random() * shields.length)]; - client.nearby.characters.push(character); - } - - const npcCount = client.enf.npcs.length; - for (let i = 1; i <= numNpcs; ++i) { - const npc = new NpcMapInfo(); - npc.index = i; - npc.id = Math.floor(Math.random() * npcCount) + 1; - npc.direction = Direction.Down; - npc.coords = new Coords(); - npc.coords.x = 1; - npc.coords.y = 1; - client.nearby.npcs.push(npc); - } - - const itemCount = client.eif.totalItemsCount; - for (let i = 1; i <= numItems; ++i) { - const item = new ItemMapInfo(); - item.uid = i; - item.id = Math.floor(Math.random() * itemCount) + 1; - item.amount = Math.floor(Math.random() * 10_000) + 1; - item.coords = new Coords(); - item.coords.x = 1; - item.coords.y = 1; - client.nearby.items.push(item); - } - - client.atlas.refresh(); -} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 00000000..f66cc767 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,68 @@ +import { + BigCoords, + CharacterMapInfo, + Direction, + Emf, + EoReader, + EquipmentMapInfo, + Gender, + SitState, +} from 'eolib'; +import './css/style.css'; +import 'notyf/notyf.min.css'; +import React from 'preact/compat'; +import { Client } from '@/client'; +import { Ui } from './ui'; + +// ── Client & Mobile ────────────────────────────────────────────────────── + +const client = new Client(); + +// ── Render Loop ────────────────────────────────────────────────────────── + +let accumulator = 0; +const TICK = 120; +const MAX_ACCUMULATOR = TICK * 10; +window.addEventListener('DOMContentLoaded', async () => { + await client.initPixi(); + + client.app.ticker.add((ticker) => { + accumulator = Math.min(accumulator + ticker.deltaMS, MAX_ACCUMULATOR); + while (accumulator >= TICK) { + client.tick(); + accumulator -= TICK; + } + const interpolation = accumulator / TICK; + client.render(interpolation); + }); + + const response = await fetch('/maps/00005.emf'); + const map = await response.arrayBuffer(); + const reader = new EoReader(new Uint8Array(map)); + const emf = Emf.deserialize(reader); + client.setMap(emf); + + client.playerId = 0; + const character = new CharacterMapInfo(); + character.playerId = 0; + character.coords = new BigCoords(); + character.coords.x = 35; + character.coords.y = 38; + character.gender = Gender.Female; + character.sitState = SitState.Floor; + character.skin = 0; + character.hairStyle = 1; + character.hairColor = 0; + character.name = 'debug'; + character.guildTag = ' '; + character.direction = Direction.Down; + character.equipment = new EquipmentMapInfo(); + character.equipment.armor = 0; + character.equipment.weapon = 0; + character.equipment.boots = 0; + character.equipment.shield = 0; + character.equipment.hat = 0; + client.nearby.characters = [character]; + client.atlas.refresh(); + React.render(, document.getElementById('ui')!); +}); diff --git a/src/ui/bank-dialog/bank-dialog.css b/src/ui/bank-dialog/bank-dialog.css deleted file mode 100644 index 56dcebf3..00000000 --- a/src/ui/bank-dialog/bank-dialog.css +++ /dev/null @@ -1,30 +0,0 @@ -#bank { - position: relative; - user-select: none; - box-sizing: border-box; - width: 272px; - height: 235px; - background-color: #000; - background-image: url("/gfx/gfx002/153.png"); -} - -#bank .balance { - position: absolute; - left: 130px; - top: 21px; - width: 120px; - color: #fff; - text-align: right; -} - -#bank button[data-id="ok"] { - position: absolute; - bottom: 15px; - left: 92px; -} - -#bank .item-list { - position: absolute; - left: 28px; - top: 58px; -} diff --git a/src/ui/bank-dialog/bank-dialog.ts b/src/ui/bank-dialog/bank-dialog.ts deleted file mode 100644 index 7b49f480..00000000 --- a/src/ui/bank-dialog/bank-dialog.ts +++ /dev/null @@ -1,108 +0,0 @@ -import mitt from 'mitt'; -import type { Client } from '@/client'; -import { EOResourceID } from '@/edf'; -import { playSfxById, SfxId } from '@/sfx'; -import { Base } from '@/ui/base-ui'; -import { createIconMenuItem } from '@/ui/utils'; - -import './bank-dialog.css'; -import { DialogIcon } from '@/ui/ui-types'; - -type Events = { - deposit: undefined; - withdraw: undefined; - upgrade: undefined; -}; - -export class BankDialog extends Base { - private client: Client; - private dialogs = document.getElementById('dialogs'); - private cover = document.querySelector('#cover'); - protected container = document.getElementById('bank')!; - private itemList = - this.container!.querySelector('.item-list'); - private balance = this.container!.querySelector('.balance'); - private emitter = mitt(); - - constructor(client: Client) { - super(); - this.client = client; - - const btnOk = this.container!.querySelector( - 'button[data-id="ok"]', - ); - btnOk!.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.hide(); - }); - - client.on('bankUpdated', () => { - this.render(); - }); - } - - on( - event: Event, - handler: (data: Events[Event]) => void, - ) { - this.emitter.on(event, handler); - } - - render() { - this.balance!.innerText = `${this.client.bankController.goldBank}`; - this.itemList!.innerHTML = ''; - - const gold = this.client.getEifRecordById(1); - if (!gold) { - throw new Error('Gold item not found'); - } - - const depositItem = createIconMenuItem( - DialogIcon.BankDeposit, - this.client.getResourceString(EOResourceID.DIALOG_BANK_DEPOSIT), - `${this.client.getResourceString(EOResourceID.DIALOG_BANK_TRANSFER)} ${gold.name} ${this.client.getResourceString(EOResourceID.DIALOG_BANK_TO_ACCOUNT)}`, - ); - depositItem.addEventListener('click', () => { - this.emitter.emit('deposit', undefined); - }); - this.itemList!.appendChild(depositItem); - - const withdrawItem = createIconMenuItem( - DialogIcon.BankWithdraw, - this.client.getResourceString(EOResourceID.DIALOG_BANK_WITHDRAW), - `${this.client.getResourceString(EOResourceID.DIALOG_BANK_TRANSFER)} ${gold.name} ${this.client.getResourceString(EOResourceID.DIALOG_BANK_FROM_ACCOUNT)}`, - ); - withdrawItem.addEventListener('click', () => { - this.emitter.emit('withdraw', undefined); - }); - this.itemList!.appendChild(withdrawItem); - - const upgradeItem = createIconMenuItem( - DialogIcon.BankLockerUpgrade, - this.client.getResourceString(EOResourceID.DIALOG_BANK_LOCKER_UPGRADE), - this.client.getResourceString(EOResourceID.DIALOG_BANK_MORE_SPACE), - ); - upgradeItem.addEventListener('click', () => { - this.emitter.emit('upgrade', undefined); - }); - this.itemList!.appendChild(upgradeItem); - } - - show() { - this.render(); - this.cover!.classList.remove('hidden'); - this.container!.classList.remove('hidden'); - this.dialogs!.classList.remove('hidden'); - this.client.typing = true; - } - - hide() { - this.cover!.classList.add('hidden'); - this.container!.classList.add('hidden'); - - if (!document.querySelector('#dialogs > div:not(.hidden)')) { - this.dialogs!.classList.add('hidden'); - this.client.typing = false; - } - } -} diff --git a/src/ui/bank-dialog/index.ts b/src/ui/bank-dialog/index.ts deleted file mode 100644 index c0d6b367..00000000 --- a/src/ui/bank-dialog/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './bank-dialog'; diff --git a/src/ui/barber-dialog/barber-dialog.css b/src/ui/barber-dialog/barber-dialog.css deleted file mode 100644 index e5321f59..00000000 --- a/src/ui/barber-dialog/barber-dialog.css +++ /dev/null @@ -1,169 +0,0 @@ -#barber-dialog { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 320px; - background: linear-gradient(145deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); - border: 1px solid rgba(212, 184, 150, 0.3); - border-radius: 8px; - box-shadow: - 0 8px 32px rgba(0, 0, 0, 0.6), - inset 0 1px 0 rgba(255, 255, 255, 0.05); - color: #e0daca; - font-family: "MS Sans Serif", Tahoma, sans-serif; - font-size: 12px; - overflow: hidden; - z-index: 100; -} - -#barber-dialog .barber-header { - background: linear-gradient(90deg, rgba(212, 184, 150, 0.15), transparent); - padding: 8px 12px; - border-bottom: 1px solid rgba(212, 184, 150, 0.2); - font-size: 13px; - font-weight: bold; - letter-spacing: 0.5px; - color: #d4b896; -} - -#barber-dialog .barber-preview { - display: flex; - justify-content: center; - align-items: center; - padding: 16px 0 8px; - min-height: 80px; -} - -#barber-dialog .barber-preview img { - image-rendering: pixelated; - width: 116px; - height: 196px; - border-radius: 4px; -} - -#barber-dialog .barber-controls { - padding: 4px 16px 12px; -} - -#barber-dialog .barber-row { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 8px; -} - -#barber-dialog .barber-row-label { - color: #a89b8c; - font-size: 11px; - min-width: 70px; -} - -#barber-dialog .barber-row-controls { - display: flex; - align-items: center; - gap: 8px; -} - -#barber-dialog .barber-row-controls span { - min-width: 20px; - text-align: center; - font-size: 12px; - color: #e0daca; -} - -#barber-dialog .barber-arrow { - width: 22px; - height: 22px; - border: 1px solid rgba(212, 184, 150, 0.3); - border-radius: 3px; - background: rgba(212, 184, 150, 0.08); - color: #d4b896; - font-size: 12px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: background 0.15s; - padding: 0; - line-height: 1; -} - -#barber-dialog .barber-arrow:hover { - background: rgba(212, 184, 150, 0.2); -} - -#barber-dialog .barber-arrow:active { - background: rgba(212, 184, 150, 0.3); -} - -#barber-dialog .barber-footer { - display: flex; - justify-content: flex-end; - gap: 8px; - padding: 8px 12px; - border-top: 1px solid rgba(212, 184, 150, 0.15); - background: rgba(0, 0, 0, 0.15); -} - -#barber-dialog .barber-btn { - padding: 4px 16px; - border: 1px solid rgba(212, 184, 150, 0.3); - border-radius: 4px; - background: rgba(212, 184, 150, 0.1); - color: #d4b896; - font-family: inherit; - font-size: 11px; - cursor: pointer; - transition: - background 0.15s, - border-color 0.15s; -} - -#barber-dialog .barber-btn:hover { - background: rgba(212, 184, 150, 0.2); - border-color: rgba(212, 184, 150, 0.5); -} - -#barber-dialog .barber-btn.primary { - background: rgba(46, 125, 50, 0.3); - border-color: rgba(76, 175, 80, 0.4); - color: #a5d6a7; -} - -#barber-dialog .barber-btn.primary:hover { - background: rgba(46, 125, 50, 0.45); - border-color: rgba(76, 175, 80, 0.6); -} - -#barber-dialog .barber-price { - display: flex; - align-items: center; - justify-content: center; - padding: 4px 0 8px; - font-size: 11px; - color: #a89b8c; -} - -#barber-dialog .barber-price .gold { - color: #ffd54f; - font-weight: bold; - margin-left: 4px; -} - -#barber-dialog .barber-confirm { - padding: 12px 16px; -} - -#barber-dialog .barber-confirm-text { - text-align: center; - color: #a89b8c; - font-size: 12px; - margin: 0 0 12px; -} - -#barber-dialog .barber-confirm-buttons { - display: flex; - justify-content: center; - gap: 12px; -} diff --git a/src/ui/barber-dialog/barber-dialog.ts b/src/ui/barber-dialog/barber-dialog.ts deleted file mode 100644 index de065a53..00000000 --- a/src/ui/barber-dialog/barber-dialog.ts +++ /dev/null @@ -1,304 +0,0 @@ -import { BarberBuyClientPacket, type CharacterMapInfo, Direction } from 'eolib'; -import { CharacterFrame } from '@/atlas'; -import type { Client } from '@/client'; -import { - BARBER_BASE_COST, - BARBER_COST_PER_LEVEL, - CHARACTER_HEIGHT, - CHARACTER_WIDTH, - GAME_FPS, - MAX_HAIR_COLOR, - MAX_HAIR_STYLE, -} from '@/consts'; -import { playSfxById, SfxId } from '@/sfx'; -import { Base } from '@/ui/base-ui'; - -import './barber-dialog.css'; -import { DialogResourceID, EOResourceID } from '@/edf'; - -export class BarberDialog extends Base { - private client: Client; - protected container = document.getElementById('barber-dialog')!; - private dialogs = document.getElementById('dialogs')!; - private cover = document.querySelector('#cover')!; - private previewImage = this.container.querySelector( - '.barber-preview img', - )!; - private confirmText = this.container.querySelector( - '.barber-confirm-text', - )!; - - private hairStyle = 0; - private hairColor = 0; - private direction = Direction.Right; - - private originalHairStyle = 0; - private originalHairColor = 0; - private character: CharacterMapInfo | null = null; - private _open = false; - - private offscreen: HTMLCanvasElement; - private offscreenContext: CanvasRenderingContext2D; - private lastRenderTime: DOMHighResTimeStamp | undefined; - - private styleValue = this.container.querySelector( - '[data-id="style-val"]', - )!; - private colorValue = this.container.querySelector( - '[data-id="color-val"]', - )!; - private controls = - this.container.querySelector('.barber-controls')!; - private footer = - this.container.querySelector('.barber-footer')!; - private confirmation = - this.container.querySelector('.barber-confirm')!; - - constructor(client: Client) { - super(); - this.client = client; - - this.offscreen = document.createElement('canvas'); - this.offscreen.width = CHARACTER_WIDTH + 40; - this.offscreen.height = CHARACTER_HEIGHT + 40; - this.offscreenContext = this.offscreen.getContext('2d')!; - - this.container - .querySelector('[data-id="style-prev"]')! - .addEventListener('click', () => this.changeStyle(-1)); - this.container - .querySelector('[data-id="style-next"]')! - .addEventListener('click', () => this.changeStyle(1)); - this.container - .querySelector('[data-id="color-prev"]')! - .addEventListener('click', () => this.changeColor(-1)); - this.container - .querySelector('[data-id="color-next"]')! - .addEventListener('click', () => this.changeColor(1)); - - this.container - .querySelector('[data-id="buy"]')! - .addEventListener('click', () => this.showConfirmation()); - this.container - .querySelector('[data-id="cancel"]')! - .addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.hide(); - }); - - this.confirmation - .querySelector('[data-id="confirm-yes"]')! - .addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.buy(); - this.hideConfirmation(); - }); - this.confirmation - .querySelector('[data-id="confirm-no"]')! - .addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.hideConfirmation(); - }); - - this.previewImage.addEventListener('click', () => { - if (!this.character) return; - - this.direction = (this.direction + 1) % 4; - playSfxById(SfxId.ButtonClick); - this.applyPreview(); - }); - - this.client.on('barberPurchased', () => { - // Update originals so hide() doesn't revert back - this.originalHairStyle = this.hairStyle; - this.originalHairColor = this.hairColor; - this.hide(); - }); - } - - show() { - this.character = this.client.getCharacterById(this.client.playerId) ?? null; - if (this.character) { - this.originalHairStyle = this.character.hairStyle; - this.originalHairColor = this.character.hairColor; - this.hairStyle = this.character.hairStyle; - this.hairColor = this.character.hairColor; - this.direction = Direction.Right; - } - this._open = true; - this.updateLabels(); - this.hideConfirmation(); - this.cover.classList.remove('hidden'); - this.container.classList.remove('hidden'); - this.dialogs.classList.remove('hidden'); - this.client.typing = true; - this.lastRenderTime = undefined; - requestAnimationFrame((now) => this.renderPreview(now)); - } - - hide() { - this._open = false; - - if (this.character) { - this.character.hairStyle = this.originalHairStyle; - this.character.hairColor = this.originalHairColor; - this.client.atlas.refresh(); - } - - this.cover.classList.add('hidden'); - this.container.classList.add('hidden'); - - if (!document.querySelector('#dialogs > div:not(.hidden)')) { - this.dialogs.classList.add('hidden'); - this.client.typing = false; - } - } - - private changeStyle(delta: number) { - this.hairStyle = (this.hairStyle + delta + MAX_HAIR_STYLE) % MAX_HAIR_STYLE; - playSfxById(SfxId.ButtonClick); - this.applyPreview(); - } - - private changeColor(delta: number) { - this.hairColor = (this.hairColor + delta + MAX_HAIR_COLOR) % MAX_HAIR_COLOR; - playSfxById(SfxId.ButtonClick); - this.applyPreview(); - } - - private applyPreview() { - if (this.character) { - this.character.hairStyle = this.hairStyle; - this.character.hairColor = this.hairColor; - this.client.atlas.refresh(); - } - this.updateLabels(); - } - - private updateLabels() { - this.styleValue.textContent = String(this.hairStyle); - this.colorValue.textContent = String(this.hairColor); - } - - private calculateCost(): number { - return ( - BARBER_BASE_COST + - Math.max(this.client.level - 1, 0) * BARBER_COST_PER_LEVEL - ); - } - - private showConfirmation() { - playSfxById(SfxId.ButtonClick); - - const cost = this.calculateCost(); - const goldRecord = this.client.getEifRecordById(1); - const gold = this.client.items.find((i) => i.id === 1); - if (!gold || gold.amount < cost) { - const strings = this.client.getDialogStrings( - DialogResourceID.WARNING_YOU_HAVE_NOT_ENOUGH, - ); - this.client.emit('smallAlert', { - title: strings[0], - message: `${strings[1]} ${goldRecord?.name}`, - }); - return; - } - - this.confirmText.textContent = `${this.client.getResourceString( - EOResourceID.DIALOG_BARBER_BUY_HAIRSTYLE, - )} (${this.calculateCost()} gold)`; - - this.controls.classList.add('hidden'); - this.footer.classList.add('hidden'); - this.confirmation.classList.remove('hidden'); - } - - private hideConfirmation() { - this.confirmation.classList.add('hidden'); - this.controls.classList.remove('hidden'); - this.footer.classList.remove('hidden'); - } - - private renderPreview(now: DOMHighResTimeStamp) { - if (!this._open) return; - - if (this.lastRenderTime) { - const elapsed = now - this.lastRenderTime; - if (elapsed < GAME_FPS) { - requestAnimationFrame((n) => this.renderPreview(n)); - return; - } - } - this.lastRenderTime = now; - - this.offscreenContext.clearRect( - 0, - 0, - this.offscreen.width, - this.offscreen.height, - ); - - const downRight = [Direction.Down, Direction.Right].includes( - this.direction ?? Direction.Down, - ); - - const frame = this.client.atlas.getCharacterFrame( - this.client.playerId, - downRight - ? CharacterFrame.StandingDownRight - : CharacterFrame.StandingUpLeft, - ); - - if (!frame) { - requestAnimationFrame((n) => this.renderPreview(n)); - return; - } - - const atlas = this.client.atlas.getAtlas(frame.atlasIndex); - if (!atlas) { - requestAnimationFrame((n) => this.renderPreview(n)); - return; - } - - const mirrored = [Direction.Right, Direction.Up].includes( - this.direction ?? Direction.Down, - ); - - if (mirrored) { - this.offscreenContext.save(); - this.offscreenContext.scale(-1, 1); - this.offscreenContext.translate(-this.offscreen.width, 0); - } - - this.offscreenContext.drawImage( - atlas, - frame.x, - frame.y, - frame.w, - frame.h, - Math.floor( - (this.offscreen.width >> 1) + - (mirrored ? frame.mirroredXOffset : frame.xOffset), - ), - this.offscreen.height + frame.yOffset - 20, - frame.w, - frame.h, - ); - - if (mirrored) { - this.offscreenContext.restore(); - } - - this.previewImage.src = this.offscreen.toDataURL(); - - requestAnimationFrame((n) => this.renderPreview(n)); - } - - private buy() { - const packet = new BarberBuyClientPacket(); - packet.hairStyle = this.hairStyle; - packet.hairColor = this.hairColor; - packet.sessionId = this.client.sessionId; - this.client.bus.send(packet); - } -} diff --git a/src/ui/barber-dialog/index.ts b/src/ui/barber-dialog/index.ts deleted file mode 100644 index 83480545..00000000 --- a/src/ui/barber-dialog/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { BarberDialog } from './barber-dialog'; diff --git a/src/ui/base-dialog-md.ts b/src/ui/base-dialog-md.ts deleted file mode 100644 index 7d5d5c57..00000000 --- a/src/ui/base-dialog-md.ts +++ /dev/null @@ -1,104 +0,0 @@ -import mitt, { type EventType } from 'mitt'; -import type { Client } from '@/client'; -import { playSfxById, SfxId } from '@/sfx'; -import { Base } from './base-ui'; - -export abstract class BaseDialogMd< - TEvent extends Record, -> extends Base { - protected dialogContents: HTMLDivElement; - protected client: Client; - protected emitter = mitt(); - protected dialogs = document.getElementById('dialogs'); - - private btnCancel: HTMLButtonElement; - private label: HTMLSpanElement; - private scrollHandle: HTMLDivElement; - - constructor(client: Client, container: HTMLDivElement, labelText: string) { - super(); - this.container = container; - this.client = client; - this.dialogContents = container.querySelector('.dialog-contents')!; - this.btnCancel = container.querySelector('button[data-id="cancel"]')!; - this.scrollHandle = container.querySelector('.scroll-handle')!; - this.label = container.querySelector('.label')!; - - this.label.innerText = labelText; - - this.btnCancel.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.hide(); - }); - - this.dialogContents.addEventListener('scroll', () => { - this.setScrollThumbPosition(); - }); - - this.scrollHandle.addEventListener('pointerdown', () => { - const onPointerMove = (e: PointerEvent) => { - const rect = this.dialogContents.getBoundingClientRect(); - const min = 30; - const max = 212; - const clampedY = Math.min( - Math.max(e.clientY, rect.top + min), - rect.top + max, - ); - const scrollPercent = (clampedY - rect.top - min) / (max - min); - const scrollHeight = this.dialogContents.scrollHeight; - const clientHeight = this.dialogContents.clientHeight; - this.dialogContents.scrollTop = - scrollPercent * (scrollHeight - clientHeight); - }; - - const onPointerUp = () => { - document.removeEventListener('pointermove', onPointerMove); - document.removeEventListener('pointerup', onPointerUp); - }; - - document.addEventListener('pointermove', onPointerMove); - document.addEventListener('pointerup', onPointerUp); - }); - } - - on( - event: Event, - handler: (data: TEvent[Event]) => void, - ) { - this.emitter.on(event, handler); - } - - updateLabelText(newText: string) { - this.label.innerText = newText; - } - - setScrollThumbPosition() { - const min = 60; - const max = 212; - const scrollTop = this.dialogContents.scrollTop; - const scrollHeight = this.dialogContents.scrollHeight; - const clientHeight = this.dialogContents.clientHeight; - const scrollPercent = scrollTop / (scrollHeight - clientHeight); - const clampedPercent = Math.min(Math.max(scrollPercent, 0), 1); - const top = min + (max - min) * clampedPercent || min; - this.scrollHandle.style.top = `${top}px`; - } - - show() { - this.render(); - this.container.classList.remove('hidden'); - this.dialogs!.classList.remove('hidden'); - this.setScrollThumbPosition(); - } - - hide() { - this.container.classList.add('hidden'); - - if (!document.querySelector('#dialogs > div:not(.hidden)')) { - this.dialogs!.classList.add('hidden'); - this.client.typing = false; - } - } - - abstract render(): void; -} diff --git a/src/ui/base-ui.ts b/src/ui/base-ui.ts deleted file mode 100644 index 12f89e43..00000000 --- a/src/ui/base-ui.ts +++ /dev/null @@ -1,19 +0,0 @@ -export abstract class Base { - protected container!: Element; - - show() { - this.container.classList.remove('hidden'); - } - - hide() { - this.container.classList.add('hidden'); - } - - toggle() { - if (this.container.classList.contains('hidden')) { - this.show(); - } else { - this.hide(); - } - } -} diff --git a/src/ui/board-dialog/board-dialog.css b/src/ui/board-dialog/board-dialog.css deleted file mode 100644 index 12ada4fa..00000000 --- a/src/ui/board-dialog/board-dialog.css +++ /dev/null @@ -1,198 +0,0 @@ -#board { - position: relative; - user-select: none; - box-sizing: border-box; - width: 484px; - height: 290px; - background-color: #000; - background-image: url("/gfx/gfx002/159.png"); - background-position: 0 0; - background-repeat: no-repeat; -} - -/* Switch to bottom half of the background sprite for post view/create */ -#board[data-state="view-post"], -#board[data-state="create-post"] { - background-position: 0 -290px; -} - -/* ── Title ────────────────────────────────────────────────── */ -/* List state: X=18, Y=14 (title in header bar) */ -#board .board-title { - position: absolute; - left: 18px; - top: 14px; - width: 452px; - height: 19px; - color: #fff; - font-size: 12px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - line-height: 19px; -} - -/* View/create post: author or "Posting New Message" beside Poster Name label — X=150, Y=14 */ -#board[data-state="view-post"] .board-title, -#board[data-state="create-post"] .board-title { - left: 150px; - width: 315px; -} - -/* ── List state ───────────────────────────────────────────── */ -/* Items start at X=2, Y=48; scrollbar at X=449 so usable width=447 */ -#board .post-list { - position: absolute; - top: 48px; - left: 17px; - width: 432px; - height: 199px; - overflow-y: scroll; - overflow-x: hidden; - -ms-overflow-style: none; - scrollbar-width: none; - color: #fff; - font-size: 12px; -} - -#board .post-list::-webkit-scrollbar { - display: none; -} - -#board .post-item { - position: relative; - cursor: pointer; - height: 16px; - line-height: 16px; - white-space: nowrap; - overflow: hidden; - width: 432px; -} - -#board .post-item:hover { - background-color: rgba(255, 255, 255, 0.08); -} - -#board .post-author { - position: absolute; - left: 2px; - width: 96px; - color: #e8d0a0; - overflow: hidden; - text-overflow: ellipsis; -} - -#board .post-subject-text { - position: absolute; - left: 100px; - right: 0; - color: #c8c0b0; - overflow: hidden; - text-overflow: ellipsis; -} - -/* Scrollbar at X=449, Y=44 */ -#board .scroll-handle { - position: absolute; - left: 449px; - top: 44px; - width: 16px; - height: 15px; - background: url("/gfx/gfx002/129.png") 0 -75px; - touch-action: none; - user-select: none; -} - -#board .list-buttons { - position: absolute; - bottom: 8px; - left: 0; - width: 484px; - display: flex; - gap: 4px; - justify-content: center; -} - -/* Hide list elements when not in list state */ -#board:not([data-state="list"]) .post-list, -#board:not([data-state="list"]) .scroll-handle, -#board:not([data-state="list"]) .list-buttons { - display: none; -} - -/* ── View/create shared: subject — X=150, Y=44, W=315, H=19 ── */ -#board .post-subject-view, -#board .create-subject { - position: absolute; - top: 44px; - left: 150px; - width: 315px; - height: 19px; - background: transparent; - border: none; - color: #fff; - font-size: 12px; - outline: none; - padding: 0; - line-height: 19px; -} - -/* ── View/create shared: body — X=18, Y=80, W=430, H=168 ──── */ -#board .post-body, -#board .create-body { - position: absolute; - top: 80px; - left: 18px; - width: 430px; - height: 168px; - background: transparent; - border: none; - color: #d0c8b8; - font-size: 12px; - outline: none; - padding: 0; - font-family: inherit; - -ms-overflow-style: none; - scrollbar-width: none; -} - -#board .post-body { - overflow-y: auto; - white-space: pre-wrap; - word-break: break-word; -} - -#board .post-body::-webkit-scrollbar, -#board .create-body::-webkit-scrollbar { - display: none; -} - -#board .create-body { - resize: none; - overflow-y: auto; -} - -#board .view-buttons, -#board .create-buttons { - position: absolute; - bottom: 8px; - left: 0; - width: 484px; - display: flex; - gap: 4px; - justify-content: center; -} - -/* Hide view elements when not in view-post state */ -#board:not([data-state="view-post"]) .post-subject-view, -#board:not([data-state="view-post"]) .post-body, -#board:not([data-state="view-post"]) .view-buttons { - display: none; -} - -/* Hide create elements when not in create-post state */ -#board:not([data-state="create-post"]) .create-subject, -#board:not([data-state="create-post"]) .create-body, -#board:not([data-state="create-post"]) .create-buttons { - display: none; -} diff --git a/src/ui/board-dialog/board-dialog.ts b/src/ui/board-dialog/board-dialog.ts deleted file mode 100644 index 6c11ba00..00000000 --- a/src/ui/board-dialog/board-dialog.ts +++ /dev/null @@ -1,314 +0,0 @@ -import { AdminLevel, type BoardPostListing } from 'eolib'; -import type { Client } from '@/client'; -import { MAX_PLAYER_POSTS } from '@/consts'; -import { DialogResourceID, EOResourceID } from '@/edf'; -import { playSfxById, SfxId } from '@/sfx'; -import { Base } from '@/ui/base-ui'; -import { capitalize } from '@/utils'; - -import './board-dialog.css'; - -enum State { - List = 'list', - ViewPost = 'view-post', - CreatePost = 'create-post', -} - -export class BoardDialog extends Base { - private client: Client; - protected container = document.getElementById('board')!; - private dialogs = document.getElementById('dialogs'); - private cover = document.querySelector('#cover'); - - private txtTitle = - this.container!.querySelector('.board-title'); - - // List state - private postList = - this.container!.querySelector('.post-list'); - private scrollHandle = - this.container!.querySelector('.scroll-handle'); - private btnAdd = this.container!.querySelector( - '.list-buttons button[data-id="add"]', - ); - private btnCancel = this.container!.querySelector( - '.list-buttons button[data-id="cancel"]', - ); - - // View post state - private postSubjectView = - this.container!.querySelector('.post-subject-view'); - private postBody = - this.container!.querySelector('.post-body'); - private btnDelete = this.container!.querySelector( - '.view-buttons button[data-id="delete"]', - ); - private btnBack = this.container!.querySelector( - '.view-buttons button[data-id="back"]', - ); - - // Create post state - private createSubject = - this.container!.querySelector('.create-subject'); - private createBody = - this.container!.querySelector('.create-body'); - private btnOk = this.container!.querySelector( - '.create-buttons button[data-id="ok"]', - ); - private btnCancelCreate = this.container!.querySelector( - '.create-buttons button[data-id="cancel"]', - ); - - private state: State = State.List; - private posts: BoardPostListing[] = []; - private activePostId = 0; - - constructor(client: Client) { - super(); - this.client = client; - - this.btnCancel!.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.hide(); - }); - - this.btnAdd!.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - const playerPosts = this.posts.filter( - (p) => p.author.toLowerCase() === this.client.name.toLowerCase(), - ); - if (playerPosts.length >= MAX_PLAYER_POSTS) { - const strings = this.client.getDialogStrings( - DialogResourceID.BOARD_ERROR_TOO_MANY_MESSAGES, - ); - if (strings) { - this.client.emit('smallAlert', { - title: strings[0], - message: strings[1], - }); - } - return; - } - this.changeState(State.CreatePost); - }); - - this.btnBack!.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.changeState(State.List); - }); - - this.btnDelete!.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.client.boardController.deletePost(this.activePostId); - this.changeState(State.List); - }); - - this.btnOk!.addEventListener('click', () => { - // Play sfx before validation so the click is heard even if validation fails - playSfxById(SfxId.ButtonClick); - - const subject = this.createSubject!.value.trim(); - const body = this.createBody!.value.trim(); - - if (!subject) { - const strings = this.client.getDialogStrings( - DialogResourceID.BOARD_ERROR_NO_SUBJECT, - ); - if (strings) { - this.client.emit('smallAlert', { - title: strings[0], - message: strings[1], - }); - } - return; - } - - if (!body) { - const strings = this.client.getDialogStrings( - DialogResourceID.BOARD_ERROR_NO_MESSAGE, - ); - if (strings) { - this.client.emit('smallAlert', { - title: strings[0], - message: strings[1], - }); - } - return; - } - - this.client.boardController.createPost(subject, body); - this.hide(); - }); - - this.btnCancelCreate!.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.changeState(State.List); - }); - - const createFormElements = [this.createSubject, this.createBody]; - for (const [index, element] of createFormElements.entries()) { - element!.addEventListener('keydown', (evt) => { - const e = evt as KeyboardEvent; - if (e.key === 'Tab' && this.state === State.CreatePost) { - e.preventDefault(); - if (e.shiftKey) { - const prevIndex = - index === 0 ? createFormElements.length - 1 : index - 1; - createFormElements[prevIndex]!.focus(); - } else { - const nextIndex = - index === createFormElements.length - 1 ? 0 : index + 1; - createFormElements[nextIndex]!.focus(); - } - } - }); - } - - this.postList!.addEventListener('scroll', () => { - this.setScrollThumbPosition(); - }); - - this.scrollHandle!.addEventListener('pointerdown', () => { - const onPointerMove = (e: PointerEvent) => { - const rect = this.postList!.getBoundingClientRect(); - const min = 30; - const max = 212; - const clampedY = Math.min( - Math.max(e.clientY, rect.top + min), - rect.top + max, - ); - const scrollPercent = (clampedY - rect.top - min) / (max - min); - this.postList!.scrollTop = - scrollPercent * - (this.postList!.scrollHeight - this.postList!.clientHeight); - }; - - const onPointerUp = () => { - document.removeEventListener('pointermove', onPointerMove); - document.removeEventListener('pointerup', onPointerUp); - }; - - document.addEventListener('pointermove', onPointerMove); - document.addEventListener('pointerup', onPointerUp); - }); - - this.client.on('postRead', ({ postId, body }) => { - if (this.state !== State.ViewPost || this.activePostId !== postId) return; - this.postBody!.innerText = body; - }); - } - - setData(posts: BoardPostListing[]) { - this.posts = posts; - this.changeState(State.List); - } - - show() { - this.cover!.classList.remove('hidden'); - this.container!.classList.remove('hidden'); - this.dialogs!.classList.remove('hidden'); - this.client.typing = true; - this.setScrollThumbPosition(); - } - - hide() { - this.cover!.classList.add('hidden'); - this.container!.classList.add('hidden'); - - if (!document.querySelector('#dialogs > div:not(.hidden)')) { - this.dialogs!.classList.add('hidden'); - this.client.typing = false; - } - } - - private changeState(state: State) { - this.state = state; - this.container!.dataset.state = state; - this.render(); - } - - private setScrollThumbPosition() { - const min = 60; - const max = 212; - const scrollTop = this.postList!.scrollTop; - const scrollHeight = this.postList!.scrollHeight; - const clientHeight = this.postList!.clientHeight; - const scrollPercent = scrollTop / (scrollHeight - clientHeight); - const clampedPercent = Math.min(Math.max(scrollPercent, 0), 1); - const top = min + (max - min) * clampedPercent || min; - this.scrollHandle!.style.top = `${top}px`; - } - - private render() { - switch (this.state) { - case State.List: - this.renderList(); - break; - case State.ViewPost: - this.renderViewPost(); - break; - case State.CreatePost: - this.renderCreatePost(); - break; - } - } - - private renderList() { - this.txtTitle!.innerText = - this.client.getResourceString(EOResourceID.BOARD_TOWN_BOARD) ?? - 'Town Board'; - this.postList!.innerHTML = ''; - - for (const post of this.posts) { - const item = document.createElement('div'); - item.className = 'post-item'; - - const authorEl = document.createElement('span'); - authorEl.className = 'post-author'; - authorEl.innerText = capitalize(post.author); - - const subjectEl = document.createElement('span'); - subjectEl.className = 'post-subject-text'; - subjectEl.innerText = post.subject; - - item.appendChild(authorEl); - item.appendChild(subjectEl); - - const onClick = () => { - this.activePostId = post.postId; - this.postSubjectView!.value = post.subject; - this.postBody!.innerText = - this.client.getResourceString(EOResourceID.BOARD_LOADING_MESSAGE) ?? - 'Loading...'; - - const isOwner = - post.author.toLowerCase() === this.client.name.toLowerCase() || - this.client.admin !== AdminLevel.Player; - this.btnDelete!.classList.toggle('hidden', !isOwner); - - this.client.boardController.readPost(post.postId); - this.changeState(State.ViewPost); - }; - - item.addEventListener('click', onClick); - item.addEventListener('contextmenu', onClick); - this.postList!.appendChild(item); - } - - this.setScrollThumbPosition(); - } - - private renderViewPost() { - const post = this.posts.find((p) => p.postId === this.activePostId); - this.txtTitle!.innerText = capitalize(post?.author ?? ''); - } - - private renderCreatePost() { - this.txtTitle!.innerText = - this.client.getResourceString(EOResourceID.BOARD_POSTING_NEW_MESSAGE) ?? - 'Posting New Message'; - this.createSubject!.value = ''; - this.createBody!.value = ''; - this.createSubject!.focus(); - } -} diff --git a/src/ui/board-dialog/index.ts b/src/ui/board-dialog/index.ts deleted file mode 100644 index 4784527f..00000000 --- a/src/ui/board-dialog/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './board-dialog'; diff --git a/src/ui/change-password/change-password.css b/src/ui/change-password/change-password.css deleted file mode 100644 index 9707a8cc..00000000 --- a/src/ui/change-password/change-password.css +++ /dev/null @@ -1,56 +0,0 @@ -#change-password-form { - position: absolute; - width: 360px; - height: 240px; - background-color: #000; - background-image: url("/gfx/gfx001/121.png"); - z-index: 1020; -} - -#change-password-form input[name="username"] { - background: transparent; - border: none; - position: absolute; - color: #b4a08c; - top: 62px; - left: 200px; - width: 130px; -} - -#change-password-form input[name="old-password"] { - background: transparent; - border: none; - position: absolute; - color: #b4a08c; - top: 92px; - left: 200px; - width: 130px; -} - -#change-password-form input[name="new-password"] { - background: transparent; - border: none; - position: absolute; - color: #b4a08c; - top: 122px; - left: 200px; - width: 130px; -} - -#change-password-form input[name="confirm-new-password"] { - background: transparent; - border: none; - position: absolute; - color: #b4a08c; - top: 152px; - left: 200px; - width: 130px; -} - -#change-password-form .buttons { - position: absolute; - display: flex; - gap: 2px; - bottom: 16px; - right: 19px; -} diff --git a/src/ui/change-password/change-password.ts b/src/ui/change-password/change-password.ts deleted file mode 100644 index 44af5813..00000000 --- a/src/ui/change-password/change-password.ts +++ /dev/null @@ -1,124 +0,0 @@ -import mitt from 'mitt'; -import type { Client } from '@/client'; -import { DialogResourceID } from '@/edf'; -import { playSfxById, SfxId } from '@/sfx'; -import { Base } from '@/ui/base-ui'; - -import './change-password.css'; - -type Events = { - error: { title: string; message: string }; - changePassword: { - username: string; - oldPassword: string; - newPassword: string; - }; -}; - -export class ChangePasswordForm extends Base { - protected container = document.getElementById('change-password-form')!; - private emitter = mitt(); - private cover: HTMLDivElement = document.querySelector('#cover')!; - private username: HTMLInputElement = this.container!.querySelector( - 'input[name="username"]', - )!; - private oldPassword: HTMLInputElement = this.container!.querySelector( - 'input[name="old-password"]', - )!; - private newPassword: HTMLInputElement = this.container!.querySelector( - 'input[name="new-password"]', - )!; - private confirmNewPassword: HTMLInputElement = this.container!.querySelector( - 'input[name="confirm-new-password"]', - )!; - private form: HTMLFormElement = this.container!.querySelector('form')!; - private btnCancel: HTMLButtonElement = this.container!.querySelector( - 'button[data-id="cancel"]', - )!; - - private client: Client; - - private open = false; - isOpen(): boolean { - return this.open; - } - - on( - event: Event, - handler: (data: Events[Event]) => void, - ) { - this.emitter.on(event, handler); - } - - show() { - this.open = true; - this.username.value = ''; - this.oldPassword.value = ''; - this.newPassword.value = ''; - this.confirmNewPassword.value = ''; - this.cover.classList.remove('hidden'); - this.container!.classList.remove('hidden'); - this.container!.style.left = - `${Math.floor(window.innerWidth / 2 - this.container!.clientWidth / 2)}px`; - this.container!.style.top = - `${Math.floor(window.innerHeight / 2 - this.container!.clientHeight / 2)}px`; - this.username.focus(); - } - - hide() { - this.open = false; - this.container!.classList.add('hidden'); - this.cover.classList.add('hidden'); - } - - constructor(client: Client) { - super(); - - this.client = client; - - this.btnCancel.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.hide(); - }); - - this.form.addEventListener('submit', (e) => { - e.preventDefault(); - playSfxById(SfxId.ButtonClick); - - const username = this.username.value.trim(); - const oldPassword = this.oldPassword.value.trim(); - const newPassword = this.newPassword.value.trim(); - const confirmNewPassword = this.confirmNewPassword.value.trim(); - - if (!username || !oldPassword || !newPassword || !confirmNewPassword) { - const text = this.client.getDialogStrings( - DialogResourceID.ACCOUNT_CREATE_FIELDS_STILL_EMPTY, - ); - this.emitter.emit('error', { - title: text[0], - message: text[1], - }); - return false; - } - - if (newPassword !== confirmNewPassword) { - const text = this.client.getDialogStrings( - DialogResourceID.CHANGE_PASSWORD_MISMATCH, - ); - this.emitter.emit('error', { - title: text[0], - message: text[1], - }); - return false; - } - - this.emitter.emit('changePassword', { - username, - oldPassword, - newPassword, - }); - - return false; - }); - } -} diff --git a/src/ui/change-password/index.ts b/src/ui/change-password/index.ts deleted file mode 100644 index ad1c4149..00000000 --- a/src/ui/change-password/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './change-password'; diff --git a/src/ui/character-select/character-select.css b/src/ui/character-select/character-select.css deleted file mode 100644 index bcd46320..00000000 --- a/src/ui/character-select/character-select.css +++ /dev/null @@ -1,86 +0,0 @@ -#character-select { - display: flex; - flex-flow: column; - align-self: center; - margin: auto; - gap: 20px; -} - -#character-select .characters { - display: flex; - flex-flow: column; - align-items: center; - gap: 2px; -} - -@media screen and (min-width: 840px) { - #character-select .characters { - flex-flow: row; - } -} - -#character-select .character { - background: url("/gfx/gfx001/111.png"); - background-color: #000; - width: 276px; - height: 123px; - position: relative; - user-select: none; -} - -#character-select .character > .buttons { - position: absolute; - bottom: 8px; - left: 160px; - display: flex; - gap: 2px; - flex-flow: column; -} - -#character-select .character .name { - position: absolute; - top: 30px; - left: 160px; - width: 90px; - text-align: center; - color: #b4a08c; - font-size: 12px; -} - -#character-select .character .level { - position: absolute; - top: 98px; - left: 30px; - color: #b4a08c; - font-size: 12px; -} - -#character-select .character .preview { - position: absolute; - top: 15px; - left: 50px; -} - -#character-select .character .admin-level { - position: absolute; - top: 100px; - left: 110px; - width: 13px; - height: 13px; -} - -#character-select .character .admin-level.level-2, -#character-select .character .admin-level.level-3, -#character-select .character .admin-level.level-4 { - background-image: url("/gfx/gfx002/132.png"); - background-position: 0 -155px; -} - -#character-select .character .admin-level.level-5 { - background-image: url("/gfx/gfx002/132.png"); - background-position: 0 -181px; -} - -#character-select > .buttons { - align-self: center; -} diff --git a/src/ui/character-select/character-select.ts b/src/ui/character-select/character-select.ts deleted file mode 100644 index 80120d7e..00000000 --- a/src/ui/character-select/character-select.ts +++ /dev/null @@ -1,292 +0,0 @@ -import { - CharacterMapInfo, - type CharacterSelectionListEntry, - Direction, - EquipmentMapInfo, -} from 'eolib'; -import mitt from 'mitt'; -import { CharacterFrame } from '@/atlas'; -import type { Client } from '@/client'; -import { CHARACTER_HEIGHT, CHARACTER_WIDTH, GAME_FPS } from '@/consts'; -import { DialogResourceID } from '@/edf'; -import { playSfxById, SfxId } from '@/sfx'; -import { Base } from '@/ui/base-ui'; -import { capitalize } from '@/utils'; - -import './character-select.css'; - -type Events = { - cancel: undefined; - selectCharacter: number; - requestCharacterDeletion: { - id: number; - name: string; - }; - deleteCharacter: { - id: number; - name: string; - }; - create: undefined; - changePassword: undefined; - error: { title: string; message: string }; -}; - -let lastTime: DOMHighResTimeStamp | undefined; - -export class CharacterSelect extends Base { - protected container = document.getElementById('character-select')!; - private btnCreate: HTMLButtonElement = this.container!.querySelector( - 'button[data-id="create"]', - )!; - private btnPassword: HTMLButtonElement = this.container!.querySelector( - 'button[data-id="password"]', - )!; - private btnCancel: HTMLButtonElement = this.container!.querySelector( - 'button[data-id="cancel-big"]', - )!; - private characters: CharacterSelectionListEntry[] = []; - private canvas: HTMLCanvasElement; - private ctx: CanvasRenderingContext2D; - private open = false; - confirmed = false; - - isOpen(): boolean { - return this.open; - } - - private emitter = mitt(); - - private onLogin: ((e: Event) => undefined)[] = []; - private onDelete: ((e: Event) => undefined)[] = []; - private client: Client; - - show() { - this.container!.classList.remove('hidden'); - this.container!.style.left = - `${Math.floor(window.innerWidth / 2 - this.container!.clientWidth / 2)}px`; - this.container!.style.top = - `${Math.floor(window.innerHeight / 2 - this.container!.clientHeight / 2)}px`; - this.open = true; - for (const el of this.container!.querySelectorAll('.preview')) { - const image = el as HTMLImageElement; - image.src = ''; - } - window.requestAnimationFrame(this.render.bind(this)); - } - - hide() { - this.container!.classList.add('hidden'); - this.open = false; - } - - render(now: DOMHighResTimeStamp) { - if (!lastTime) { - lastTime = now; - } - - const ellapsed = now - lastTime; - if (ellapsed < GAME_FPS) { - requestAnimationFrame((n) => { - this.render(n); - }); - return; - } - - lastTime = now; - - for (let i = 0; i < 3; ++i) { - const preview: HTMLImageElement = this.container!.querySelectorAll( - '.preview', - )[i] as HTMLImageElement; - const adminLevel: HTMLImageElement = - this.container!.querySelector('.admin-level')!; - - const character = this.characters[i]; - if (!character) { - preview.src = ''; - adminLevel.className = 'admin-level'; - continue; - } - - const frame = this.client.atlas.getCharacterFrame( - this.client.playerId + i + 1, - CharacterFrame.StandingDownRight, - ); - if (!frame) { - continue; - } - - const atlas = this.client.atlas.getAtlas(frame.atlasIndex); - if (!atlas) { - continue; - } - - this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); - this.ctx.drawImage( - atlas, - frame.x, - frame.y, - frame.w, - frame.h, - Math.floor((this.canvas.width >> 1) - (frame.w >> 1)), - Math.floor((this.canvas.height >> 1) - (frame.h >> 1)), - frame.w, - frame.h, - ); - - if (preview) { - preview.src = this.canvas.toDataURL(); - } - - if (adminLevel) { - adminLevel.classList.add(`level-${character.admin}`); - } - } - - if (this.open) { - window.requestAnimationFrame(this.render.bind(this)); - return; - } - } - - setCharacters(characters: CharacterSelectionListEntry[]) { - this.confirmed = false; - this.characters = characters; - - this.client.nearby.characters = this.client.nearby.characters.filter( - (c) => c.playerId === this.client.playerId, - ); - this.client.nearby.characters.push( - ...this.characters.map((c, i) => { - const info = new CharacterMapInfo(); - info.playerId = this.client.playerId + i + 1; - info.name = c.name; - info.mapId = this.client.mapId; - info.direction = Direction.Down; - info.gender = c.gender; - info.hairStyle = c.hairStyle; - info.hairColor = c.hairColor; - info.skin = c.skin; - info.equipment = new EquipmentMapInfo(); - info.equipment.armor = c.equipment.armor; - info.equipment.weapon = c.equipment.weapon; - info.equipment.boots = c.equipment.boots; - info.equipment.shield = c.equipment.shield; - info.equipment.hat = c.equipment.hat; - return info; - }), - ); - - this.client.atlas.refresh(); - const characterBoxes = this.container!.querySelectorAll('.character'); - let index = 0; - for (const box of characterBoxes) { - const nameLabel: HTMLSpanElement = box.querySelector('.name')!; - nameLabel.innerText = ''; - const levelLabel: HTMLSpanElement = box.querySelector('.level')!; - levelLabel.innerText = ''; - - const btnLogin: HTMLButtonElement = box.querySelector( - 'button[data-id="login"]', - )!; - const btnDelete: HTMLButtonElement = box.querySelector( - 'button[data-id="delete"]', - )!; - - const character = characters[index]; - if (character) { - nameLabel.innerText = capitalize(character.name); - levelLabel.innerText = character.level.toString(); - } - - btnLogin.removeEventListener('click', this.onLogin[index]); - this.onLogin[index] = () => { - playSfxById(SfxId.ButtonClick); - if (!character) { - return; - } - - this.emitter.emit('selectCharacter', character.id); - }; - btnLogin.addEventListener('click', this.onLogin[index]); - - btnDelete.removeEventListener('click', this.onDelete[index]); - this.onDelete[index] = () => { - playSfxById(SfxId.ButtonClick); - if (!character) { - return; - } - - if (this.confirmed) { - this.emitter.emit('deleteCharacter', { - id: character.id, - name: character.name, - }); - } else { - this.emitter.emit('requestCharacterDeletion', { - id: character.id, - name: character.name, - }); - } - }; - btnDelete.addEventListener('click', this.onDelete[index]); - - index += 1; - } - } - - constructor(client: Client) { - super(); - - this.client = client; - - this.canvas = document.createElement('canvas'); - const w = CHARACTER_WIDTH + 40; - const h = CHARACTER_HEIGHT + 40; - this.canvas.width = w; - this.canvas.height = h; - this.ctx = this.canvas.getContext('2d')!; - - this.btnCreate.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - - if (this.characters.length >= 3) { - const text = this.client.getDialogStrings( - DialogResourceID.CHARACTER_CREATE_TOO_MANY_CHARS, - ); - this.emitter.emit('error', { - title: text[0], - message: text[1], - }); - return; - } - - this.emitter.emit('create', undefined); - }); - - this.btnPassword.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.emitter.emit('changePassword', undefined); - }); - - this.btnCancel.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.emitter.emit('cancel', undefined); - }); - } - - selectCharacter(index: number) { - const character = this.characters[index - 1]; - if (character) { - playSfxById(SfxId.ButtonClick); - this.emitter.emit('selectCharacter', character.id); - } - } - - on( - event: Event, - handler: (data: Events[Event]) => void, - ) { - this.emitter.on(event, handler); - } -} diff --git a/src/ui/character-select/index.ts b/src/ui/character-select/index.ts deleted file mode 100644 index 7ba7c285..00000000 --- a/src/ui/character-select/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './character-select'; diff --git a/src/ui/chat/chat.css b/src/ui/chat/chat.css deleted file mode 100644 index fb6cdce6..00000000 --- a/src/ui/chat/chat.css +++ /dev/null @@ -1,110 +0,0 @@ -@media screen and (min-width: 470px) { - #chat { - width: 400px !important; - left: 10px; - } -} - -@media screen and (min-width: 940px) { - #chat { - top: unset !important; - bottom: 0; - } -} - -#chat ul, -#chat ul * { - user-select: text !important; -} - -#chat ul { - overflow-y: scroll; - overflow-x: hidden; - -ms-overflow-style: none; - scrollbar-width: none; -} - -#chat ul::-webkit-scrollbar { - display: none; -} - -#chat { - position: absolute; - z-index: 1020; - top: 30px; - background: rgba(0, 0, 0, 0.4); - border-image: url("/border-thin.png") 9 / 9px 9px; - width: 200px; - color: #fff; - padding: 5px; -} - -#chat a { - color: #fff; -} - -#chat-message { - padding: 5px; - background: rgba(255, 255, 255, 0.7); - border-color: #281a00; - border-image: url("/border-thin.png") 9 / 9px 9px; - width: 100%; - height: 24px; - margin-top: 5px; - box-sizing: border-box; -} - -#btn-toggle-chat { - padding: 3px 8px; - background: #201c18; - color: #adadad; - border-image: url("/border-thin.png") 9 / 9px 9px; - outline: none; - user-select: none; - float: right; -} - -#chat-tab-bar { - margin-top: -14px; -} - -#chat-tab-bar button { - padding: 3px 8px; - background: #201c18; - color: #adadad; - border-image: url("/border-thin.png") 9 / 9px 9px; - outline: none; - user-select: none; -} - -#chat-tab-bar button.active { - color: #fff; -} - -#chat ul { - list-style: none; - padding: 0; - height: 130px; - overflow-y: scroll; -} - -#chat li { - padding-top: 4px; - padding-bottom: 2px; - word-break: break-all; - display: flex; - gap: 1px; -} - -#chat li div.msg { - max-width: 96%; -} - -#chat li div.msg > .author { - color: #f0f0c8; - padding-right: 5px; -} - -#chat li.error-message { - color: #ff4c4c; -} diff --git a/src/ui/chat/chat.ts b/src/ui/chat/chat.ts deleted file mode 100644 index 7c408bf0..00000000 --- a/src/ui/chat/chat.ts +++ /dev/null @@ -1,310 +0,0 @@ -import mitt from 'mitt'; -import type { Client } from '@/client'; -import { Base } from '@/ui/base-ui'; - -import './chat.css'; -import { ChatIcon, ChatTab } from '@/ui/ui-types'; - -type Events = { - click: undefined; - chat: string; - focus: undefined; - blur: undefined; -}; - -export class Chat extends Base { - protected container = document.getElementById('chat')!; - private form: HTMLFormElement = this.container!.querySelector('form')!; - private localChat = - this.container!.querySelector('#local-chat'); - private globalChat = - this.container!.querySelector('#global-chat'); - private groupChat = - this.container!.querySelector('#group-chat'); - private systemChat = - this.container!.querySelector('#system-chat'); - private activeChat: HTMLUListElement = this.localChat!; - private message: HTMLInputElement = this.container!.querySelector('input')!; - private emitter = mitt(); - private btnToggle: HTMLButtonElement = - this.container!.querySelector('#btn-toggle-chat')!; - private btnLocal: HTMLButtonElement = this.container!.querySelector( - '#btn-chat-tab-local', - )!; - private btnGlobal: HTMLButtonElement = this.container!.querySelector( - '#btn-chat-tab-global', - )!; - private btnGroup: HTMLButtonElement = this.container!.querySelector( - '#btn-chat-tab-group', - )!; - private btnSystem: HTMLButtonElement = this.container!.querySelector( - '#btn-chat-tab-system', - )!; - private collapsed = false; - private client: Client; - - setMessage(message: string) { - this.message.value = message; - this.focus(); - } - - addMessage(tab: ChatTab, message: string, icon: ChatIcon, name?: string) { - const li = document.createElement('li'); - li.setAttribute('data-author', name!); - - const img = document.createElement('div'); - img.classList.add('icon'); - img.setAttribute('data-id', icon.toString()); - li.appendChild(img); - - const msgContainer = document.createElement('div'); - msgContainer.classList.add('msg'); - - if (name) { - const playerName = document.createElement('span'); - playerName.classList.add('author'); - playerName.innerText = name; - - const click = () => { - let playerName = name; - if (playerName.includes('->')) { - playerName = playerName - .split('->') - .filter( - (n) => n.toLowerCase() !== this.client.name.toLowerCase(), - )[0]; - } - - this.setMessage(`!${playerName} `); - }; - - playerName.addEventListener('click', click); - playerName.addEventListener('contextmenu', (e) => { - e.preventDefault(); - click(); - }); - - msgContainer.prepend(playerName); - } - - const msg = document.createElement('span'); - msg.classList.add('chat-message'); - - const urlRegex = /\b((https?:\/\/|www\.)[^\s<]+)/gi; - - let lastIndex = 0; - let match: RegExpExecArray | null = urlRegex.exec(message); - while (match !== null) { - let rawUrl = match[0]; - - // Handle trailing punctuation (common in chat) - const trailingMatch = rawUrl.match(/[.,!?)\]}]+$/); - let trailing = ''; - - if (trailingMatch) { - trailing = trailingMatch[0]; - rawUrl = rawUrl.slice(0, -trailing.length); - } - - // Append text before the URL - if (match.index > lastIndex) { - msg.appendChild( - document.createTextNode(message.slice(lastIndex, match.index)), - ); - } - - // Normalize URL (add protocol if missing) - const normalizedUrl = rawUrl.startsWith('www.') - ? `https://${rawUrl}` - : rawUrl; - - if (this.isSafeUrl(normalizedUrl)) { - const a = document.createElement('a'); - a.href = normalizedUrl; - - // Display original text (not normalized) - a.textContent = rawUrl; - - a.target = '_blank'; - a.rel = 'noopener noreferrer nofollow'; - - msg.appendChild(a); - } else { - // Fallback: treat as plain text - msg.appendChild(document.createTextNode(rawUrl)); - } - - // Append trailing punctuation back as text - if (trailing) { - msg.appendChild(document.createTextNode(trailing)); - } - - lastIndex = match.index + match[0].length; - match = urlRegex.exec(message); - } - - // Append remaining text - if (lastIndex < message.length) { - msg.appendChild(document.createTextNode(message.slice(lastIndex))); - } - - msgContainer.appendChild(msg); - li.appendChild(msgContainer); - - if (icon === ChatIcon.Error) { - li.classList.add('error-message'); - } - - let chatWindow: HTMLUListElement; - let chatTab: HTMLButtonElement; - switch (tab) { - case ChatTab.Local: - chatWindow = this.localChat!; - chatTab = this.btnLocal; - break; - case ChatTab.Global: - chatWindow = this.globalChat!; - chatTab = this.btnGlobal; - break; - case ChatTab.Group: - chatWindow = this.groupChat!; - chatTab = this.btnGroup; - break; - case ChatTab.System: - chatWindow = this.systemChat!; - chatTab = this.btnSystem; - break; - default: - throw new Error(`Invalid chat tab: ${tab}`); - } - - chatWindow.appendChild(li); - chatWindow.scrollTo(0, chatWindow.scrollHeight); - chatTab.classList.add('active'); - } - - private isSafeUrl(url: string): boolean { - try { - const parsed = new URL(url); - - // Only allow safe protocols - return parsed.protocol === 'http:' || parsed.protocol === 'https:'; - } catch { - return false; - } - } - - clear() { - this.localChat!.innerHTML = ''; - this.globalChat!.innerHTML = ''; - this.groupChat!.innerHTML = ''; - this.systemChat!.innerHTML = ''; - } - - focus() { - this.message.focus(); - } - - constructor(client: Client) { - super(); - this.client = client; - this.form.addEventListener('submit', (e) => { - e.preventDefault(); - this.emitter.emit('chat', this.message.value); - if (!this.message.value) { - setTimeout(() => { - this.message.blur(); - }, 200); - } - this.message.value = ''; - return false; - }); - - this.message.addEventListener('keyup', (e) => { - if (e.key === 'Escape') { - this.message.value = ''; - this.message.blur(); - } - }); - - this.message.addEventListener('focus', () => { - this.emitter.emit('focus', undefined); - }); - - this.message.addEventListener('blur', () => { - this.emitter.emit('blur', undefined); - }); - - this.btnToggle.addEventListener('click', () => { - if (this.collapsed) { - this.activeChat.classList.remove('hidden'); - this.collapsed = false; - } else { - this.activeChat.classList.add('hidden'); - this.collapsed = true; - } - }); - - this.btnLocal.addEventListener('click', () => { - this.localChat!.classList.remove('hidden'); - this.globalChat!.classList.add('hidden'); - this.groupChat!.classList.add('hidden'); - this.systemChat!.classList.add('hidden'); - this.btnLocal.classList.add('active'); - this.btnGlobal.classList.remove('active'); - this.btnGroup.classList.remove('active'); - this.btnSystem.classList.remove('active'); - this.localChat!.scrollTo(0, this.localChat!.scrollHeight); - this.activeChat = this.localChat!; - this.collapsed = false; - }); - - this.btnGlobal.addEventListener('click', () => { - this.localChat!.classList.add('hidden'); - this.globalChat!.classList.remove('hidden'); - this.groupChat!.classList.add('hidden'); - this.systemChat!.classList.add('hidden'); - this.btnLocal.classList.remove('active'); - this.btnGlobal.classList.add('active'); - this.btnGroup.classList.remove('active'); - this.btnSystem.classList.remove('active'); - this.globalChat!.scrollTo(0, this.globalChat!.scrollHeight); - this.activeChat = this.globalChat!; - }); - - this.btnGroup.addEventListener('click', () => { - this.localChat!.classList.add('hidden'); - this.globalChat!.classList.add('hidden'); - this.groupChat!.classList.remove('hidden'); - this.systemChat!.classList.add('hidden'); - this.btnLocal.classList.remove('active'); - this.btnGlobal.classList.remove('active'); - this.btnGroup.classList.add('active'); - this.btnSystem.classList.remove('active'); - this.groupChat!.scrollTo(0, this.groupChat!.scrollHeight); - this.activeChat = this.groupChat!; - this.collapsed = false; - }); - - this.btnSystem.addEventListener('click', () => { - this.localChat!.classList.add('hidden'); - this.globalChat!.classList.add('hidden'); - this.groupChat!.classList.add('hidden'); - this.systemChat!.classList.remove('hidden'); - this.btnLocal.classList.remove('active'); - this.btnGlobal.classList.remove('active'); - this.btnGroup.classList.remove('active'); - this.btnSystem.classList.add('active'); - this.systemChat!.scrollTo(0, this.systemChat!.scrollHeight); - this.activeChat = this.systemChat!; - this.collapsed = false; - }); - } - - on( - event: Event, - handler: (data: Events[Event]) => void, - ) { - this.emitter.on(event, handler); - } -} diff --git a/src/ui/chat/index.ts b/src/ui/chat/index.ts deleted file mode 100644 index 043e5b39..00000000 --- a/src/ui/chat/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './chat'; diff --git a/src/ui/chest-dialog/chest-dialog.css b/src/ui/chest-dialog/chest-dialog.css deleted file mode 100644 index 34bb74a0..00000000 --- a/src/ui/chest-dialog/chest-dialog.css +++ /dev/null @@ -1,83 +0,0 @@ -#chest { - position: relative; - user-select: none; - box-sizing: border-box; - width: 272px; - height: 270px; - background-color: #000; - background-image: url("/gfx/gfx002/151.png"); -} - -#chest .chest-items { - position: absolute; - top: 25px; - left: 20px; - color: #fff; - font-size: 12px; - width: 230px; - height: 192px; -} - -#chest .chest-item { - position: relative; - display: grid; - grid-template-columns: 48px auto; - align-items: center; - padding: 0 0.5rem; - cursor: pointer; - height: 38px; - background-image: url("/gfx/gfx003/100.png"); - background-repeat: no-repeat; - background-position: left center; - background-size: auto; - image-rendering: pixelated; -} - -#chest .chest-item::before { - content: ""; - position: absolute; - inset: 0; - background-color: rgba(255, 255, 255, 0.05); - opacity: 0; - pointer-events: none; - z-index: 0; - box-sizing: border-box; -} - -#chest .chest-item:hover::before { - opacity: 1; -} - -#chest .chest-item .item-image { - max-width: 48px; - max-height: 48px; - object-fit: contain; - image-rendering: pixelated; - position: relative; - z-index: 1; - justify-self: center; -} - -#chest .chest-item .item-text { - position: relative; - z-index: 1; - flex: 1; -} - -#chest .chest-item .item-name { - color: #fff; - font-size: 12px; - margin: 0; -} - -#chest .chest-item .item-quantity { - color: #b4a08c; - font-size: 11px; - margin: 2px 0 0 0; -} - -#chest button[data-id="cancel"] { - position: absolute; - top: 227px; - left: 92px; -} diff --git a/src/ui/chest-dialog/chest-dialog.ts b/src/ui/chest-dialog/chest-dialog.ts deleted file mode 100644 index bf4b83e0..00000000 --- a/src/ui/chest-dialog/chest-dialog.ts +++ /dev/null @@ -1,113 +0,0 @@ -import type { ThreeItem } from 'eolib'; -import { Gender, ItemType } from 'eolib'; -import type { Client } from '@/client'; -import { EOResourceID } from '@/edf'; -import { playSfxById, SfxId } from '@/sfx'; -import { Base } from '@/ui/base-ui'; -import { setItemImageFromGfx } from '@/ui/utils'; - -import './chest-dialog.css'; - -export class ChestDialog extends Base { - private client: Client; - protected container = document.getElementById('chest')!; - private cover = document.querySelector('#cover'); - private btnCancel = this.container!.querySelector( - 'button[data-id="cancel"]', - ); - private dialogs = document.getElementById('dialogs'); - private itemsList = - this.container!.querySelector('.chest-items'); - private items: ThreeItem[] = []; - - constructor(client: Client) { - super(); - this.client = client; - - this.btnCancel!.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.hide(); - }); - } - - setItems(items: ThreeItem[]) { - this.items = items; - this.render(); - } - - show() { - this.cover!.classList.remove('hidden'); - this.container!.classList.remove('hidden'); - this.dialogs!.classList.remove('hidden'); - this.client.typing = true; - } - - hide() { - this.cover!.classList.add('hidden'); - this.container!.classList.add('hidden'); - - if (!document.querySelector('#dialogs > div:not(.hidden)')) { - this.dialogs!.classList.add('hidden'); - this.client.typing = false; - } - } - - private render() { - this.itemsList!.innerHTML = ''; - - if (this.items.length === 0) { - return; - } - - for (const item of this.items) { - const record = this.client.getEifRecordById(item.id); - if (!record) { - continue; - } - - const itemElement = document.createElement('div'); - itemElement.className = 'chest-item'; - - const itemImage = document.createElement('img'); - void setItemImageFromGfx( - itemImage, - item.id, - record.graphicId, - item.amount, - ); - itemImage.classList.add('item-image'); - itemElement.appendChild(itemImage); - - const itemText = document.createElement('div'); - itemText.className = 'item-text'; - - const itemNameElement = document.createElement('div'); - itemNameElement.className = 'item-name'; - itemNameElement.textContent = record.name; - itemText.appendChild(itemNameElement); - - if (item.amount) { - const quantityElement = document.createElement('div'); - quantityElement.className = 'item-quantity'; - quantityElement.textContent = `x ${item.amount} `; - - if (record.type === ItemType.Armor) { - const text = - record.spec2 === Gender.Female - ? this.client.getResourceString(EOResourceID.FEMALE) - : this.client.getResourceString(EOResourceID.MALE); - quantityElement.textContent += `(${text})`; - } - - itemText.appendChild(quantityElement); - } - - itemElement.addEventListener('contextmenu', () => { - this.client.chestController.takeItem(item.id); - }); - - itemElement.appendChild(itemText); - this.itemsList!.appendChild(itemElement); - } - } -} diff --git a/src/ui/chest-dialog/index.ts b/src/ui/chest-dialog/index.ts deleted file mode 100644 index 0855c54e..00000000 --- a/src/ui/chest-dialog/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './chest-dialog'; diff --git a/src/ui/create-account/create-account.css b/src/ui/create-account/create-account.css deleted file mode 100644 index b2b253c7..00000000 --- a/src/ui/create-account/create-account.css +++ /dev/null @@ -1,25 +0,0 @@ -#create-account-form { - display: flex; - flex-flow: column; - align-self: center; - margin: auto; -} - -#create-account-form form { - display: flex; - flex-flow: column; - align-items: center; - gap: 10px; -} - -#create-account-form form > div { - display: flex; - flex-flow: column; - gap: 5px; -} - -#create-account-form .buttons { - display: flex; - flex-flow: row; - gap: 2px; -} diff --git a/src/ui/create-account/create-account.ts b/src/ui/create-account/create-account.ts deleted file mode 100644 index 23339937..00000000 --- a/src/ui/create-account/create-account.ts +++ /dev/null @@ -1,168 +0,0 @@ -import mitt from 'mitt'; -import type { Client } from '@/client'; -import { MAX_PASSWORD_LENGTH, MAX_USERNAME_LENGTH } from '@/consts'; -import { DialogResourceID } from '@/edf'; -import { playSfxById, SfxId } from '@/sfx'; -import { Base } from '@/ui/base-ui'; - -import './create-account.css'; - -type Events = { - cancel: undefined; - error: { title: string; message: string }; - create: { - username: string; - password: string; - name: string; - location: string; - email: string; - }; -}; - -export class CreateAccountForm extends Base { - protected container = document.getElementById('create-account-form')!; - private form: HTMLFormElement = this.container!.querySelector('form')!; - private username: HTMLInputElement = - this.container!.querySelector('#create-username')!; - private password: HTMLInputElement = - this.container!.querySelector('#create-password')!; - private confirmPassword: HTMLInputElement = this.container!.querySelector( - '#create-confirm-password', - )!; - private name: HTMLInputElement = - this.container!.querySelector('#create-name')!; - private location: HTMLInputElement = - this.container!.querySelector('#create-location')!; - private email: HTMLInputElement = - this.container!.querySelector('#create-email')!; - private btnCancel: HTMLButtonElement = this.container!.querySelector( - 'button[data-id="cancel-big"]', - )!; - private emitter = mitt(); - private formElements: HTMLInputElement[]; - private client: Client; - - show() { - this.username.value = ''; - this.password.value = ''; - this.confirmPassword.value = ''; - this.name.value = ''; - this.location.value = ''; - this.email.value = ''; - this.container!.classList.remove('hidden'); - this.username.focus(); - } - - constructor(client: Client) { - super(); - - this.client = client; - - this.formElements = [ - this.username, - this.password, - this.confirmPassword, - this.name, - this.location, - this.email, - ]; - - this.username.maxLength = MAX_USERNAME_LENGTH; - this.password.maxLength = MAX_PASSWORD_LENGTH; - this.confirmPassword.maxLength = MAX_PASSWORD_LENGTH; - - this.btnCancel.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.emitter.emit('cancel'); - }); - - this.form.addEventListener('submit', (e) => { - e.preventDefault(); - playSfxById(SfxId.ButtonClick); - const username = this.username.value.trim(); - const password = this.password.value.trim(); - const confirmPassword = this.confirmPassword.value.trim(); - const name = this.name.value.trim(); - const location = this.location.value.trim(); - const email = this.email.value.trim(); - - if ( - !username || - !password || - !confirmPassword || - !name || - !location || - !email - ) { - const text = this.client.getDialogStrings( - DialogResourceID.ACCOUNT_CREATE_FIELDS_STILL_EMPTY, - ); - this.emitter.emit('error', { - title: text[0], - message: text[1], - }); - return false; - } - - if (password !== confirmPassword) { - const text = this.client.getDialogStrings( - DialogResourceID.ACCOUNT_CREATE_PASSWORD_MISMATCH, - ); - this.emitter.emit('error', { - title: text[0], - message: text[1], - }); - return false; - } - - if (!email.includes('@') || !email.includes('.')) { - const text = this.client.getDialogStrings( - DialogResourceID.ACCOUNT_CREATE_EMAIL_INVALID, - ); - this.emitter.emit('error', { - title: text[0], - message: text[1], - }); - return false; - } - - this.emitter.emit('create', { - username, - password, - name, - location, - email, - }); - return false; - }); - - this.setupTabTrapping(); - } - - private setupTabTrapping() { - this.formElements.forEach((element, index) => { - element.addEventListener('keydown', (e: KeyboardEvent) => { - if (e.key === 'Tab' && !this.container!.classList.contains('hidden')) { - e.preventDefault(); - - if (e.shiftKey) { - const prevIndex = - index === 0 ? this.formElements.length - 1 : index - 1; - this.formElements[prevIndex].focus(); - } else { - const nextIndex = - index === this.formElements.length - 1 ? 0 : index + 1; - this.formElements[nextIndex].focus(); - } - } - }); - }); - } - - on( - event: Event, - handler: (data: Events[Event]) => void, - ) { - this.emitter.on(event, handler); - } -} diff --git a/src/ui/create-account/index.ts b/src/ui/create-account/index.ts deleted file mode 100644 index 68f212a8..00000000 --- a/src/ui/create-account/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './create-account'; diff --git a/src/ui/create-character/create-character.css b/src/ui/create-character/create-character.css deleted file mode 100644 index 338e338c..00000000 --- a/src/ui/create-character/create-character.css +++ /dev/null @@ -1,45 +0,0 @@ -#create-character-form { - position: absolute; - width: 360px; - height: 240px; - background-color: #000; - background-image: url("/gfx/gfx001/120.png"); - z-index: 1020; -} - -#create-character-preview { - position: absolute; - top: 75px; - right: 47px; -} - -#create-character-name { - background: transparent; - border: none; - position: absolute; - color: #b4a08c; - top: 58px; - left: 82px; - width: 130px; -} - -#create-character-form .buttons { - position: absolute; - display: flex; - gap: 2px; - bottom: 16px; - right: 19px; -} - -#create-character-toggles { - position: absolute; - display: flex; - flex-flow: column; - top: 84px; - left: 170px; - gap: 9px; -} - -#create-character-toggles > div { - display: flex; -} diff --git a/src/ui/create-character/create-character.ts b/src/ui/create-character/create-character.ts deleted file mode 100644 index 88a79103..00000000 --- a/src/ui/create-character/create-character.ts +++ /dev/null @@ -1,316 +0,0 @@ -import { type CharacterMapInfo, Direction, Gender } from 'eolib'; -import mitt from 'mitt'; -import { CharacterFrame } from '@/atlas'; -import type { Client } from '@/client'; -import { - CHARACTER_HEIGHT, - CHARACTER_WIDTH, - GAME_FPS, - MAX_GENDER, - MAX_HAIR_COLOR, - MAX_HAIR_STYLE, - MAX_SKIN, -} from '@/consts'; -import { playSfxById, SfxId } from '@/sfx'; -import { Base } from '@/ui/base-ui'; - -import './create-character.css'; -let lastTime: DOMHighResTimeStamp | undefined; - -type Events = { - create: { - name: string; - gender: Gender; - hairStyle: number; - hairColor: number; - skin: number; - }; -}; - -export class CreateCharacterForm extends Base { - protected container = document.getElementById('create-character-form')!; - private emitter = mitt(); - private cover = document.getElementById('cover'); - private form: HTMLFormElement = this.container!.querySelector('form')!; - private preview: HTMLImageElement = this.container!.querySelector( - '#create-character-preview', - )!; - private canvas: HTMLCanvasElement; - private ctx: CanvasRenderingContext2D; - private name: HTMLInputElement = this.container!.querySelector( - '#create-character-name', - )!; - private btnCancel: HTMLButtonElement = this.container!.querySelector( - 'button[data-id="cancel"]', - )!; - private btnToggleGender: HTMLButtonElement = this.container!.querySelector( - 'button[data-toggle="gender"]', - )!; - private lblGender: HTMLDivElement = this.container!.querySelector( - 'div[data-id="gender"]', - )!; - private btnToggleHairStyle: HTMLButtonElement = this.container!.querySelector( - 'button[data-toggle="hair-style"]', - )!; - private lblHairStyle: HTMLDivElement = this.container!.querySelector( - 'div[data-id="hair-style"]', - )!; - private btnToggleHairColor: HTMLButtonElement = this.container!.querySelector( - 'button[data-toggle="hair-color"]', - )!; - private lblHairColor: HTMLDivElement = this.container!.querySelector( - 'div[data-id="hair-color"]', - )!; - private btnToggleSkin: HTMLButtonElement = this.container!.querySelector( - 'button[data-toggle="skin"]', - )!; - private lblSkin: HTMLDivElement = this.container!.querySelector( - 'div[data-id="skin"]', - )!; - private character: CharacterMapInfo | undefined; - private client: Client; - - private gender = 0; - private hairStyle = 0; - private hairColor = 0; - private skin = 0; - private open = false; - isOpen(): boolean { - return this.open; - } - - on( - event: Event, - handler: (data: Events[Event]) => void, - ) { - this.emitter.on(event, handler); - } - - render(now: DOMHighResTimeStamp) { - if (!lastTime) { - lastTime = now; - } - - const ellapsed = now - lastTime; - if (ellapsed < GAME_FPS) { - requestAnimationFrame((n) => { - this.render(n); - }); - return; - } - - lastTime = now; - - this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); - - const downRight = [Direction.Down, Direction.Right].includes( - this.character!.direction, - ); - - const frame = this.client.atlas.getCharacterFrame( - this.client.playerId, - downRight - ? CharacterFrame.StandingDownRight - : CharacterFrame.StandingUpLeft, - ); - if (!frame) { - requestAnimationFrame((n) => { - this.render(n); - }); - return; - } - - const atlas = this.client.atlas.getAtlas(frame.atlasIndex); - if (!atlas) { - requestAnimationFrame((n) => { - this.render(n); - }); - return; - } - - const mirrored = [Direction.Right, Direction.Up].includes( - this.character!.direction, - ); - - if (mirrored) { - this.ctx.save(); - this.ctx.scale(-1, 1); - this.ctx.translate(-this.canvas.width, 0); - } - - this.ctx.drawImage( - atlas, - frame.x, - frame.y, - frame.w, - frame.h, - Math.floor( - (this.canvas.width >> 1) + - (mirrored ? frame.mirroredXOffset : frame.xOffset), - ), - this.canvas.height + frame.yOffset - 20, - frame.w, - frame.h, - ); - - if (mirrored) { - this.ctx.restore(); - } - - this.preview.src = this.canvas.toDataURL(); - - if (this.open) { - window.requestAnimationFrame((now) => { - this.render(now); - }); - } - } - - show() { - /// Step 1: reuse the current hidden preview character and reset base looks. - this.name.value = ''; - this.character = this.client.getCharacterById(this.client.playerId); - this.character!.gender = Gender.Female; - this.character!.skin = 0; - this.character!.direction = Direction.Down; - this.character!.hairStyle = 1; - this.character!.hairColor = 0; - - /// Step 2: clear the preview equipment before we rebuild the atlas. - this.character!.equipment.armor = 0; - this.character!.equipment.weapon = 0; - this.character!.equipment.boots = 0; - this.character!.equipment.shield = 0; - this.character!.equipment.hat = 0; - - this.gender = 0; - this.hairStyle = 0; - this.hairColor = 0; - this.skin = 0; - - /// Step 3: wait for the atlas refresh, then show and render the dialog. - this.client.atlas.refreshAsync().then(() => { - window.requestAnimationFrame((now) => { - this.render(now); - }); - - this.cover!.classList.remove('hidden'); - this.container!.classList.remove('hidden'); - this.container!.style.left = - `${Math.floor(window.innerWidth / 2 - this.container!.clientWidth / 2)}px`; - this.container!.style.top = - `${Math.floor(window.innerHeight / 2 - this.container!.clientHeight / 2)}px`; - - this.open = true; - this.updateIcons(); - }); - } - - hide() { - this.container!.classList.add('hidden'); - this.cover!.classList.add('hidden'); - this.open = false; - } - - private updateIcons() { - this.lblGender.style.backgroundPositionX = `-${this.gender * 23}px`; - this.lblHairStyle.style.backgroundPositionX = `-${this.hairStyle * 23}px`; - this.lblHairColor.style.backgroundPositionX = `-${this.hairColor * 23}px`; - this.lblSkin.style.backgroundPositionX = `-${this.skin * 23 + 46}px`; - } - - constructor(client: Client) { - super(); - - this.client = client; - - this.canvas = document.createElement('canvas'); - this.canvas.width = CHARACTER_WIDTH + 40; - this.canvas.height = CHARACTER_HEIGHT + 40; - this.ctx = this.canvas.getContext('2d')!; - - this.btnCancel.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.hide(); - this.cover!.classList.add('hidden'); - }); - - this.preview.addEventListener('click', () => { - switch (this.character!.direction) { - case Direction.Up: - this.character!.direction = Direction.Right; - break; - case Direction.Down: - this.character!.direction = Direction.Left; - break; - case Direction.Left: - this.character!.direction = Direction.Up; - break; - case Direction.Right: - this.character!.direction = Direction.Down; - break; - } - }); - - this.form.addEventListener('submit', (e) => { - e.preventDefault(); - playSfxById(SfxId.ButtonClick); - - const name = this.name.value.trim().toLowerCase(); - if (!name) { - return false; - } - - this.emitter.emit('create', { - name, - gender: this.gender as Gender, - hairStyle: this.hairStyle + 1, - hairColor: this.hairColor, - skin: this.skin, - }); - - return false; - }); - - this.btnToggleGender.addEventListener('click', () => { - playSfxById(SfxId.TextBoxFocus); - this.gender = incrementOrWrap(this.gender, MAX_GENDER); - this.character!.gender = this.gender as Gender; - this.updateIcons(); - this.client.atlas.refresh(); - }); - - this.btnToggleHairStyle.addEventListener('click', () => { - playSfxById(SfxId.TextBoxFocus); - this.hairStyle = incrementOrWrap(this.hairStyle, MAX_HAIR_STYLE); - this.character!.hairStyle = this.hairStyle + 1; - this.updateIcons(); - this.client.atlas.refresh(); - }); - - this.btnToggleHairColor.addEventListener('click', () => { - playSfxById(SfxId.TextBoxFocus); - this.hairColor = incrementOrWrap(this.hairColor, MAX_HAIR_COLOR); - this.character!.hairColor = this.hairColor; - this.updateIcons(); - this.client.atlas.refresh(); - }); - - this.btnToggleSkin.addEventListener('click', () => { - playSfxById(SfxId.TextBoxFocus); - this.skin = incrementOrWrap(this.skin, MAX_SKIN); - this.character!.skin = this.skin; - this.updateIcons(); - this.client.atlas.refresh(); - }); - } -} - -function incrementOrWrap(value: number, max: number): number { - let result = value + 1; - if (result >= max) { - result = 0; - } - - return result; -} diff --git a/src/ui/create-character/index.ts b/src/ui/create-character/index.ts deleted file mode 100644 index f715b07f..00000000 --- a/src/ui/create-character/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './create-character'; diff --git a/src/ui/exit-game/exit-game.css b/src/ui/exit-game/exit-game.css deleted file mode 100644 index 22bb9309..00000000 --- a/src/ui/exit-game/exit-game.css +++ /dev/null @@ -1,6 +0,0 @@ -#exit-game { - position: absolute; - z-index: 1020; - top: 0; - right: 0; -} diff --git a/src/ui/exit-game/exit-game.ts b/src/ui/exit-game/exit-game.ts deleted file mode 100644 index d55212c6..00000000 --- a/src/ui/exit-game/exit-game.ts +++ /dev/null @@ -1,30 +0,0 @@ -import mitt from 'mitt'; -import { Base } from '@/ui/base-ui'; - -import './exit-game.css'; - -type Events = { - click: undefined; -}; - -export class ExitGame extends Base { - protected container = document.getElementById('exit-game')!; - private button: HTMLButtonElement = this.container!.querySelector( - 'button[data-id="exit-game"]', - )!; - private emitter = mitt(); - - constructor() { - super(); - this.button.addEventListener('click', () => { - this.emitter.emit('click', undefined); - }); - } - - on( - event: Event, - handler: (data: Events[Event]) => void, - ) { - this.emitter.on(event, handler); - } -} diff --git a/src/ui/exit-game/index.ts b/src/ui/exit-game/index.ts deleted file mode 100644 index 0aa436f5..00000000 --- a/src/ui/exit-game/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './exit-game'; diff --git a/src/ui/guild-dialog/guild-dialog.css b/src/ui/guild-dialog/guild-dialog.css deleted file mode 100644 index 6da7b3e8..00000000 --- a/src/ui/guild-dialog/guild-dialog.css +++ /dev/null @@ -1,238 +0,0 @@ -#guild-dialog { - width: 340px; - background: linear-gradient( - 180deg, - rgba(30, 26, 20, 0.97), - rgba(22, 19, 15, 0.98) - ); - border: 1px solid rgba(212, 184, 150, 0.2); - border-radius: 6px; - overflow: hidden; - font-size: 11px; - color: #e0daca; - position: relative; -} - -.guild-header { - background: linear-gradient( - 90deg, - rgba(212, 184, 150, 0.12), - rgba(212, 184, 150, 0.04) - ); - padding: 8px 12px; - font-weight: bold; - color: #d4b896; - border-bottom: 1px solid rgba(212, 184, 150, 0.15); - font-size: 12px; -} - -.guild-body { - padding: 8px; - max-height: 350px; - overflow-y: auto; -} - -.guild-footer { - padding: 8px; - border-top: 1px solid rgba(212, 184, 150, 0.1); - display: flex; - justify-content: flex-end; - gap: 6px; -} - -/* Menu buttons */ -.guild-menu-btn { - display: block; - width: 100%; - padding: 8px 12px; - margin-bottom: 4px; - background: rgba(212, 184, 150, 0.06); - border: 1px solid rgba(212, 184, 150, 0.12); - border-radius: 4px; - color: #e0daca; - cursor: pointer; - text-align: left; - font-size: 11px; - font-family: inherit; - transition: background 0.15s; -} - -.guild-menu-btn:hover { - background: rgba(212, 184, 150, 0.15); -} - -.guild-menu-btn:last-child { - margin-bottom: 0; -} - -/* Generic guild button */ -.guild-btn { - padding: 5px 12px; - background: rgba(212, 184, 150, 0.08); - border: 1px solid rgba(212, 184, 150, 0.2); - border-radius: 3px; - color: #e0daca; - cursor: pointer; - font-size: 11px; - font-family: inherit; - transition: background 0.15s; -} - -.guild-btn:hover { - background: rgba(212, 184, 150, 0.18); -} - -.guild-btn.primary { - background: rgba(212, 184, 150, 0.15); - border-color: rgba(212, 184, 150, 0.35); - color: #d4b896; -} - -.guild-btn.primary:hover { - background: rgba(212, 184, 150, 0.28); -} - -.guild-btn.danger { - border-color: rgba(244, 67, 54, 0.3); - color: #ef9a9a; -} - -.guild-btn.danger:hover { - background: rgba(244, 67, 54, 0.15); -} - -/* Info view */ -.guild-info-section { - margin-bottom: 8px; -} - -.guild-info-label { - color: #a89b8c; - font-size: 10px; - text-transform: uppercase; - letter-spacing: 0.5px; - margin-bottom: 2px; -} - -.guild-info-value { - color: #e0daca; - padding: 2px 0; -} - -.guild-info-value.highlight { - color: #d4b896; - font-weight: bold; -} - -/* Member list */ -.guild-member-row { - display: flex; - justify-content: space-between; - align-items: center; - padding: 4px 8px; - border-radius: 3px; -} - -.guild-member-row:nth-child(even) { - background: rgba(212, 184, 150, 0.04); -} - -.guild-member-name { - color: #e0daca; -} - -.guild-member-rank { - color: #a89b8c; - font-size: 10px; -} - -/* Staff list in info view */ -.guild-staff-row { - display: flex; - justify-content: space-between; - padding: 3px 6px; - border-radius: 3px; -} - -.guild-staff-row:nth-child(even) { - background: rgba(212, 184, 150, 0.04); -} - -.guild-staff-name { - color: #d4b896; -} - -.guild-staff-type { - color: #a89b8c; - font-size: 10px; -} - -/* Input fields */ -.guild-input { - width: 100%; - padding: 5px 8px; - border: 1px solid rgba(212, 184, 150, 0.2); - border-radius: 3px; - background: rgba(0, 0, 0, 0.3); - color: #e0daca; - font-family: inherit; - font-size: 11px; - box-sizing: border-box; -} - -.guild-input:focus { - outline: none; - border-color: rgba(212, 184, 150, 0.4); -} - -textarea.guild-input { - resize: vertical; -} - -.guild-input-group { - margin-bottom: 8px; -} - -.guild-input-group label { - display: block; - color: #a89b8c; - font-size: 10px; - text-transform: uppercase; - letter-spacing: 0.5px; - margin-bottom: 3px; -} - -/* Waiting state */ -.guild-waiting { - text-align: center; - padding: 16px; - color: #a89b8c; -} - -.guild-create-list { - padding: 8px 0; -} - -.guild-waiting-dot { - display: inline-block; - animation: guildPulse 1.4s infinite both; -} - -.guild-waiting-dot:nth-child(2) { - animation-delay: 0.2s; -} - -.guild-waiting-dot:nth-child(3) { - animation-delay: 0.4s; -} - -@keyframes guildPulse { - 0%, - 80%, - 100% { - opacity: 0.3; - } - 40% { - opacity: 1; - } -} diff --git a/src/ui/guild-dialog/guild-dialog.ts b/src/ui/guild-dialog/guild-dialog.ts deleted file mode 100644 index 2076a7ea..00000000 --- a/src/ui/guild-dialog/guild-dialog.ts +++ /dev/null @@ -1,678 +0,0 @@ -import type { Client } from '@/client'; -import { GuildDialogState } from '@/game-state'; -import { playSfxById, SfxId } from '@/sfx'; -import { Base } from '@/ui/base-ui'; - -import './guild-dialog.css'; -import { GUILD_MAX_RANK, GUILD_MIN_DEPOSIT } from '@/consts'; - -export class GuildDialog extends Base { - private client: Client; - protected container = document.getElementById('guild-dialog')!; - private dialogs = document.getElementById('dialogs')!; - private cover = document.querySelector('#cover')!; - - constructor(client: Client) { - super(); - this.client = client; - - // Guild NPC opened - this.client.on('guildOpened', () => this.show()); - - // All guild state changes - this.client.on('guildUpdated', () => this.render()); - } - - show() { - this.client.guildController.state = GuildDialogState.MainMenu; - this.render(); - this.cover.classList.remove('hidden'); - this.container.classList.remove('hidden'); - this.dialogs.classList.remove('hidden'); - this.client.typing = true; - playSfxById(SfxId.ButtonClick); - } - - close() { - this.client.guildController.state = GuildDialogState.None; - this.cover.classList.add('hidden'); - this.container.classList.add('hidden'); - if (!document.querySelector('#dialogs > div:not(.hidden)')) { - this.dialogs.classList.add('hidden'); - this.client.typing = false; - } - } - - private render() { - const view = this.client.guildController.state; - - if (view === GuildDialogState.None) { - this.close(); - return; - } - - const body = this.container.querySelector('.guild-body')!; - const footer = this.container.querySelector('.guild-footer')!; - const header = this.container.querySelector('.guild-header')!; - body.innerHTML = ''; - footer.innerHTML = ''; - - switch (view) { - case GuildDialogState.MainMenu: - header.textContent = 'Guild'; - this.renderMenu(body, footer); - break; - case GuildDialogState.Create: - header.textContent = 'Create Guild'; - this.renderCreate(body, footer); - break; - case GuildDialogState.CreateWaiting: - header.textContent = 'Creating Guild...'; - this.renderCreateWaiting(body, footer); - break; - case GuildDialogState.Join: - header.textContent = 'Join Guild'; - this.renderJoin(body, footer); - break; - case GuildDialogState.Lookup: - header.textContent = 'Look Up Guild'; - this.renderLookup(body, footer); - break; - case GuildDialogState.GuildInfo: { - const info = this.client.guildController.cachedInfo; - header.textContent = info ? `${info.name} [${info.tag}]` : 'Guild Info'; - this.renderInfo(body, footer); - break; - } - case GuildDialogState.GuildMembers: { - const members = this.client.guildController.cachedMembers; - header.textContent = `Members (${members.length})`; - this.renderMembers(body, footer); - break; - } - case GuildDialogState.Manage: - header.textContent = 'Manage Guild'; - this.renderManage(body, footer); - break; - case GuildDialogState.EditDescription: - header.textContent = 'Edit Description'; - this.renderEditDescription(body, footer); - break; - case GuildDialogState.EditRanks: - header.textContent = 'Edit Ranks'; - this.renderEditRanks(body, footer); - break; - case GuildDialogState.Bank: - header.textContent = 'Guild Bank'; - this.renderBank(body, footer); - break; - case GuildDialogState.KickMember: - header.textContent = 'Kick Member'; - this.renderKick(body, footer); - break; - case GuildDialogState.AssignRank: - header.textContent = 'Change Rank'; - this.renderChangeRank(body, footer); - break; - } - - const firstInput = body.querySelector('input, textarea'); - firstInput?.focus(); - } - - // ── Menu ────────────────────────────────────────────────────────────── - - private renderMenu(body: Element, footer: Element) { - const inGuild = this.client.guildTag.trim().length > 0; - const rank = this.client.guildRank; - - if (!inGuild) { - this.addMenuButton(body, 'Create Guild', () => { - this.client.guildController.state = GuildDialogState.Create; - this.render(); - }); - this.addMenuButton(body, 'Join Guild', () => { - this.client.guildController.state = GuildDialogState.Join; - this.render(); - }); - } else { - this.addMenuButton(body, 'Guild Information', () => { - this.client.guildController.requestGuildInfo(this.client.guildTag); - }); - this.addMenuButton(body, 'Member List', () => { - this.client.guildController.requestMemberList(this.client.guildTag); - }); - // Guild bank is accessible to all members - this.addMenuButton(body, 'Guild Bank', () => { - this.client.guildController.requestBankInfo(); - }); - // Management options only for leaders/recruiters (rank <= 2) - if (rank <= 2) { - this.addMenuButton(body, 'Manage Guild', () => { - this.client.guildController.state = GuildDialogState.Manage; - this.render(); - }); - } - this.addMenuButton(body, 'Leave Guild', () => { - this.client.guildController.leaveGuild(); - }); - } - - this.addMenuButton(body, 'Look Up Guild', () => { - this.client.guildController.state = GuildDialogState.Lookup; - this.render(); - }); - - this.addFooterButton(footer, 'Close', () => this.close()); - } - - // ── Create ──────────────────────────────────────────────────────────── - - private renderCreate(body: Element, footer: Element) { - const tagGroup = this.createInputGroup('Guild Tag (2-3 chars)', 'tag', 3); - body.appendChild(tagGroup); - - const nameGroup = this.createInputGroup('Guild Name', 'name', 24); - body.appendChild(nameGroup); - - const descriptionGroup = this.createInputGroup( - 'Guild Description', - 'description', - 240, - true, - ); - body.appendChild(descriptionGroup); - - this.addFooterButton(footer, 'Back', () => { - this.client.guildController.state = GuildDialogState.MainMenu; - this.render(); - }); - this.addFooterButton( - footer, - 'Begin Creation', - () => { - const tag = ( - body.querySelector('input[name="tag"]') as HTMLInputElement - ).value.trim(); - const name = ( - body.querySelector('input[name="name"]') as HTMLInputElement - ).value.trim(); - const description = ( - body.querySelector( - 'textarea[name="description"]', - ) as HTMLTextAreaElement - ).value.trim(); - if (!tag || !name) return; - this.client.guildController.beginCreate(tag, name, description); - }, - 'primary', - ); - } - - private renderCreateWaiting(body: Element, footer: Element) { - const waiting = document.createElement('div'); - waiting.className = 'guild-waiting'; - - const textNode = document.createTextNode('Waiting for members to join'); - waiting.appendChild(textNode); - - for (let i = 0; i < 3; i++) { - const dot = document.createElement('span'); - dot.className = 'guild-waiting-dot'; - dot.textContent = '.'; - waiting.appendChild(dot); - } - - body.appendChild(waiting); - - if (this.client.guildController.createMembers.length > 0) { - const list = document.createElement('div'); - list.className = 'guild-create-list'; - for (const name of this.client.guildController.createMembers) { - const row = this.createMemberRow(name, 'Joined', '#a5d6a7'); - list.appendChild(row); - } - body.appendChild(list); - } - - this.addFooterButton(footer, 'Cancel', () => { - this.client.guildController.state = GuildDialogState.MainMenu; - this.render(); - }); - } - - // ── Join ────────────────────────────────────────────────────────────── - - private renderJoin(body: Element, footer: Element) { - const tagGroup = this.createInputGroup('Guild Tag', 'tag', 3); - body.appendChild(tagGroup); - - const recruiterGroup = this.createInputGroup( - 'Recruiter Name', - 'recruiter', - 12, - ); - body.appendChild(recruiterGroup); - - this.addFooterButton(footer, 'Back', () => { - this.client.guildController.state = GuildDialogState.MainMenu; - this.render(); - }); - this.addFooterButton( - footer, - 'Request to Join', - () => { - const tag = ( - body.querySelector('input[name="tag"]') as HTMLInputElement - ).value.trim(); - const recruiter = ( - body.querySelector('input[name="recruiter"]') as HTMLInputElement - ).value.trim(); - if (!tag || !recruiter) return; - this.client.guildController.requestToJoin(tag, recruiter); - }, - 'primary', - ); - } - - // ── Info View ───────────────────────────────────────────────────────── - - private renderInfo(body: Element, footer: Element) { - const data = this.client.guildController.cachedInfo; - if (!data) return; - - const fields: [string, string, boolean?][] = [ - ['Name', data.name, true], - ['Tag', data.tag], - ['Created', data.createDate], - ['Wealth', data.wealth], - ]; - - for (const [label, value, highlight] of fields) { - const section = this.createInfoSection(label, value, !!highlight); - body.appendChild(section); - } - - if (data.description) { - const descriptionSection = this.createInfoSection( - 'Description', - data.description, - ); - body.appendChild(descriptionSection); - } - - if (data.staff.length > 0) { - const staffSection = document.createElement('div'); - staffSection.className = 'guild-info-section'; - const staffLabel = document.createElement('div'); - staffLabel.className = 'guild-info-label'; - staffLabel.textContent = 'Staff'; - staffSection.appendChild(staffLabel); - - for (const staffMember of data.staff) { - const row = document.createElement('div'); - row.className = 'guild-staff-row'; - - const nameSpan = document.createElement('span'); - nameSpan.className = 'guild-staff-name'; - nameSpan.textContent = staffMember.name; - row.appendChild(nameSpan); - - const typeSpan = document.createElement('span'); - typeSpan.className = 'guild-staff-type'; - typeSpan.textContent = staffMember.rank === 1 ? 'Leader' : 'Recruiter'; - row.appendChild(typeSpan); - - staffSection.appendChild(row); - } - body.appendChild(staffSection); - } - - this.addFooterButton(footer, 'Member List', () => { - this.client.guildController.requestMemberList(data.tag); - }); - this.addFooterButton(footer, 'Back', () => { - this.client.guildController.state = GuildDialogState.MainMenu; - this.render(); - }); - } - - // ── Member List ─────────────────────────────────────────────────────── - - private renderMembers(body: Element, footer: Element) { - for (const member of this.client.guildController.cachedMembers) { - const row = this.createMemberRow(member.name, member.rankName); - body.appendChild(row); - } - - this.addFooterButton(footer, 'Back', () => { - this.client.guildController.state = GuildDialogState.MainMenu; - this.render(); - }); - } - - // ── Manage ──────────────────────────────────────────────────────────── - - private renderManage(body: Element, footer: Element) { - const rank = this.client.guildRank; - - // Edit description/ranks: rank <= 2 (leaders) - if (rank <= 2) { - this.addMenuButton(body, 'Edit Description', () => { - this.client.guildController.requestDescriptionInfo(); - }); - this.addMenuButton(body, 'Edit Ranks', () => { - this.client.guildController.requestRanksInfo(); - }); - } - // Kick/rank changes: rank <= 2 - if (rank <= 2) { - this.addMenuButton(body, 'Kick Member', () => { - this.client.guildController.state = GuildDialogState.KickMember; - this.render(); - }); - this.addMenuButton(body, 'Change Member Rank', () => { - this.client.guildController.state = GuildDialogState.AssignRank; - this.render(); - }); - } - // Disband: founder only (rank 0) - if (rank === 0) { - this.addMenuButton(body, 'Disband Guild', () => { - this.client.guildController.disbandGuild(); - }); - } - - this.addFooterButton(footer, 'Back', () => { - this.client.guildController.state = GuildDialogState.MainMenu; - this.render(); - }); - } - - // ── Edit Description ────────────────────────────────────────────────── - - private renderEditDescription(body: Element, footer: Element) { - const group = this.createInputGroup( - 'Description', - 'description', - 240, - true, - ); - const textarea = group.querySelector('textarea') as HTMLTextAreaElement; - textarea.value = this.client.guildController.cachedDescription; - body.appendChild(group); - - this.addFooterButton(footer, 'Back', () => { - this.client.guildController.state = GuildDialogState.Manage; - this.render(); - }); - this.addFooterButton( - footer, - 'Save', - () => { - this.client.guildController.saveDescription(textarea.value); - }, - 'primary', - ); - } - - // ── Edit Ranks ──────────────────────────────────────────────────────── - - private renderEditRanks(body: Element, footer: Element) { - for (let i = 0; i < this.client.guildController.cachedRanks.length; i++) { - const group = this.createInputGroup(`Rank ${i + 1}`, `rank-${i}`, 16); - const input = group.querySelector('input') as HTMLInputElement; - input.value = this.client.guildController.cachedRanks[i].trim(); - body.appendChild(group); - } - - this.addFooterButton(footer, 'Back', () => { - this.client.guildController.state = GuildDialogState.Manage; - this.render(); - }); - this.addFooterButton( - footer, - 'Save', - () => { - const ranks: string[] = []; - for ( - let i = 0; - i < this.client.guildController.cachedRanks.length; - i++ - ) { - const input = body.querySelector( - `input[name="rank-${i}"]`, - ) as HTMLInputElement; - ranks.push( - input.value.trim() || this.client.guildController.cachedRanks[i], - ); - } - this.client.guildController.saveRanks(ranks); - }, - 'primary', - ); - } - - // ── Bank ────────────────────────────────────────────────────────────── - - private renderBank(body: Element, footer: Element) { - const section = this.createInfoSection( - 'Guild Bank Balance', - this.client.guildController.cachedBankGold.toString(), - true, - ); - body.appendChild(section); - - const group = this.createInputGroup('Deposit Amount', 'deposit', 10); - const input = group.querySelector('input') as HTMLInputElement; - input.type = 'number'; - input.min = `${GUILD_MIN_DEPOSIT}`; - input.value = input.min; - body.appendChild(group); - - this.addFooterButton(footer, 'Back', () => { - this.client.guildController.state = GuildDialogState.MainMenu; - this.render(); - }); - this.addFooterButton( - footer, - 'Deposit', - () => { - const amount = Number.parseInt(input.value, 10); - if (Number.isNaN(amount) || amount < GUILD_MIN_DEPOSIT) return; - this.client.guildController.depositToBank(amount); - }, - 'primary', - ); - } - - // ── Kick ────────────────────────────────────────────────────────────── - - private renderKick(body: Element, footer: Element) { - const group = this.createInputGroup('Member Name', 'kick-name', 12); - body.appendChild(group); - - this.addFooterButton(footer, 'Back', () => { - this.client.guildController.state = GuildDialogState.Manage; - this.render(); - }); - this.addFooterButton( - footer, - 'Kick', - () => { - const name = ( - body.querySelector('input[name="kick-name"]') as HTMLInputElement - ).value.trim(); - if (!name) return; - this.client.guildController.kickMember(name); - }, - 'danger', - ); - } - - // ── Change Rank ─────────────────────────────────────────────────────── - - private renderChangeRank(body: Element, footer: Element) { - const nameGroup = this.createInputGroup('Member Name', 'rank-name', 12); - body.appendChild(nameGroup); - - const rankGroup = this.createInputGroup( - `New Rank (0-${GUILD_MAX_RANK})`, - 'new-rank', - 2, - ); - const rankInput = rankGroup.querySelector('input') as HTMLInputElement; - rankInput.type = 'number'; - rankInput.min = '0'; - rankInput.max = `${GUILD_MAX_RANK}`; - body.appendChild(rankGroup); - - this.addFooterButton(footer, 'Back', () => { - this.client.guildController.state = GuildDialogState.Manage; - this.render(); - }); - this.addFooterButton( - footer, - 'Update', - () => { - const name = ( - body.querySelector('input[name="rank-name"]') as HTMLInputElement - ).value.trim(); - const rank = Number.parseInt(rankInput.value, 10); - if (!name || Number.isNaN(rank)) return; - this.client.guildController.changeMemberRank(name, rank); - }, - 'primary', - ); - } - - private renderLookup(body: Element, footer: Element) { - const group = this.createInputGroup('Guild Tag or Name', 'lookup', 24); - body.appendChild(group); - - this.addFooterButton(footer, 'Back', () => { - this.client.guildController.state = GuildDialogState.MainMenu; - this.render(); - }); - this.addFooterButton( - footer, - 'Look Up', - () => { - const query = ( - body.querySelector('input[name="lookup"]') as HTMLInputElement - ).value.trim(); - if (!query) return; - this.client.guildController.requestGuildInfo(query); - }, - 'primary', - ); - } - - private addMenuButton(parent: Element, text: string, onClick: () => void) { - const button = document.createElement('button'); - button.className = 'guild-menu-btn'; - button.textContent = text; - button.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - onClick(); - }); - parent.appendChild(button); - } - - private addFooterButton( - parent: Element, - text: string, - onClick: () => void, - variant?: string, - ) { - const button = document.createElement('button'); - button.className = `guild-btn${variant ? ` ${variant}` : ''}`; - button.textContent = text; - button.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - onClick(); - }); - parent.appendChild(button); - } - - private createInputGroup( - label: string, - name: string, - maxLength: number, - isTextarea?: boolean, - ): HTMLDivElement { - const id = `guild-input-${name}`; - const group = document.createElement('div'); - group.className = 'guild-input-group'; - - const labelElement = document.createElement('label'); - labelElement.htmlFor = id; - labelElement.textContent = label; - group.appendChild(labelElement); - - if (isTextarea) { - const textarea = document.createElement('textarea'); - textarea.className = 'guild-input'; - textarea.id = id; - textarea.name = name; - textarea.maxLength = maxLength; - textarea.rows = 4; - group.appendChild(textarea); - } else { - const input = document.createElement('input'); - input.className = 'guild-input'; - input.id = id; - input.type = 'text'; - input.name = name; - input.maxLength = maxLength; - group.appendChild(input); - } - - return group; - } - - private createInfoSection( - label: string, - value: string, - highlight = false, - ): HTMLDivElement { - const section = document.createElement('div'); - section.className = 'guild-info-section'; - - const labelElement = document.createElement('div'); - labelElement.className = 'guild-info-label'; - labelElement.textContent = label; - section.appendChild(labelElement); - - const valueElement = document.createElement('div'); - valueElement.className = `guild-info-value${highlight ? ' highlight' : ''}`; - valueElement.textContent = value; - section.appendChild(valueElement); - - return section; - } - - private createMemberRow( - name: string, - rankName: string, - rankColor?: string, - ): HTMLDivElement { - const row = document.createElement('div'); - row.className = 'guild-member-row'; - - const nameSpan = document.createElement('span'); - nameSpan.className = 'guild-member-name'; - nameSpan.textContent = name; - row.appendChild(nameSpan); - - const rankSpan = document.createElement('span'); - rankSpan.className = 'guild-member-rank'; - rankSpan.textContent = rankName; - if (rankColor) { - rankSpan.style.color = rankColor; - } - row.appendChild(rankSpan); - - return row; - } -} diff --git a/src/ui/guild-dialog/index.ts b/src/ui/guild-dialog/index.ts deleted file mode 100644 index 339ed849..00000000 --- a/src/ui/guild-dialog/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { GuildDialog } from './guild-dialog'; diff --git a/src/ui/hotbar/hotbar.css b/src/ui/hotbar/hotbar.css deleted file mode 100644 index ad51515f..00000000 --- a/src/ui/hotbar/hotbar.css +++ /dev/null @@ -1,75 +0,0 @@ -#hotbar { - position: fixed; - bottom: 10px; - left: 50%; - transform: translateX(-50%); - display: flex; - gap: 4px; - z-index: 1020; - - counter-reset: slot-counter; -} - -#hotbar .slot { - padding: 3px; - position: relative; - width: 4em; - height: 4em; - border-image: url("/border-thin.png") 9 / 9px 9px; - display: flex; - justify-content: center; - align-items: center; - - counter-increment: slot-counter; -} - -#hotbar .slot::before { - content: ""; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.3); - z-index: 1021; - box-sizing: border-box; -} - -#hotbar .slot::after { - width: 14px; - height: 14px; - content: counter(slot-counter); - color: #e6c5a5; - font-size: 10px; - top: 3px; - left: 3px; - background-color: #5a3100; - position: absolute; - border-bottom: 1px solid #3a2110; - border-right: 1px solid #3a2110; - border-radius: 0 0 4px 0; - align-items: center; - justify-content: center; - line-height: normal; - z-index: 1022; - display: flex; -} - -#hotbar .slot .item, -#hotbar .slot .skill { - position: absolute; - z-index: 1023; -} - -#hotbar .slot .skill { - width: 34px; - height: 32px; - scale: 1.3; -} - -#hotbar .slot .item { - background: url("/gfx/gfx003/100.png"); - width: 64px; - height: 32px; - display: flex; - justify-content: center; - align-items: center; - scale: 1.3; -} diff --git a/src/ui/hotbar/hotbar.ts b/src/ui/hotbar/hotbar.ts deleted file mode 100644 index 2c07c367..00000000 --- a/src/ui/hotbar/hotbar.ts +++ /dev/null @@ -1,113 +0,0 @@ -import type { Client } from '@/client'; -import { HOTBAR_SLOTS } from '@/consts'; -import { Base } from '@/ui/base-ui'; -import { SlotType } from '@/ui/ui-types'; -import { setItemImageFromGfx, setSkillBackgroundFromGfx } from '@/ui/utils'; - -import './hotbar.css'; - -class Slot { - type: SlotType; - typeId: number; - - constructor(type: SlotType, typeId = 0) { - this.type = type; - this.typeId = typeId; - } -} - -export class Hotbar extends Base { - protected container: HTMLDivElement = document.querySelector('#hotbar')!; - private client: Client; - - constructor(client: Client) { - super(); - this.client = client; - - for (let i = 0; i < HOTBAR_SLOTS; ++i) { - const slot = document.createElement('div'); - slot.classList.add('slot'); - - slot.addEventListener('click', () => { - this.client.spellController.useHotbarSlot(i); - }); - - this.container.appendChild(slot); - } - } - - refresh() { - this.render(); - } - - show() { - this.render(); - this.container.classList.remove('hidden'); - } - - setSlot(slotIndex: number, type: SlotType, typeId: number) { - this.client.hotbarSlots[slotIndex] = new Slot(type, typeId); - localStorage.setItem( - `${this.client.name}-hotbar`, - JSON.stringify(this.client.hotbarSlots), - ); - this.render(); - } - - private render() { - if (!this.client.hotbarSlots.length) { - this.loadSlots(); - } - - for (const [index, slot] of this.client.hotbarSlots.entries()) { - if (slot.type === SlotType.Empty) { - continue; - } - - const element = this.container.children[index] as HTMLDivElement; - element.innerHTML = ''; - - if (slot.type === SlotType.Skill) { - const skill = this.client.getEsfRecordById(slot.typeId); - if (!skill) { - continue; - } - - const img = document.createElement('div'); - img.classList.add('skill'); - void setSkillBackgroundFromGfx(img, skill.iconId); - - if (this.client.spellController.selectedSpellId === slot.typeId) { - img.style.backgroundPositionX = '-34px'; - } - - element.appendChild(img); - } else { - const item = this.client.getEifRecordById(slot.typeId); - if (!item) { - continue; - } - - const itemContainer = document.createElement('div'); - itemContainer.classList.add('item'); - - const img = document.createElement('img'); - void setItemImageFromGfx(img, slot.typeId, item.graphicId); - itemContainer.appendChild(img); - - element.appendChild(itemContainer); - } - } - } - - private loadSlots() { - const json = localStorage.getItem(`${this.client.name}-hotbar`); - if (json) { - this.client.hotbarSlots = JSON.parse(json); - } else { - for (let i = 0; i < HOTBAR_SLOTS; ++i) { - this.client.hotbarSlots.push(new Slot(SlotType.Empty)); - } - } - } -} diff --git a/src/ui/hotbar/index.ts b/src/ui/hotbar/index.ts deleted file mode 100644 index cdfbcad6..00000000 --- a/src/ui/hotbar/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './hotbar'; diff --git a/src/ui/hud/hud.css b/src/ui/hud/hud.css deleted file mode 100644 index a7549de4..00000000 --- a/src/ui/hud/hud.css +++ /dev/null @@ -1,89 +0,0 @@ -#hud { - position: fixed; - z-index: 1020; - top: 10px; - left: 50%; - transform: translateX(-50%); - user-select: none; - pointer-events: none; - display: flex; - gap: 8px; -} - -#hud .stat-container { - position: relative; -} - -#hud .bar { - width: 110px; - height: 14px; - background: none; - border-radius: 10px; - overflow: hidden; - transform: scale(var(--ui-scale, 1)); - transform-origin: left center; - pointer-events: auto; - cursor: pointer; - z-index: 20; -} - -#hud .dropdown { - width: 110px; - height: 20px; - background-image: url("/gfx/gfx002/158.png"); - background-position: -221px -30px; - background-repeat: no-repeat; - background-size: 440px 64px; - pointer-events: auto; - z-index: 10; - white-space: nowrap; - color: #d4c4a8; - font-size: 12px; - line-height: 21px; - - position: absolute; - bottom: calc(-17px * var(--ui-scale)); - - transform: scale(var(--ui-scale)); - /* scales visually with rest of UI */ - transform-origin: left center; - /* scaling doesn't shift it sideways */ -} - -#hud .dropdown span { - padding: 2px 6px; - line-height: 18px; -} - -#hud .stat-empty { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-image: url("/gfx/gfx002/158.png"); - background-repeat: no-repeat; - background-size: 439px 63px; -} - -#hud .stat-fill { - position: absolute; - top: 0; - left: 0; - width: 0; - height: 100%; - background-image: url("/gfx/gfx002/158.png"); - background-repeat: no-repeat; - background-size: 439px 63px; - background-position-y: -14px; -} - -#hud .stat-container[data-id="tp"] .stat-empty, -#hud .stat-container[data-id="tp"] .stat-fill { - background-position-x: -110px; -} - -#hud .stat-container[data-id="exp"] .stat-empty, -#hud .stat-container[data-id="exp"] .stat-fill { - background-position-x: -329px; -} diff --git a/src/ui/hud/hud.ts b/src/ui/hud/hud.ts deleted file mode 100644 index 28b8f279..00000000 --- a/src/ui/hud/hud.ts +++ /dev/null @@ -1,96 +0,0 @@ -import type { Client } from '@/client'; -import { playSfxById, SfxId } from '@/sfx'; -import { Base } from '@/ui/base-ui'; -import { calculateTnl, getExpForLevel } from '@/utils'; - -import './hud.css'; - -export class HUD extends Base { - protected container = document.getElementById('hud')!; - private hpText: HTMLDivElement = this.container!.querySelector( - '.stat-container[data-id="hp"] span', - )!; - private hpFill: HTMLDivElement = this.container!.querySelector( - '.stat-container[data-id="hp"] .stat-fill', - )!; - private hpBar: HTMLDivElement = this.container!.querySelector( - '.stat-container[data-id="hp"] .bar', - )!; - private hpDropdown: HTMLDivElement = this.container!.querySelector( - '.stat-container[data-id="hp"] .dropdown', - )!; - - private tpText: HTMLDivElement = this.container!.querySelector( - '.stat-container[data-id="tp"] span', - )!; - private tpFill: HTMLDivElement = this.container!.querySelector( - '.stat-container[data-id="tp"] .stat-fill', - )!; - private tpBar: HTMLDivElement = this.container!.querySelector( - '.stat-container[data-id="tp"] .bar', - )!; - private tpDropdown: HTMLDivElement = this.container!.querySelector( - '.stat-container[data-id="tp"] .dropdown', - )!; - - private expText: HTMLDivElement = this.container!.querySelector( - '.stat-container[data-id="exp"] span', - )!; - private expFill: HTMLDivElement = this.container!.querySelector( - '.stat-container[data-id="exp"] .stat-fill', - )!; - private expBar: HTMLDivElement = this.container!.querySelector( - '.stat-container[data-id="exp"] .bar', - )!; - private expDropdown: HTMLDivElement = this.container!.querySelector( - '.stat-container[data-id="exp"] .dropdown', - )!; - private readonly LEFT_SIDE_WIDTH = 24; - private readonly STAT_WIDTH = 79; - - setStats(client: Client) { - this.hpText.innerText = `${client.hp}/${client.maxHp}`; - this.hpFill.style.width = `${this.LEFT_SIDE_WIDTH + Math.floor((client.hp / client.maxHp) * this.STAT_WIDTH)}px`; - - this.tpText.innerText = `${client.tp}/${client.maxTp}`; - this.tpFill.style.width = `${this.LEFT_SIDE_WIDTH + Math.floor((client.tp / client.maxTp) * this.STAT_WIDTH)}px`; - - const tnl = calculateTnl(client.experience); - const currentLevelExp = getExpForLevel(client.level); - const nextLevelExp = getExpForLevel(client.level + 1); - - const progress = client.experience - currentLevelExp; - const range = nextLevelExp - currentLevelExp; - - const percent = progress / range; - - this.expText.innerText = `${tnl}`; - this.expFill.style.width = `${this.LEFT_SIDE_WIDTH + Math.floor(percent * this.STAT_WIDTH)}px`; - } - - show() { - this.hpDropdown.classList.add('hidden'); - this.tpDropdown.classList.add('hidden'); - this.expDropdown.classList.add('hidden'); - this.container!.classList.remove('hidden'); - } - - constructor() { - super(); - - this.hpBar.addEventListener('click', () => { - playSfxById(SfxId.HudStatusBarClick); - this.hpDropdown.classList.toggle('hidden'); - }); - - this.tpBar.addEventListener('click', () => { - playSfxById(SfxId.HudStatusBarClick); - this.tpDropdown.classList.toggle('hidden'); - }); - - this.expBar.addEventListener('click', () => { - playSfxById(SfxId.HudStatusBarClick); - this.expDropdown.classList.toggle('hidden'); - }); - } -} diff --git a/src/ui/hud/index.ts b/src/ui/hud/index.ts deleted file mode 100644 index 522e18fb..00000000 --- a/src/ui/hud/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './hud'; diff --git a/src/ui/in-game-menu/in-game-menu.css b/src/ui/in-game-menu/in-game-menu.css deleted file mode 100644 index 0364881c..00000000 --- a/src/ui/in-game-menu/in-game-menu.css +++ /dev/null @@ -1,9 +0,0 @@ -#in-game-menu { - position: absolute; - right: 0; - display: flex; - flex-flow: column; - height: 100%; - justify-content: center; - z-index: 1020; -} diff --git a/src/ui/in-game-menu/in-game-menu.ts b/src/ui/in-game-menu/in-game-menu.ts deleted file mode 100644 index 26eaabbc..00000000 --- a/src/ui/in-game-menu/in-game-menu.ts +++ /dev/null @@ -1,70 +0,0 @@ -import mitt from 'mitt'; -import { playSfxById, SfxId } from '@/sfx'; -import { Base } from '@/ui/base-ui'; - -import './in-game-menu.css'; - -type Events = { - toggle: 'inventory' | 'map' | 'spells' | 'stats' | 'online' | 'party'; -}; - -export class InGameMenu extends Base { - private emitter = mitt(); - - constructor() { - super(); - this.container = document.querySelector('#in-game-menu')!; - - const btnInventory = this.container.querySelector( - 'button[data-id="inventory"]', - ); - const btnMap = this.container.querySelector('button[data-id="map"]'); - const btnSpells = this.container.querySelector('button[data-id="spells"]'); - const btnStats = this.container.querySelector('button[data-id="stats"]'); - const btnOnline = this.container.querySelector('button[data-id="online"]'); - const btnParty = this.container.querySelector('button[data-id="party"]'); - - btnInventory!.addEventListener('click', (e) => { - e.stopPropagation(); - playSfxById(SfxId.ButtonClick); - this.emitter.emit('toggle', 'inventory'); - }); - - btnMap!.addEventListener('click', (e) => { - e.stopPropagation(); - playSfxById(SfxId.ButtonClick); - this.emitter.emit('toggle', 'map'); - }); - - btnSpells!.addEventListener('click', (e) => { - e.stopPropagation(); - playSfxById(SfxId.ButtonClick); - this.emitter.emit('toggle', 'spells'); - }); - - btnStats!.addEventListener('click', (e) => { - e.stopPropagation(); - playSfxById(SfxId.ButtonClick); - this.emitter.emit('toggle', 'stats'); - }); - - btnOnline!.addEventListener('click', (e) => { - e.stopPropagation(); - playSfxById(SfxId.ButtonClick); - this.emitter.emit('toggle', 'online'); - }); - - btnParty!.addEventListener('click', (e) => { - e.stopPropagation(); - playSfxById(SfxId.ButtonClick); - this.emitter.emit('toggle', 'party'); - }); - } - - on( - event: Event, - handler: (data: Events[Event]) => void, - ) { - this.emitter.on(event, handler); - } -} diff --git a/src/ui/in-game-menu/index.ts b/src/ui/in-game-menu/index.ts deleted file mode 100644 index 6cf1b79f..00000000 --- a/src/ui/in-game-menu/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './in-game-menu'; diff --git a/src/ui/ui-types.ts b/src/ui/index.ts similarity index 97% rename from src/ui/ui-types.ts rename to src/ui/index.ts index 0759322a..85bd9209 100644 --- a/src/ui/ui-types.ts +++ b/src/ui/index.ts @@ -1,3 +1,5 @@ +export { Ui } from './ui'; + export enum DialogIcon { Buy = 0, Sell = 1, diff --git a/src/ui/inventory/index.ts b/src/ui/inventory/index.ts deleted file mode 100644 index 63f61f19..00000000 --- a/src/ui/inventory/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './inventory'; diff --git a/src/ui/inventory/inventory.css b/src/ui/inventory/inventory.css deleted file mode 100644 index 77d522ff..00000000 --- a/src/ui/inventory/inventory.css +++ /dev/null @@ -1,161 +0,0 @@ -#inventory { - position: absolute; - right: 40px; - width: 186px; - height: 270px; - padding: 6px; - border-image: url("/border-thin.png") 9 / 9px 9px; - background: #7b5a29; - user-select: none; - z-index: 1030; -} - -#inventory .top { - display: flex; - justify-content: space-between; -} - -#inventory .top > div { - user-select: none; -} - -#inventory .weight { - margin-right: 6px; - margin-top: 3px; - color: #ccc; - font-size: 12px; -} - -#inventory .grid { - height: 229px; - width: 185px; - cursor: default; - display: grid; - border-top: 1px solid #000; - border-bottom: 1px solid #000; - grid-template-columns: repeat(8, 23px); - grid-template-rows: repeat(10, 23px); -} - -#inventory .item { - position: relative; - display: flex; - justify-content: center; - align-items: center; -} - -#inventory .item .tooltip { - position: absolute; - bottom: 100%; - /* Position above the item */ - left: 50%; - transform: translateX(-50%); - background-color: black; - color: white; - padding: 4px 8px; - border-radius: 4px; - font-size: 12px; - white-space: nowrap; - opacity: 0; - pointer-events: none; - z-index: 10; -} - -#inventory .item:hover .tooltip { - opacity: 0.8; -} - -#inventory .item::before { - content: ""; - position: absolute; - inset: 0; - background-color: rgba(248, 248, 190, 0.3); - opacity: 0; - pointer-events: none; - z-index: 0; - box-sizing: border-box; -} - -#inventory .item:hover::before { - opacity: 0.3 !important; -} - -#inventory .item img { - position: relative; - z-index: 1; - max-width: 100%; - max-height: 100%; - object-fit: contain; - image-rendering: pixelated; -} - -#inventory .item[data-size="Size1x1"] { - grid-column: span 1; - grid-row: span 1; -} - -#inventory .item[data-size="Size1x2"] { - grid-column: span 1; - grid-row: span 2; -} - -#inventory .item[data-size="Size1x3"] { - grid-column: span 1; - grid-row: span 3; -} - -#inventory .item[data-size="Size1x4"] { - grid-column: span 1; - grid-row: span 4; -} - -#inventory .item[data-size="Size2x1"] { - grid-column: span 2; - grid-row: span 1; -} - -#inventory .item[data-size="Size2x2"] { - grid-column: span 2; - grid-row: span 2; -} - -#inventory .item[data-size="Size2x3"] { - grid-column: span 2; - grid-row: span 3; -} - -#inventory .item[data-size="Size2x4"] { - grid-column: span 2; - grid-row: span 4; -} - -#inventory .tabs { - display: flex; - gap: 1px; -} - -#inventory .tabs button, -#inventory .buttons button { - background: transparent; - border: 1px solid #000; - border-bottom: none; - color: #8a8a8a; - padding: 2px 10px; - cursor: pointer; -} - -#inventory .tabs button.active { - color: #cccccc; -} - -#inventory .buttons { - display: flex; - gap: 1px; - position: absolute; - right: 5px; - bottom: 1px; -} - -#inventory .buttons button { - color: #ccc; -} diff --git a/src/ui/inventory/inventory.ts b/src/ui/inventory/inventory.ts deleted file mode 100644 index 8f81ab6b..00000000 --- a/src/ui/inventory/inventory.ts +++ /dev/null @@ -1,594 +0,0 @@ -import { type Item, ItemSize } from 'eolib'; -import mitt from 'mitt'; -import type { Client } from '@/client'; -import { playSfxById, SfxId } from '@/sfx'; -import { Base } from '@/ui/base-ui'; -import { setItemGridImageFromGfx } from '@/ui/utils'; -import { getItemMeta } from '@/utils'; -import type { Vector2 } from '@/vector'; - -import './inventory.css'; -import type { EquipmentSlot } from '@/equipment'; -import { getEquipmentSlotFromString } from '@/equipment'; - -type ItemPosition = { - id: number; - tab: number; - x: number; - y: number; -}; - -const TABS = 2; -const COLS = 8; -const ROWS = 10; -const CELL_SIZE = 23; - -const ITEM_SIZE = { - [ItemSize.Size1x1]: { x: 1, y: 1 }, - [ItemSize.Size1x2]: { x: 1, y: 2 }, - [ItemSize.Size1x3]: { x: 1, y: 3 }, - [ItemSize.Size1x4]: { x: 1, y: 4 }, - [ItemSize.Size2x1]: { x: 2, y: 1 }, - [ItemSize.Size2x2]: { x: 2, y: 2 }, - [ItemSize.Size2x3]: { x: 2, y: 3 }, - [ItemSize.Size2x4]: { x: 2, y: 4 }, -}; - -type Events = { - dropItem: { at: 'cursor' | 'feet'; itemId: number }; - useItem: number; - openPaperdoll: undefined; - equipItem: { slot: EquipmentSlot; itemId: number }; - junkItem: number; - addChestItem: number; - addLockerItem: number; - assignToSlot: { itemId: number; slotIndex: number }; -}; - -export class Inventory extends Base { - private client: Client; - private emitter = mitt(); - protected container: HTMLDivElement = document.querySelector('#inventory')!; - private grid: HTMLDivElement = this.container.querySelector('.grid')!; - private positions: ItemPosition[] = []; - private tab = 0; - private uiContainer = document.getElementById('ui'); - private pointerDownAt = 0; - - private dragging: { - item: Item; - el: HTMLElement; - pointerId: number; - ghost: HTMLElement; - offsetX: number; - offsetY: number; - } | null = null; - - private currentWeight: HTMLSpanElement = - this.container.querySelector('.weight .current')!; - private maxWeight: HTMLSpanElement = - this.container.querySelector('.weight .max')!; - private btnPaperdoll: HTMLButtonElement = this.container.querySelector( - 'button[data-id="paperdoll"]', - )!; - private btnDrop: HTMLButtonElement = this.container.querySelector( - 'button[data-id="drop"]', - )!; - private btnJunk: HTMLButtonElement = this.container.querySelector( - 'button[data-id="junk"]', - )!; - private btnTab1: HTMLButtonElement = this.container.querySelector( - '.tabs > button:nth-child(1)', - )!; - private btnTab2: HTMLButtonElement = this.container.querySelector( - '.tabs > button:nth-child(2)', - )!; - private lastItemSelected = 0; - - private onPointerDown(e: PointerEvent, el: HTMLDivElement, item: Item) { - if (e.button !== 0 && e.pointerType !== 'touch') return; - - const now = new Date(); - if (this.pointerDownAt) { - const diff = now.getTime() - this.pointerDownAt; - if (diff < 200) { - this.emitter.emit('useItem', item.id); - } - this.pointerDownAt = now.getTime(); - } else { - this.pointerDownAt = now.getTime(); - } - - (e.target as Element).setPointerCapture(e.pointerId); - - const rect = el.getBoundingClientRect(); - const offsetX = e.clientX - rect.left; - const offsetY = e.clientY - rect.top; - - const ghost = el.querySelector('img')!.cloneNode(true) as HTMLElement; - ghost.style.position = 'fixed'; - ghost.style.pointerEvents = 'none'; - ghost.style.margin = '0'; - ghost.style.inset = 'auto'; - ghost.style.left = '0'; - ghost.style.top = '0'; - ghost.style.transform = `translate(${e.clientX - offsetX}px, ${e.clientY - offsetY}px)`; - ghost.style.opacity = '0.9'; - ghost.style.willChange = 'transform'; - ghost.style.zIndex = '9999'; - // hide original element - el.style.display = 'none'; - - document.body.appendChild(ghost); - - this.dragging = { - item, - el, - pointerId: e.pointerId, - ghost, - offsetX, - offsetY, - }; - - playSfxById(SfxId.InventoryPickup); - - window.addEventListener('pointermove', this.onPointerMove.bind(this), { - passive: false, - }); - window.addEventListener('pointerup', this.onPointerUp.bind(this), { - passive: false, - }); - window.addEventListener('pointercancel', this.onPointerCancel.bind(this), { - passive: false, - }); - } - - private onPointerMove(e: PointerEvent) { - if (!this.dragging || e.pointerId !== this.dragging.pointerId) return; - - // keep ghost under the finger/cursor - const { ghost, offsetX, offsetY } = this.dragging; - ghost.style.transform = `translate(${e.clientX - offsetX}px, ${e.clientY - offsetY}px)`; - - // prevent page scrolling while dragging on mobile - e.preventDefault(); - } - - private onPointerUp(e: PointerEvent) { - if (!this.dragging || e.pointerId !== this.dragging.pointerId) return; - - const { el, ghost, item } = this.dragging; - - const target = document.elementFromPoint(e.clientX, e.clientY); - - playSfxById(SfxId.InventoryPlace); - ghost.remove(); - el.style.display = 'flex'; - this.teardownDragListeners(); - this.dragging = null; - - if (!target) return; - - const slot = target.closest('.slot') as HTMLDivElement; - if (slot) { - const slots = document.querySelectorAll('#hotbar .slot'); - const slotIndex = Array.from(slots).indexOf(slot); - if (slotIndex === -1) return; - - this.emitter.emit('assignToSlot', { - itemId: item.id, - slotIndex, - }); - - return; - } - - if (target === this.btnTab1) { - this.tryMoveToTab(item.id, 0); - return; - } - - if (target === this.btnTab2) { - this.tryMoveToTab(item.id, 1); - return; - } - - if (target === this.btnDrop) { - this.emitter.emit('dropItem', { at: 'feet', itemId: item.id }); - return; - } - - if (target === this.btnJunk) { - this.emitter.emit('junkItem', item.id); - return; - } - - const chestItems = target.closest('.chest-items'); - if (chestItems) { - this.emitter.emit('addChestItem', item.id); - return; - } - - const lockerItems = target.closest('.locker-items'); - if (lockerItems) { - this.emitter.emit('addLockerItem', item.id); - return; - } - - const paperdoll = target.closest('#paperdoll'); - if (paperdoll) { - const itemEl = target.closest('.item'); - if (!itemEl) { - return; - } - - const slot = getEquipmentSlotFromString(itemEl.getAttribute('data-id')!); - if (typeof slot === 'undefined') { - return; - } - - this.emitter.emit('equipItem', { - slot, - itemId: item.id, - }); - return; - } - - if (target === this.uiContainer) { - this.emitter.emit('dropItem', { at: 'cursor', itemId: item.id }); - return; - } - - const rect = this.grid.getBoundingClientRect(); - const pointerX = e.clientX - rect.left; - const pointerY = e.clientY - rect.top; - - if ( - pointerX < 0 || - pointerY < 0 || - pointerX > rect.width || - pointerY > rect.height - ) { - return; - } - - const gridX = Math.floor(pointerX / CELL_SIZE); - const gridY = Math.floor(pointerY / CELL_SIZE); - - this.tryMoveItem(item.id, gridX, gridY); - } - - private onPointerCancel() { - if (!this.dragging) return; - - const { el, ghost } = this.dragging; - ghost.remove(); - el.style.opacity = '1'; - this.teardownDragListeners(); - this.dragging = null; - } - - private teardownDragListeners() { - window.removeEventListener('pointermove', this.onPointerMove); - window.removeEventListener('pointerup', this.onPointerUp); - window.removeEventListener('pointercancel', this.onPointerCancel); - } - - constructor(client: Client) { - super(); - this.client = client; - - this.client.on('inventoryChanged', () => { - this.loadPositions(); - this.render(); - }); - - this.btnTab1.addEventListener('click', (e) => { - playSfxById(SfxId.ButtonClick); - this.tab = 0; - this.btnTab1.classList.add('active'); - this.btnTab2.classList.remove('active'); - this.render(); - e.stopPropagation(); - }); - - this.btnTab2.addEventListener('click', (e) => { - playSfxById(SfxId.ButtonClick); - this.tab = 1; - this.btnTab1.classList.remove('active'); - this.btnTab2.classList.add('active'); - this.render(); - e.stopPropagation(); - }); - this.btnPaperdoll.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.emitter.emit('openPaperdoll', undefined); - }); - - this.btnDrop.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - if (this.lastItemSelected) { - this.emitter.emit('dropItem', { - at: 'feet', - itemId: this.lastItemSelected, - }); - } - }); - - this.btnJunk.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - if (this.lastItemSelected) { - this.emitter.emit('junkItem', this.lastItemSelected); - } - }); - - window.addEventListener('resize', () => { - this.container.style.top = `${Math.floor(window.innerHeight / 2 - this.container.clientHeight / 2)}px`; - }); - } - - on( - event: Event, - handler: (data: Events[Event]) => void, - ) { - this.emitter.on(event, handler); - } - - private tryMoveToTab(itemId: number, tab: number) { - const position = this.getPosition(itemId); - if (!position) return; - - if (![0, 1].includes(tab)) return; - - const record = this.client.getEifRecordById(itemId); - if (!record) return; - - // Set position to next place it fits - const nextPosition = this.getNextAvailablePositionInTab( - itemId, - ITEM_SIZE[record.size], - tab, - ); - - if (nextPosition) { - position.x = nextPosition.x; - position.y = nextPosition.y; - position.tab = tab; - this.render(); - this.savePositions(); - } - } - - private tryMoveItem(itemId: number, x: number, y: number) { - const position = this.getPosition(itemId); - if (!position) return; - - const record = this.client.getEifRecordById(itemId); - if (!record) return; - - const size = ITEM_SIZE[record.size]; - - // Temporarily remove this item from the positions array to avoid false overlap - const otherPositions = this.positions.filter((p) => p.id !== itemId); - - // Reuse your `doesItemFitAt` function but pass in the reduced list - const fits = this.doesItemFitAt(position.tab, x, y, size, otherPositions); - if (!fits) return; - - // Update position - position.x = x; - position.y = y; - - // Re-render - this.render(); - this.savePositions(); - } - - private render() { - this.grid.innerHTML = ''; - - this.currentWeight.innerText = this.client.weight.current.toString(); - this.maxWeight.innerText = this.client.weight.max.toString(); - - if (!this.client.items.length) { - return; - } - - if (!this.positions.length) { - this.loadPositions(); - } - - for (const item of this.client.items) { - const position = this.getPosition(item.id); - if (!position || position.tab !== this.tab) { - continue; - } - - const record = this.client.getEifRecordById(item.id); - if (!record) { - continue; - } - - const imgContainer = document.createElement('div'); - imgContainer.classList.add('item'); - const img = document.createElement('img'); - - void setItemGridImageFromGfx(img, record.graphicId); - - const size = ITEM_SIZE[record.size]; - - imgContainer.style.gridColumn = `${position.x + 1} / span ${size.x}`; - imgContainer.style.gridRow = `${position.y + 1} / span ${size.y}`; - - const tooltip = document.createElement('div'); - tooltip.classList.add('tooltip'); - - const meta = getItemMeta(record); - - if (item.id === 1) { - tooltip.innerText = `${item.amount} ${record.name}\n${meta.join('\n')}`; - } else { - if (item.amount > 1) { - tooltip.innerText = `${record.name} x${item.amount}\n${meta.join('\n')}`; - } else { - tooltip.innerText = `${record.name}\n${meta.join('\n')}`; - } - } - - imgContainer.appendChild(tooltip); - imgContainer.appendChild(img); - - imgContainer.addEventListener('pointerdown', (e) => { - this.onPointerDown(e, imgContainer, item); - }); - - this.grid.appendChild(imgContainer); - } - } - - private getPosition(id: number): ItemPosition | undefined { - return this.positions.find((i) => i.id === id); - } - - private savePositions() { - localStorage.setItem( - `${this.client.name}-inventory`, - JSON.stringify(this.positions), - ); - } - - loadPositions() { - const json = localStorage.getItem(`${this.client.name}-inventory`); - if (!json) { - this.setInitialItemPositions(); - return; - } - - this.positions = JSON.parse(json); - - let changed = false; - for (let i = this.positions.length - 1; i >= 0; --i) { - const position = this.positions[i]; - if ( - position.id !== 1 && - !this.client.items.some((i) => i.id === position.id) - ) { - this.positions.splice(i, 1); - changed = true; - } - } - - for (const item of this.client.items) { - const record = this.client.getEifRecordById(item.id); - if (!record) { - continue; - } - - const existing = this.positions.find((p) => p.id === item.id); - if (!existing) { - changed = true; - const position = this.getNextAvailablePosition( - item.id, - ITEM_SIZE[record.size], - ); - if (position) { - this.positions.push(position); - } - } - } - - if (changed) { - this.savePositions(); - } - } - - private setInitialItemPositions() { - this.positions = []; - for (const item of this.client.items) { - const record = this.client.getEifRecordById(item.id); - if (!record) { - continue; - } - - const position = this.getNextAvailablePosition( - item.id, - ITEM_SIZE[record.size], - ); - if (position) { - this.positions.push(position); - } - } - - this.savePositions(); - } - - private getNextAvailablePosition( - id: number, - size: Vector2, - ): ItemPosition | null { - for (let tab = 0; tab < TABS; ++tab) { - const position = this.getNextAvailablePositionInTab(id, size, tab); - if (position) { - return position; - } - } - - return null; - } - - private getNextAvailablePositionInTab( - id: number, - size: Vector2, - tab: number, - ): ItemPosition | null { - for (let y = 0; y < ROWS; ++y) { - for (let x = 0; x < COLS; ++x) { - if (this.doesItemFitAt(tab, x, y, size)) { - return { x, y, tab, id }; - } - } - } - - return null; - } - - private doesItemFitAt( - tab: number, - x: number, - y: number, - size: Vector2, - positions: ItemPosition[] = this.positions, - ): boolean { - for (const pos of positions) { - if (pos.tab !== tab) continue; - - const record = this.client.getEifRecordById(pos.id); - if (!record) continue; - - const existingSize = ITEM_SIZE[record.size]; - - const overlapX = x < pos.x + existingSize.x && x + size.x > pos.x; - const overlapY = y < pos.y + existingSize.y && y + size.y > pos.y; - - if ((overlapX && overlapY) || x + size.x > COLS || y + size.y > ROWS) { - return false; - } - } - - return true; - } - - show() { - this.render(); - this.container.classList.remove('hidden'); - this.container.style.top = `${Math.floor(window.innerHeight / 2 - this.container.clientHeight / 2)}px`; - } - - toggle() { - if (this.container.classList.contains('hidden')) { - this.show(); - } else { - this.hide(); - } - } -} diff --git a/src/ui/item-amount-dialog/index.ts b/src/ui/item-amount-dialog/index.ts deleted file mode 100644 index d0f6d4b5..00000000 --- a/src/ui/item-amount-dialog/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './item-amount-dialog'; diff --git a/src/ui/item-amount-dialog/item-amount-dialog.css b/src/ui/item-amount-dialog/item-amount-dialog.css deleted file mode 100644 index d500d0e7..00000000 --- a/src/ui/item-amount-dialog/item-amount-dialog.css +++ /dev/null @@ -1,75 +0,0 @@ -#item-amount-dialog { - position: absolute; - background: url("/gfx/gfx002/127.png") -38px 0; - background-color: #000; - width: 265px; - height: 170px; - user-select: none; - z-index: 1020; -} - -#item-amount-dialog .label { - position: absolute; - top: 40px; - color: #e2e0c8; - width: 232px; - left: 16px; - margin: 0; - height: 48px; -} - -#item-amount-dialog .header { - background-color: #000; - width: 243px; - height: 22px; - position: absolute; - top: 10px; - left: 11px; -} - -#item-amount-dialog input[type="number"] { - background: transparent; - border: none; - outline: none; - position: absolute; - right: 20px; - bottom: 58px; - width: 83px; - color: #b4a08c; -} - -#item-amount-dialog button[data-id="ok"] { - position: absolute; - left: 60px; - bottom: 16px; -} - -#item-amount-dialog button[data-id="cancel"] { - position: absolute; - left: 153px; - bottom: 16px; -} - -#item-amount-dialog .header[data-id="drop"] { - display: none; -} - -#item-amount-dialog .header[data-id="junk"] { - background: url("/gfx/gfx002/127.png") -39px -172px; -} - -#item-amount-dialog .header[data-id="give"] { - background: url("/gfx/gfx002/127.png") -39px -196px; -} - -#item-amount-dialog .header[data-id="trade"] { - background: url("/gfx/gfx002/127.png") -39px -220px; -} - -#item-amount-dialog .header[data-id="shop"] { - background: url("/gfx/gfx002/127.png") -39px -244px; -} - -#item-amount-dialog .header[data-id="bank"] { - background: url("/gfx/gfx002/127.png") -39px -268px; -} diff --git a/src/ui/item-amount-dialog/item-amount-dialog.ts b/src/ui/item-amount-dialog/item-amount-dialog.ts deleted file mode 100644 index 9cce40f9..00000000 --- a/src/ui/item-amount-dialog/item-amount-dialog.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { playSfxById, SfxId } from '@/sfx'; -import { Base } from '@/ui/base-ui'; - -import './item-amount-dialog.css'; - -export class ItemAmountDialog extends Base { - protected container = document.getElementById('item-amount-dialog')!; - private cover = document.getElementById('cover'); - private header = this.container!.querySelector('.header'); - private label: HTMLParagraphElement = - this.container!.querySelector('.label')!; - private btnCancel: HTMLButtonElement = this.container!.querySelector( - 'button[data-id="cancel"]', - )!; - private form: HTMLFormElement = this.container!.querySelector('form')!; - private txtAmount: HTMLInputElement = this.container!.querySelector('input')!; - private amount = 1; - private maxAmount = 1; - private callback: ((amount: number) => void) | null = null; - private cancelCallback: (() => void) | null = null; - private slider: HTMLDivElement = - this.container!.querySelector('.slider-container')!; - private thumb: HTMLDivElement = - this.container!.querySelector('.slider-thumb')!; - private dragging = false; - - setHeader(header: 'drop' | 'junk' | 'give' | 'trade' | 'shop' | 'bank') { - this.header!.setAttribute('data-id', header); - } - - setLabel(label: string) { - this.label.innerText = label; - } - - setMaxAmount(amount: number) { - this.maxAmount = amount; - this.txtAmount.max = this.maxAmount.toString(); - } - - setCallback( - callback: (amount: number) => void, - cancelCallback: () => void = () => {}, - ) { - this.callback = callback; - this.cancelCallback = cancelCallback; - } - - show() { - this.amount = 1; - this.txtAmount.value = this.amount.toString(); - this.thumb.style.left = '0'; - this.cover!.classList.remove('hidden'); - this.container!.classList.remove('hidden'); - this.container!.style.left = - `${Math.floor(window.innerWidth / 2 - this.container!.clientWidth / 2)}px`; - this.container!.style.top = - `${Math.floor(window.innerHeight / 2 - this.container!.clientHeight / 2)}px`; - this.txtAmount.focus(); - } - - constructor() { - super(); - - this.form.addEventListener('submit', (e) => { - e.preventDefault(); - - if (this.callback) { - this.callback(this.amount); - this.callback = null; - this.cancelCallback = null; - } - - this.hide(); - this.cover!.classList.add('hidden'); - - return false; - }); - - this.btnCancel.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.hide(); - this.cover!.classList.add('hidden'); - if (this.cancelCallback) { - this.cancelCallback(); - this.cancelCallback = null; - this.callback = null; - } - }); - - this.txtAmount.addEventListener('change', () => { - this.amount = Number.parseInt(this.txtAmount.value, 10); - if (Number.isNaN(this.amount)) { - this.amount = 1; - } - - if (this.amount > this.maxAmount) { - this.amount = this.maxAmount; - this.txtAmount.value = this.amount.toString(); - } - - const percent = (this.amount - 1) / (this.maxAmount - 1); - const sliderWidth = this.slider.offsetWidth; - const x = percent * sliderWidth; - if (this.amount === 1) { - this.thumb.style.left = '0'; - } else { - this.thumb.style.left = `${x - this.thumb.offsetWidth}px`; - } - }); - - this.thumb.addEventListener('mousedown', () => { - this.dragging = true; - }); - - this.thumb.addEventListener('touchstart', () => { - this.dragging = true; - }); - - document.addEventListener('mousemove', (e) => { - if (!this.dragging) { - return; - } - this.setValueFromX(e.clientX); - }); - document.addEventListener('touchmove', (e) => { - if (!this.dragging || !e.touches) { - return; - } - this.setValueFromX(e.touches[0].clientX); - }); - - document.addEventListener('mouseup', () => { - this.dragging = false; - }); - document.addEventListener('touchend', () => { - this.dragging = false; - }); - } - - private setValueFromX(clientX: number) { - const rect = this.slider.getBoundingClientRect(); - const x = Math.min(Math.max(clientX - rect.left, 0), rect.width); - if (x - this.thumb.offsetWidth < 0) { - return; - } - - const percent = x / rect.width; - this.amount = Math.floor(1 + percent * (this.maxAmount - 1)); - this.txtAmount.value = this.amount.toString(); - this.thumb.style.left = `${x - this.thumb.offsetWidth}px`; - } -} diff --git a/src/ui/large-alert-small-header/index.ts b/src/ui/large-alert-small-header/index.ts deleted file mode 100644 index a2a1cc84..00000000 --- a/src/ui/large-alert-small-header/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './large-alert-small-header'; diff --git a/src/ui/large-alert-small-header/large-alert-small-header.css b/src/ui/large-alert-small-header/large-alert-small-header.css deleted file mode 100644 index 91bb4a24..00000000 --- a/src/ui/large-alert-small-header/large-alert-small-header.css +++ /dev/null @@ -1,30 +0,0 @@ -#large-alert-small-header { - position: absolute; - background: url("/gfx/gfx001/125.png"); - background-color: #000; - width: 290px; - height: 189px; - user-select: none; - z-index: 1020; -} - -#large-alert-small-header .title { - position: absolute; - left: 18px; - top: 15px; - width: 258px; -} - -#large-alert-small-header .message { - position: absolute; - left: 18px; - top: 40px; - width: 255px; - height: 40px; -} - -#large-alert-small-header button { - position: absolute; - top: 145px; - right: 18px; -} diff --git a/src/ui/large-alert-small-header/large-alert-small-header.ts b/src/ui/large-alert-small-header/large-alert-small-header.ts deleted file mode 100644 index 956137da..00000000 --- a/src/ui/large-alert-small-header/large-alert-small-header.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { playSfxById, SfxId } from '@/sfx'; -import { Base } from '@/ui/base-ui'; - -import './large-alert-small-header.css'; - -export class LargeAlertSmallHeader extends Base { - protected container = document.getElementById('large-alert-small-header')!; - private cover = document.getElementById('cover'); - private title: HTMLSpanElement = this.container!.querySelector('.title')!; - private message: HTMLSpanElement = this.container!.querySelector('.message')!; - private btnCancel: HTMLButtonElement = this.container!.querySelector( - 'button[data-id="cancel"]', - )!; - - show() { - this.cover!.classList.remove('hidden'); - this.container!.classList.remove('hidden'); - this.container!.style.left = - `${Math.floor(window.innerWidth / 2 - this.container!.clientWidth / 2)}px`; - this.container!.style.top = - `${Math.floor(window.innerHeight / 2 - this.container!.clientHeight / 2)}px`; - } - - constructor() { - super(); - this.btnCancel.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.hide(); - this.cover!.classList.add('hidden'); - }); - } - - setContent(message: string, title = 'Error') { - this.title.innerText = title; - this.message.innerText = message; - } -} diff --git a/src/ui/large-confirm-small-header/index.ts b/src/ui/large-confirm-small-header/index.ts deleted file mode 100644 index 92900142..00000000 --- a/src/ui/large-confirm-small-header/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './large-confirm-small-header'; diff --git a/src/ui/large-confirm-small-header/large-confirm-small-header.css b/src/ui/large-confirm-small-header/large-confirm-small-header.css deleted file mode 100644 index 795ac8ad..00000000 --- a/src/ui/large-confirm-small-header/large-confirm-small-header.css +++ /dev/null @@ -1,32 +0,0 @@ -#large-confirm-small-header { - position: absolute; - background: url("/gfx/gfx001/125.png"); - background-color: #000; - width: 290px; - height: 189px; - user-select: none; - z-index: 1020; -} - -#large-confirm-small-header .title { - position: absolute; - left: 18px; - top: 15px; - width: 258px; -} - -#large-confirm-small-header .message { - position: absolute; - left: 18px; - top: 40px; - width: 255px; - height: 40px; -} - -#large-confirm-small-header .buttons { - position: absolute; - top: 145px; - right: 18px; - display: flex; - gap: 2px; -} diff --git a/src/ui/large-confirm-small-header/large-confirm-small-header.ts b/src/ui/large-confirm-small-header/large-confirm-small-header.ts deleted file mode 100644 index 83c12bad..00000000 --- a/src/ui/large-confirm-small-header/large-confirm-small-header.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { playSfxById, SfxId } from '@/sfx'; -import { Base } from '@/ui/base-ui'; - -import './large-confirm-small-header.css'; - -export class LargeConfirmSmallHeader extends Base { - protected container = document.getElementById('large-confirm-small-header')!; - private cover = document.getElementById('cover'); - private title: HTMLSpanElement = this.container!.querySelector('.title')!; - private message: HTMLSpanElement = this.container!.querySelector('.message')!; - private btnOk: HTMLButtonElement = this.container!.querySelector( - 'button[data-id="ok"]', - )!; - private btnCancel: HTMLButtonElement = this.container!.querySelector( - 'button[data-id="cancel"]', - )!; - private callback: (() => undefined) | null = null; - - show() { - this.cover!.classList.remove('hidden'); - this.container!.classList.remove('hidden'); - this.container!.style.left = - `${Math.floor(window.innerWidth / 2 - this.container!.clientWidth / 2)}px`; - this.container!.style.top = - `${Math.floor(window.innerHeight / 2 - this.container!.clientHeight / 2)}px`; - } - - constructor() { - super(); - this.btnCancel.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.hide(); - this.cover!.classList.add('hidden'); - }); - - this.btnOk.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.hide(); - this.cover!.classList.add('hidden'); - - if (this.callback) { - this.callback(); - } - }); - } - - setContent(message: string, title = 'Error') { - this.title.innerText = title; - this.message.innerText = message; - } - - setCallback(callback: () => undefined) { - this.callback = callback; - } -} diff --git a/src/ui/locker-dialog/index.ts b/src/ui/locker-dialog/index.ts deleted file mode 100644 index fbd5d885..00000000 --- a/src/ui/locker-dialog/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './locker-dialog'; diff --git a/src/ui/locker-dialog/locker-dialog.css b/src/ui/locker-dialog/locker-dialog.css deleted file mode 100644 index 86693abf..00000000 --- a/src/ui/locker-dialog/locker-dialog.css +++ /dev/null @@ -1,109 +0,0 @@ -#locker { - position: relative; - user-select: none; - box-sizing: border-box; - width: 284px; - height: 290px; - background-color: #000; - background-image: url("/gfx/gfx002/152.png"); -} - -#locker .title { - position: absolute; - color: #fff; - font-size: 12px; - left: 24px; - top: 18px; -} - -#locker .scroll-handle { - position: absolute; - right: 16px; - width: 16px; - height: 15px; - background: url("/gfx/gfx002/129.png") 0 -75px; - touch-action: none; - user-select: none; -} - -#locker .locker-items { - position: absolute; - top: 50px; - left: 22px; - color: #fff; - font-size: 12px; - width: 232px; - height: 193px; - overflow-y: scroll; - overflow-x: hidden; - -ms-overflow-style: none; - scrollbar-width: none; -} - -#locker .locker-items::-webkit-scrollbar { - display: none; -} - -#locker .locker-item { - position: relative; - display: grid; - grid-template-columns: 48px auto; - align-items: center; - padding: 0 0.5rem; - cursor: pointer; - height: 38px; - background-image: url("/gfx/gfx003/100.png"); - background-repeat: no-repeat; - background-position: left center; - background-size: auto; - image-rendering: pixelated; -} - -#locker .locker-item::before { - content: ""; - position: absolute; - inset: 0; - background-color: rgba(255, 255, 255, 0.05); - opacity: 0; - pointer-events: none; - z-index: 0; - box-sizing: border-box; -} - -#locker .locker-item:hover::before { - opacity: 1; -} - -#locker .locker-item .item-image { - max-width: 48px; - max-height: 48px; - object-fit: contain; - image-rendering: pixelated; - position: relative; - z-index: 1; - justify-self: center; -} - -#locker .locker-item .item-text { - position: relative; - z-index: 1; - flex: 1; -} - -#locker .locker-item .item-name { - color: #fff; - font-size: 12px; - margin: 0; -} - -#locker .locker-item .item-quantity { - color: #b4a08c; - font-size: 11px; - margin: 2px 0 0 0; -} - -#locker button[data-id="cancel"] { - position: absolute; - top: 251px; - left: 98px; -} diff --git a/src/ui/locker-dialog/locker-dialog.ts b/src/ui/locker-dialog/locker-dialog.ts deleted file mode 100644 index 40d840c8..00000000 --- a/src/ui/locker-dialog/locker-dialog.ts +++ /dev/null @@ -1,153 +0,0 @@ -import type { ThreeItem } from 'eolib'; -import type { Client } from '@/client'; -import { EOResourceID } from '@/edf'; -import { playSfxById, SfxId } from '@/sfx'; -import { Base } from '@/ui/base-ui'; -import { createItemMenuItem } from '@/ui/utils'; -import { capitalize } from '@/utils'; - -import './locker-dialog.css'; - -export class LockerDialog extends Base { - private client: Client; - protected container = document.getElementById('locker')!; - private cover = document.querySelector('#cover'); - private btnCancel = this.container!.querySelector( - 'button[data-id="cancel"]', - ); - private scrollHandle = - this.container!.querySelector('.scroll-handle'); - private title = this.container!.querySelector('.title'); - private dialogs = document.getElementById('dialogs'); - private itemList = - this.container!.querySelector('.locker-items'); - private items: ThreeItem[] = []; - - constructor(client: Client) { - super(); - this.client = client; - - this.btnCancel!.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.hide(); - }); - - this.itemList!.addEventListener('scroll', () => { - this.setScrollThumbPosition(); - }); - - this.scrollHandle!.addEventListener('pointerdown', () => { - const onPointerMove = (e: PointerEvent) => { - const rect = this.itemList!.getBoundingClientRect(); - const min = 30; - const max = 212; - const clampedY = Math.min( - Math.max(e.clientY, rect.top + min), - rect.top + max, - ); - const scrollPercent = (clampedY - rect.top - min) / (max - min); - const scrollHeight = this.itemList!.scrollHeight; - const clientHeight = this.itemList!.clientHeight; - this.itemList!.scrollTop = - scrollPercent * (scrollHeight - clientHeight); - }; - - const onPointerUp = () => { - document.removeEventListener('pointermove', onPointerMove); - document.removeEventListener('pointerup', onPointerUp); - }; - - document.addEventListener('pointermove', onPointerMove); - document.addEventListener('pointerup', onPointerUp); - }); - } - - setItems(items: ThreeItem[]) { - this.items = items; - this.render(); - } - - getItemCount(): number { - return this.items.length; - } - - getItemAmount(id: number): number { - const item = this.items.find((i) => i.id === id); - return item ? item.amount : 0; - } - - setScrollThumbPosition() { - const min = 60; - const max = 212; - const scrollTop = this.itemList!.scrollTop; - const scrollHeight = this.itemList!.scrollHeight; - const clientHeight = this.itemList!.clientHeight; - const scrollPercent = scrollTop / (scrollHeight - clientHeight); - const clampedPercent = Math.min(Math.max(scrollPercent, 0), 1); - const top = min + (max - min) * clampedPercent || min; - this.scrollHandle!.style.top = `${top}px`; - } - - show() { - this.cover!.classList.remove('hidden'); - this.container!.classList.remove('hidden'); - this.dialogs!.classList.remove('hidden'); - this.client.typing = true; - this.setScrollThumbPosition(); - } - - hide() { - this.cover!.classList.add('hidden'); - this.container!.classList.add('hidden'); - - if (!document.querySelector('#dialogs > div:not(.hidden)')) { - this.dialogs!.classList.add('hidden'); - this.client.typing = false; - } - } - - private render() { - this.itemList!.innerHTML = ''; - this.title!.innerText = - `${capitalize(this.client.name)}'s ${this.client.getResourceString(EOResourceID.DIALOG_TITLE_PRIVATE_LOCKER)} [${this.items.length}]`; - - if (this.items.length === 0) { - return; - } - - for (const item of this.items) { - const record = this.client.getEifRecordById(item.id); - if (!record) { - continue; - } - - const itemElement = createItemMenuItem( - item.id, - record, - record.name, - `x ${item.amount}`, - ); - - itemElement.addEventListener('contextmenu', () => { - if ( - this.client.weight.current + record.weight > - this.client.weight.max - ) { - this.client.emit('smallAlert', { - title: this.client.getResourceString( - EOResourceID.STATUS_LABEL_TYPE_WARNING, - ), - message: this.client.getResourceString( - EOResourceID.DIALOG_ITS_TOO_HEAVY_WEIGHT, - ), - }); - return; - } - - this.client.lockerController.takeItem(item.id); - }); - - this.itemList!.appendChild(itemElement); - } - } -} diff --git a/src/ui/login/index.ts b/src/ui/login/index.ts deleted file mode 100644 index 6cc1e6e2..00000000 --- a/src/ui/login/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './login'; diff --git a/src/ui/login/login.css b/src/ui/login/login.css deleted file mode 100644 index e12a667f..00000000 --- a/src/ui/login/login.css +++ /dev/null @@ -1,49 +0,0 @@ -#login-form { - background: url("/gfx/gfx001/102.png"); - background-color: #000; - width: 320px; - height: 159px; - margin: auto; - position: relative; -} - -label[for="login-remember"] { - position: absolute; - top: 91px; - left: 126px; - font-size: 11px; - color: #fff; -} - -input[type="text"], -input[type="password"] { - background: #a38269; - color: #000; - border: 1px solid #000; - outline: none; -} - -#login-username { - position: absolute; - top: 35px; - left: 130px; - width: 138px; - height: 17px; -} - -#login-password { - position: absolute; - top: 72px; - left: 130px; - width: 138px; - height: 17px; -} - -#login-form .buttons { - position: absolute; - bottom: 20px; - display: flex; - gap: 2px; - width: 100%; - justify-content: center; -} diff --git a/src/ui/login/login.ts b/src/ui/login/login.ts deleted file mode 100644 index f3bbce5e..00000000 --- a/src/ui/login/login.ts +++ /dev/null @@ -1,90 +0,0 @@ -import mitt from 'mitt'; -import { playSfxById, SfxId } from '@/sfx'; -import { Base } from '@/ui/base-ui'; - -import './login.css'; - -type Events = { - login: { username: string; password: string; rememberMe: boolean }; - cancel: undefined; -}; - -export class LoginForm extends Base { - protected container = document.getElementById('login-form')!; - private form: HTMLFormElement = this.container!.querySelector('form')!; - private username: HTMLInputElement = - this.container!.querySelector('#login-username')!; - private password: HTMLInputElement = - this.container!.querySelector('#login-password')!; - private chkRememberMe: HTMLInputElement = - this.container!.querySelector('#login-remember')!; - private rememberMe = Boolean(localStorage.getItem('remember-me')) || false; - private btnCancel: HTMLButtonElement = this.container!.querySelector( - 'button[data-id="cancel"]', - )!; - - private emitter = mitt(); - private formElements: HTMLInputElement[]; - - show() { - this.username.value = ''; - this.password.value = ''; - this.chkRememberMe.checked = this.rememberMe; - this.container!.classList.remove('hidden'); - this.username.focus(); - } - - constructor() { - super(); - - this.formElements = [this.username, this.password]; - - this.form.addEventListener('submit', (e) => { - e.preventDefault(); - playSfxById(SfxId.ButtonClick); - this.emitter.emit('login', { - username: this.username.value.trim(), - password: this.password.value.trim(), - rememberMe: this.chkRememberMe.checked, - }); - this.password.value = ''; - this.password.focus(); - localStorage.setItem('remember-me', `${this.chkRememberMe.checked}`); - return false; - }); - - this.btnCancel.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.emitter.emit('cancel', undefined); - }); - - this.setupTabTrapping(); - } - - private setupTabTrapping() { - this.formElements.forEach((element, index) => { - element.addEventListener('keydown', (e: KeyboardEvent) => { - if (e.key === 'Tab' && !this.container!.classList.contains('hidden')) { - e.preventDefault(); - - if (e.shiftKey) { - const prevIndex = - index === 0 ? this.formElements.length - 1 : index - 1; - this.formElements[prevIndex].focus(); - } else { - const nextIndex = - index === this.formElements.length - 1 ? 0 : index + 1; - this.formElements[nextIndex].focus(); - } - } - }); - }); - } - - on( - event: Event, - handler: (data: Events[Event]) => void, - ) { - this.emitter.on(event, handler); - } -} diff --git a/src/ui/main-menu/index.ts b/src/ui/main-menu/index.ts deleted file mode 100644 index 017fe41b..00000000 --- a/src/ui/main-menu/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './main-menu'; diff --git a/src/ui/main-menu/main-menu.css b/src/ui/main-menu/main-menu.css deleted file mode 100644 index 1c3aa9f6..00000000 --- a/src/ui/main-menu/main-menu.css +++ /dev/null @@ -1,25 +0,0 @@ -#main-menu { - display: flex; - flex-flow: column; - width: 100%; - align-self: center; - align-items: center; - gap: 2px; -} - -#main-menu-logo { - margin-bottom: 10px; - position: relative; -} - -#main-menu-logo::after { - content: attr(data-slogan); - position: absolute; - display: inline-block; - font-style: italic; - font-family: monospace; - color: #f9f92c; - bottom: 15px; - right: 0; - animation: pulse 2s infinite; -} diff --git a/src/ui/main-menu/main-menu.ts b/src/ui/main-menu/main-menu.ts deleted file mode 100644 index 97d052f6..00000000 --- a/src/ui/main-menu/main-menu.ts +++ /dev/null @@ -1,54 +0,0 @@ -import mitt from 'mitt'; -import { playSfxById, SfxId } from '@/sfx'; -import { Base } from '@/ui/base-ui'; - -import './main-menu.css'; - -type Events = { - 'create-account': undefined; - 'play-game': undefined; - 'view-credits': undefined; - 'host-change': string; -}; - -export class MainMenu extends Base { - protected container = document.querySelector('#main-menu')!; - private btnCreateAccount: HTMLButtonElement = this.container!.querySelector( - 'button[data-id="create-account"]', - )!; - private btnPlayGame: HTMLButtonElement = this.container!.querySelector( - 'button[data-id="play-game"]', - )!; - private btnViewCredits: HTMLButtonElement = this.container!.querySelector( - 'button[data-id="view-credits"]', - )!; - private txtHost: HTMLInputElement = - this.container!.querySelector('input[name="host"]')!; - private emitter = mitt(); - - constructor() { - super(); - this.btnCreateAccount.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.emitter.emit('create-account', undefined); - }); - this.btnPlayGame.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.emitter.emit('play-game', undefined); - }); - this.btnViewCredits.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.emitter.emit('view-credits', undefined); - }); - this.txtHost.addEventListener('change', () => { - this.emitter.emit('host-change', this.txtHost.value); - }); - } - - on( - event: Event, - handler: (data: Events[Event]) => void, - ) { - this.emitter.on(event, handler); - } -} diff --git a/src/ui/mobile-controls/index.ts b/src/ui/mobile-controls/index.ts deleted file mode 100644 index 357abc35..00000000 --- a/src/ui/mobile-controls/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './mobile-controls'; diff --git a/src/ui/mobile-controls/mobile-controls.css b/src/ui/mobile-controls/mobile-controls.css deleted file mode 100644 index 64b15817..00000000 --- a/src/ui/mobile-controls/mobile-controls.css +++ /dev/null @@ -1,56 +0,0 @@ -#mobile-controls { - opacity: 0.8; - z-index: 1020; -} - -#joystick-container { - position: absolute; - bottom: 5%; - left: 5%; - width: 120px; - height: 120px; - touch-action: none; -} - -#joystick-base { - width: 100%; - height: 100%; - border-radius: 50%; - background-color: transparent; - background-image: url("/move.png"); - background-size: cover; - position: absolute; -} - -#joystick-thumb { - width: 80px; - height: 80px; - border-radius: 50%; - position: absolute; - background-color: transparent; - background-image: url("/move-circle.png"); - background-size: cover; - left: 20px; - top: 20px; - transform: translate(0, 0); -} - -#mobile-actions-container { - position: absolute; - bottom: 1%; - right: 10%; - width: 170px; - height: 120px; - touch-action: none; - display: flex; -} - -#mobile-actions-container button { - border: none; - outline: none; - height: 84px; - width: 84px; - background-color: transparent; - background-size: cover; - background-repeat: no-repeat; -} diff --git a/src/ui/mobile-controls/mobile-controls.ts b/src/ui/mobile-controls/mobile-controls.ts deleted file mode 100644 index 8c60d95e..00000000 --- a/src/ui/mobile-controls/mobile-controls.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Base } from '@/ui/base-ui'; - -import './mobile-controls.css'; - -export class MobileControls extends Base { - protected container = document.getElementById('mobile-controls')!; -} diff --git a/src/ui/online-list/index.ts b/src/ui/online-list/index.ts deleted file mode 100644 index 555707a4..00000000 --- a/src/ui/online-list/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './online-list'; diff --git a/src/ui/online-list/online-list.css b/src/ui/online-list/online-list.css deleted file mode 100644 index 8ded2b7a..00000000 --- a/src/ui/online-list/online-list.css +++ /dev/null @@ -1,5 +0,0 @@ -#online-list.dialog-md .dialog-contents { - display: flex; - flex-direction: row; - justify-content: flex-start; -} diff --git a/src/ui/online-list/online-list.ts b/src/ui/online-list/online-list.ts deleted file mode 100644 index 56ad64a8..00000000 --- a/src/ui/online-list/online-list.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { PlayersRequestClientPacket } from 'eolib'; -import type { Client } from '@/client'; -import { BaseDialogMd } from '@/ui/base-dialog-md'; -import { characterIconToChatIcon } from '@/ui/utils'; - -import './online-list.css'; - -type Events = { - playerSelected: { playerId: number }; -}; - -export class OnlineList extends BaseDialogMd { - constructor(client: Client) { - super(client, document.querySelector('#online-list')!, 'Online Players'); - - const playersContainer = this.container.querySelector('.players'); - - this.client.on('playersListUpdated', (players) => { - if (!this.container) return; - - this.updateLabelText(`Online Players (${players.length})`); - playersContainer!.innerHTML = ''; - - for (const player of players) { - const playerElement = document.createElement('div'); - playerElement.className = 'player'; - - const nameplateElement = document.createElement('div'); - nameplateElement.className = 'nameplate'; - - const nameElement = document.createElement('span'); - nameElement.className = 'name'; - const playerIconElement = document.createElement('div'); - playerIconElement.className = 'icon'; - playerIconElement.setAttribute( - 'data-id', - characterIconToChatIcon(player.icon).toString(), - ); - const guildElement = document.createElement('span'); - guildElement.className = 'guild'; - guildElement.textContent = - !player.guildTag || player.guildTag === ' ' - ? '' - : `${player.guildTag}`; - - nameplateElement.appendChild(playerIconElement); - nameplateElement.appendChild(nameElement); - nameplateElement.appendChild(guildElement); - - const levelElement = document.createElement('span'); - levelElement.className = 'level'; - levelElement.textContent = `(Lvl: ${player.level})`; - - const titleElement = document.createElement('span'); - titleElement.className = 'title'; - titleElement.textContent = player.title ? `${player.title}` : ''; - - const classElement = document.createElement('span'); - classElement.className = 'class'; - classElement.textContent = `Lvl: ${player.level} ${this.client.ecf!.classes[player.classId - 1]?.name || ''}`; - - nameElement.textContent = player.name; - playerElement.appendChild(nameplateElement); - playerElement.appendChild(classElement); - playerElement.appendChild(titleElement); - playersContainer!.appendChild(playerElement); - - playerElement.addEventListener('contextmenu', () => { - const chatBox = document.getElementById( - 'chat-message', - ) as HTMLInputElement; - if (chatBox) { - chatBox.value = `!${player.name} `; - } - }); - } - }); - } - - show() { - this.client.bus!.send(new PlayersRequestClientPacket()); - super.show(); - } - - render(): void {} -} diff --git a/src/ui/paperdoll/index.ts b/src/ui/paperdoll/index.ts deleted file mode 100644 index 6279386a..00000000 --- a/src/ui/paperdoll/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './paperdoll'; diff --git a/src/ui/paperdoll/paperdoll.css b/src/ui/paperdoll/paperdoll.css deleted file mode 100644 index 4ef17287..00000000 --- a/src/ui/paperdoll/paperdoll.css +++ /dev/null @@ -1,208 +0,0 @@ -#paperdoll { - position: relative; - background: url("/gfx/gfx002/149.png") #000; - height: 290px; - width: 380px; - z-index: 1020; -} - -#paperdoll[data-gender="1"] { - background-position-y: -290px; -} - -#paperdoll button[data-id="ok"] { - position: absolute; - bottom: 8px; - right: 13px; -} - -#paperdoll .item, -#paperdoll span { - position: absolute; -} - -#paperdoll .item { - display: flex; - justify-content: center; -} - -#paperdoll img { - align-self: center; -} - -#paperdoll .item[data-id="boots"] { - left: 85px; - bottom: 17px; - width: 57px; - height: 55px; -} - -#paperdoll .item[data-id="accessory"] { - left: 52px; - bottom: 16px; - width: 26px; - height: 26px; -} - -#paperdoll .item[data-id="gloves"] { - left: 20px; - bottom: 48px; - width: 57px; - height: 55px; -} - -#paperdoll .item[data-id="belt"] { - left: 86px; - bottom: 79px; - width: 56px; - height: 25px; -} - -#paperdoll .item[data-id="armor"] { - left: 85px; - top: 81px; - height: 99px; - width: 57px; -} - -#paperdoll .item[data-id="necklace"] { - left: 151px; - top: 50px; - width: 56px; - height: 25px; -} - -#paperdoll .item[data-id="hat"] { - left: 85px; - top: 19px; - width: 57px; - height: 55px; -} - -#paperdoll .item[data-id="shield"] { - left: 150px; - top: 80px; - height: 99px; - width: 57px; -} - -#paperdoll .item[data-id="weapon"] { - left: 20px; - top: 81px; - height: 99px; - width: 57px; -} - -#paperdoll .item[data-id="ring-1"] { - left: 150px; - top: 186px; - width: 26px; - height: 26px; -} - -#paperdoll .item[data-id="ring-2"] { - left: 183px; - top: 186px; - width: 26px; - height: 26px; -} - -#paperdoll .item[data-id="armlet-1"] { - left: 150px; - top: 217px; - width: 26px; - height: 26px; -} - -#paperdoll .item[data-id="armlet-2"] { - left: 183px; - top: 217px; - width: 26px; - height: 26px; -} - -#paperdoll .item[data-id="bracer-1"] { - left: 150px; - top: 248px; - width: 26px; - height: 26px; -} - -#paperdoll .item[data-id="bracer-2"] { - left: 183px; - top: 248px; - width: 26px; - height: 26px; -} - -#paperdoll .item .tooltip { - position: absolute; - bottom: 100%; - /* Position above the item */ - left: 50%; - transform: translateX(-50%); - background-color: black; - color: white; - padding: 4px 8px; - border-radius: 4px; - font-size: 12px; - white-space: nowrap; - opacity: 0; - pointer-events: none; - z-index: 10; -} - -#paperdoll .item:hover .tooltip { - opacity: 0.8; -} - -#paperdoll div.icon { - position: absolute; - left: 240px; - top: 262px; -} - -#paperdoll span { - color: #f0f0c8; - font-size: 12px; -} - -#paperdoll span[data-id="name"] { - left: 230px; - top: 24px; -} - -#paperdoll span[data-id="home"] { - left: 230px; - top: 54px; -} - -#paperdoll span[data-id="class"] { - left: 230px; - top: 84px; -} - -#paperdoll span[data-id="partner"] { - left: 230px; - top: 114px; -} - -#paperdoll span[data-id="title"] { - left: 230px; - top: 144px; -} - -#paperdoll span[data-id="job"] { - left: 230px; - top: 174px; -} - -#paperdoll span[data-id="guild"] { - left: 230px; - top: 204px; -} - -#paperdoll span[data-id="rank"] { - left: 230px; - top: 234px; -} diff --git a/src/ui/paperdoll/paperdoll.ts b/src/ui/paperdoll/paperdoll.ts deleted file mode 100644 index a5ef31a4..00000000 --- a/src/ui/paperdoll/paperdoll.ts +++ /dev/null @@ -1,361 +0,0 @@ -import { CharacterDetails, CharacterIcon, EquipmentPaperdoll } from 'eolib'; -import type { Client } from '@/client'; -import { playSfxById, SfxId } from '@/sfx'; -import { Base } from '@/ui/base-ui'; -import { characterIconToChatIcon, setItemGridImageFromGfx } from '@/ui/utils'; -import { capitalize, getItemMeta } from '@/utils'; - -import './paperdoll.css'; -import { EquipmentSlot } from '@/equipment'; - -export class Paperdoll extends Base { - protected container = document.getElementById('paperdoll')!; - private dialogs = document.getElementById('dialogs'); - private client: Client; - private cover = document.getElementById('cover'); - private bntOk = this.container!.querySelector( - 'button[data-id="ok"]', - ); - private imgBoots: HTMLDivElement = this.container!.querySelector( - '.item[data-id="boots"]', - )!; - private imgAccessory: HTMLDivElement = this.container!.querySelector( - '.item[data-id="accessory"]', - )!; - private imgGloves: HTMLDivElement = this.container!.querySelector( - '.item[data-id="gloves"]', - )!; - private imgBelt: HTMLDivElement = this.container!.querySelector( - '.item[data-id="belt"]', - )!; - private imgArmor: HTMLDivElement = this.container!.querySelector( - '.item[data-id="armor"]', - )!; - private imgNecklace: HTMLDivElement = this.container!.querySelector( - '.item[data-id="necklace"]', - )!; - private imgHat: HTMLImageElement = this.container!.querySelector( - '.item[data-id="hat"]', - )!; - private imgShield: HTMLDivElement = this.container!.querySelector( - '.item[data-id="shield"]', - )!; - private imgWeapon: HTMLDivElement = this.container!.querySelector( - '.item[data-id="weapon"]', - )!; - private imgRing1: HTMLDivElement = this.container!.querySelector( - '.item[data-id="ring-1"]', - )!; - private imgRing2: HTMLDivElement = this.container!.querySelector( - '.item[data-id="ring-2"]', - )!; - private imgArmlet1: HTMLDivElement = this.container!.querySelector( - '.item[data-id="armlet-1"]', - )!; - private imgArmlet2: HTMLDivElement = this.container!.querySelector( - '.item[data-id="armlet-2"]', - )!; - private imgBracer1: HTMLDivElement = this.container!.querySelector( - '.item[data-id="bracer-1"]', - )!; - private imgBracer2: HTMLDivElement = this.container!.querySelector( - '.item[data-id="bracer-2"]', - )!; - private spanName: HTMLSpanElement = this.container!.querySelector( - 'span[data-id="name"]', - )!; - private spanHome: HTMLSpanElement = this.container!.querySelector( - 'span[data-id="home"]', - )!; - private spanClass: HTMLSpanElement = this.container!.querySelector( - 'span[data-id="class"]', - )!; - private spanPartner: HTMLSpanElement = this.container!.querySelector( - 'span[data-id="partner"]', - )!; - private spanTitle: HTMLSpanElement = this.container!.querySelector( - 'span[data-id="title"]', - )!; - private spanGuild: HTMLSpanElement = this.container!.querySelector( - 'span[data-id="guild"]', - )!; - private spanRank: HTMLSpanElement = this.container!.querySelector( - 'span[data-id="rank"]', - )!; - private divIcon: HTMLDivElement = this.container!.querySelector('div.icon')!; - - private icon = CharacterIcon.Player; - private details = new CharacterDetails(); - private equipment = new EquipmentPaperdoll(); - - constructor(client: Client) { - super(); - this.client = client; - this.bntOk!.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.hide(); - }); - - this.client.on('equipmentChanged', () => { - if (this.details.playerId === client.playerId) { - this.equipment.accessory = client.equipment.accessory; - this.equipment.armlet = client.equipment.armlet; - this.equipment.armor = client.equipment.armor; - this.equipment.belt = client.equipment.belt; - this.equipment.boots = client.equipment.boots; - this.equipment.bracer = client.equipment.bracer; - this.equipment.gloves = client.equipment.gloves; - this.equipment.hat = client.equipment.hat; - this.equipment.necklace = client.equipment.necklace; - this.equipment.ring = client.equipment.ring; - this.equipment.shield = client.equipment.shield; - this.equipment.weapon = client.equipment.weapon; - this.render(); - } - }); - - this.imgAccessory.addEventListener('contextmenu', () => { - if (this.details.playerId !== this.client.playerId) { - return; - } - this.client.inventoryController.unequipItem(EquipmentSlot.Accessory); - }); - - this.imgArmlet1.addEventListener('contextmenu', () => { - if (this.details.playerId !== this.client.playerId) { - return; - } - this.client.inventoryController.unequipItem(EquipmentSlot.Armlet1); - }); - - this.imgArmlet2.addEventListener('contextmenu', () => { - if (this.details.playerId !== this.client.playerId) { - return; - } - this.client.inventoryController.unequipItem(EquipmentSlot.Armlet2); - }); - - this.imgArmor.addEventListener('contextmenu', () => { - if (this.details.playerId !== this.client.playerId) { - return; - } - this.client.inventoryController.unequipItem(EquipmentSlot.Armor); - }); - - this.imgBelt.addEventListener('contextmenu', () => { - if (this.details.playerId !== this.client.playerId) { - return; - } - this.client.inventoryController.unequipItem(EquipmentSlot.Belt); - }); - - this.imgBoots.addEventListener('contextmenu', () => { - if (this.details.playerId !== this.client.playerId) { - return; - } - this.client.inventoryController.unequipItem(EquipmentSlot.Boots); - }); - - this.imgBracer1.addEventListener('contextmenu', () => { - if (this.details.playerId !== this.client.playerId) { - return; - } - this.client.inventoryController.unequipItem(EquipmentSlot.Bracer1); - }); - - this.imgBracer2.addEventListener('contextmenu', () => { - if (this.details.playerId !== this.client.playerId) { - return; - } - this.client.inventoryController.unequipItem(EquipmentSlot.Bracer2); - }); - - this.imgGloves.addEventListener('contextmenu', () => { - if (this.details.playerId !== this.client.playerId) { - return; - } - this.client.inventoryController.unequipItem(EquipmentSlot.Gloves); - }); - - this.imgHat.addEventListener('contextmenu', () => { - if (this.details.playerId !== this.client.playerId) { - return; - } - this.client.inventoryController.unequipItem(EquipmentSlot.Hat); - }); - - this.imgNecklace.addEventListener('contextmenu', () => { - if (this.details.playerId !== this.client.playerId) { - return; - } - this.client.inventoryController.unequipItem(EquipmentSlot.Necklace); - }); - - this.imgRing1.addEventListener('contextmenu', () => { - if (this.details.playerId !== this.client.playerId) { - return; - } - this.client.inventoryController.unequipItem(EquipmentSlot.Ring1); - }); - - this.imgRing2.addEventListener('contextmenu', () => { - if (this.details.playerId !== this.client.playerId) { - return; - } - this.client.inventoryController.unequipItem(EquipmentSlot.Ring2); - }); - - this.imgShield.addEventListener('contextmenu', () => { - if (this.details.playerId !== this.client.playerId) { - return; - } - this.client.inventoryController.unequipItem(EquipmentSlot.Shield); - }); - - this.imgWeapon.addEventListener('contextmenu', () => { - if (this.details.playerId !== this.client.playerId) { - return; - } - this.client.inventoryController.unequipItem(EquipmentSlot.Weapon); - }); - } - - setData( - icon: CharacterIcon, - details: CharacterDetails, - equipment: EquipmentPaperdoll, - ) { - this.icon = icon; - this.details = details; - this.equipment = equipment; - } - - private render() { - this.container!.setAttribute('data-gender', this.details.gender.toString()); - - this.spanName.innerText = capitalize(this.details.name); - this.spanHome.innerText = this.details.home; - - const classRecord = this.client.getEcfRecordById(this.details.classId); - if (classRecord) { - this.spanClass.innerText = classRecord.name; - } else { - this.spanClass.innerText = ''; - } - - this.spanPartner.innerText = capitalize(this.details.partner); - this.spanTitle.innerText = this.details.title; - this.spanGuild.innerText = this.details.guild; - this.spanRank.innerText = this.details.guildRank; - - this.divIcon.setAttribute( - 'data-id', - characterIconToChatIcon(this.icon).toString(), - ); - - this.setEquipment(EquipmentSlot.Boots, this.equipment.boots, this.imgBoots); - this.setEquipment( - EquipmentSlot.Accessory, - this.equipment.accessory, - this.imgAccessory, - ); - this.setEquipment( - EquipmentSlot.Gloves, - this.equipment.gloves, - this.imgGloves, - ); - this.setEquipment(EquipmentSlot.Belt, this.equipment.belt, this.imgBelt); - this.setEquipment(EquipmentSlot.Armor, this.equipment.armor, this.imgArmor); - this.setEquipment( - EquipmentSlot.Necklace, - this.equipment.necklace, - this.imgNecklace, - ); - this.setEquipment(EquipmentSlot.Hat, this.equipment.hat, this.imgHat); - this.setEquipment( - EquipmentSlot.Shield, - this.equipment.shield, - this.imgShield, - ); - this.setEquipment( - EquipmentSlot.Weapon, - this.equipment.weapon, - this.imgWeapon, - ); - this.setEquipment( - EquipmentSlot.Ring1, - this.equipment.ring[0], - this.imgRing1, - ); - this.setEquipment( - EquipmentSlot.Ring2, - this.equipment.ring[1], - this.imgRing2, - ); - this.setEquipment( - EquipmentSlot.Armlet1, - this.equipment.armlet[0], - this.imgArmlet1, - ); - this.setEquipment( - EquipmentSlot.Armlet2, - this.equipment.armlet[1], - this.imgArmlet2, - ); - this.setEquipment( - EquipmentSlot.Bracer1, - this.equipment.bracer[0], - this.imgBracer1, - ); - this.setEquipment( - EquipmentSlot.Bracer2, - this.equipment.bracer[1], - this.imgBracer2, - ); - } - - private setEquipment( - _slot: EquipmentSlot, - itemId: number, - el: HTMLDivElement, - ) { - const img = el.querySelector('img'); - const tooltip = el.querySelector('.tooltip'); - - img!.src = ''; - tooltip!.innerText = ''; - tooltip!.classList.add('hidden'); - - if (!itemId) { - return; - } - - const record = this.client.getEifRecordById(itemId); - if (!record) { - return; - } - - const meta = getItemMeta(record); - void setItemGridImageFromGfx(img!, record.graphicId); - tooltip!.innerText = `${record.name}\n${meta.join('\n')}`; - tooltip!.classList.remove('hidden'); - } - - show() { - this.render(); - this.cover!.classList.remove('hidden'); - this.container!.classList.remove('hidden'); - this.dialogs!.classList.remove('hidden'); - this.client.typing = true; - } - - hide() { - this.container!.classList.add('hidden'); - this.cover!.classList.add('hidden'); - - if (!document.querySelector('#dialogs > div:not(.hidden)')) { - this.dialogs!.classList.add('hidden'); - this.client.typing = false; - } - } -} diff --git a/src/ui/party-dialog/index.ts b/src/ui/party-dialog/index.ts deleted file mode 100644 index c8e42df8..00000000 --- a/src/ui/party-dialog/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './party-dialog'; diff --git a/src/ui/party-dialog/party-dialog.css b/src/ui/party-dialog/party-dialog.css deleted file mode 100644 index 9a814b8b..00000000 --- a/src/ui/party-dialog/party-dialog.css +++ /dev/null @@ -1,106 +0,0 @@ -#party { - position: relative; - background-color: #000; - background-image: url("/gfx/gfx002/152.png"); - width: 284px; - height: 290px; - user-select: none; -} - -#party .member-list { - position: absolute; - top: 50px; - left: 15px; - width: 233px; - height: 192px; - display: flex; - flex-wrap: wrap; - justify-content: space-evenly; - gap: 6px; - overflow-y: scroll; - overflow-x: hidden; - -ms-overflow-style: none; - scrollbar-width: none; - touch-action: pan-y; -} - -#party .member-list > div { - display: flex; - flex-direction: column; - align-items: center; -} - -#party .name-container { - display: flex; - flex-direction: row; - align-items: center; - justify-items: center; - gap: 2px; - min-height: 14px; -} - -#party .member-name { - color: #fff; - font-size: 10px; - max-width: 32px; - text-align: center; -} - -#party .remove-icon { - background: url("/gfx/gfx002/143.png"); - width: 14px; - height: 10px; - cursor: pointer; -} - -#party .remove-icon:hover { - background-position-y: -20px; -} - -#party .member-level { - color: #b4a08c; - font-size: 9px; - position: relative; -} - -#party .member-list::-webkit-scrollbar { - display: none; -} - -#party .hp-bar-container { - margin: 5px 0; - width: 50px; - height: 6px; - background: rgb(8, 8, 8); - border-radius: 5px; - border: 1px solid #fff; -} - -#party .hp-bar { - height: 100%; - border-radius: 5px; -} - -#party .scroll-handle { - position: absolute; - right: 16px; - width: 16px; - height: 15px; - background: url("/gfx/gfx002/129.png") 0 -75px; - touch-action: none; - user-select: none; -} - -#party .label { - position: absolute; - top: 18px; - left: 24px; - color: #fff; - font-size: 12px; -} - -#party button[data-id="cancel"] { - position: absolute; - bottom: 10px; - left: 98px; -} diff --git a/src/ui/party-dialog/party-dialog.ts b/src/ui/party-dialog/party-dialog.ts deleted file mode 100644 index 08f38d53..00000000 --- a/src/ui/party-dialog/party-dialog.ts +++ /dev/null @@ -1,175 +0,0 @@ -import type { Client } from '@/client'; -import { playSfxById, SfxId } from '@/sfx'; -import { Base } from '@/ui/base-ui'; -import { ChatIcon } from '@/ui/ui-types'; -import { capitalize } from '@/utils'; - -import './party-dialog.css'; - -export class PartyDialog extends Base { - protected container = document.getElementById('party')!; - private client: Client; - private dialogs = document.getElementById('dialogs'); - private btnCancel: HTMLButtonElement = this.container!.querySelector( - 'button[data-id="cancel"]', - )!; - private memberList: HTMLDivElement = - this.container!.querySelector('.member-list')!; - private label: HTMLSpanElement = this.container!.querySelector('.label')!; - private scrollHandle: HTMLDivElement = - this.container!.querySelector('.scroll-handle')!; - private open = false; - - constructor(client: Client) { - super(); - this.client = client; - - this.btnCancel.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.hide(); - }); - - this.memberList.addEventListener('scroll', () => { - this.setScrollThumbPosition(); - }); - - this.scrollHandle.addEventListener('pointerdown', () => { - const onPointerMove = (e: PointerEvent) => { - const rect = this.memberList.getBoundingClientRect(); - const min = 30; - const max = 212; - const clampedY = Math.min( - Math.max(e.clientY, rect.top + min), - rect.top + max, - ); - const scrollPercent = (clampedY - rect.top - min) / (max - min); - const scrollHeight = this.memberList.scrollHeight; - const clientHeight = this.memberList.clientHeight; - this.memberList.scrollTop = - scrollPercent * (scrollHeight - clientHeight); - }; - - const onPointerUp = () => { - document.removeEventListener('pointermove', onPointerMove); - document.removeEventListener('pointerup', onPointerUp); - }; - - document.addEventListener('pointermove', onPointerMove); - document.addEventListener('pointerup', onPointerUp); - }); - } - - setScrollThumbPosition() { - const min = 60; - const max = 212; - const scrollTop = this.memberList.scrollTop; - const scrollHeight = this.memberList.scrollHeight; - const clientHeight = this.memberList.clientHeight; - const scrollPercent = scrollTop / (scrollHeight - clientHeight); - const clampedPercent = Math.min(Math.max(scrollPercent, 0), 1); - const top = min + (max - min) * clampedPercent || min; - this.scrollHandle.style.top = `${top}px`; - } - - show() { - this.render(); - this.container!.classList.remove('hidden'); - this.dialogs!.classList.remove('hidden'); - this.open = true; - this.setScrollThumbPosition(); - } - - hide() { - this.container!.classList.add('hidden'); - this.open = false; - - if (!document.querySelector('#dialogs > div:not(.hidden)')) { - this.dialogs!.classList.add('hidden'); - this.client.typing = false; - } - } - - toggle() { - if (this.open) { - this.hide(); - } else { - this.client.socialController.requestPartyList(); - this.show(); - } - } - - refresh() { - this.render(); - } - - private render() { - this.label.textContent = `Party Members (${this.client.partyMembers.length})`; - this.memberList.innerHTML = ''; - - const leaderPlayerId = this.client.partyMembers.find( - (m) => m.leader, - )?.playerId; - if (!leaderPlayerId) { - console.warn('No party leader found'); - return; - } - - for (const member of this.client.partyMembers) { - const memberDiv = document.createElement('div'); - - const nameContainer = document.createElement('div'); - nameContainer.classList.add('name-container'); - - const nameSpan = document.createElement('span'); - nameSpan.classList.add('member-name'); - nameSpan.textContent = capitalize(member.name); - nameContainer.appendChild(nameSpan); - - if (member.leader) { - const leaderIcon = document.createElement('div'); - leaderIcon.classList.add('icon'); - leaderIcon.setAttribute('data-id', ChatIcon.Star.toString()); - nameContainer.appendChild(leaderIcon); - } - - memberDiv.appendChild(nameContainer); - - const levelSpan = document.createElement('span'); - levelSpan.classList.add('member-level'); - levelSpan.textContent = `Lvl: ${member.level}`; - memberDiv.appendChild(levelSpan); - - const hpBarContainer = document.createElement('div'); - hpBarContainer.classList.add('hp-bar-container'); - - const hpBar = document.createElement('div'); - hpBar.classList.add('hp-bar'); - const hpPercentage = member.hpPercentage || 0; - hpBar.style.width = `${hpPercentage}%`; - if (hpPercentage > 50) { - hpBar.style.backgroundColor = '#50aa2d'; - } else if (hpPercentage > 25) { - hpBar.style.backgroundColor = '#FEB04A'; - } else { - hpBar.style.backgroundColor = '#F76251'; - } - hpBarContainer.appendChild(hpBar); - memberDiv.appendChild(hpBarContainer); - - if ( - this.client.playerId === leaderPlayerId || - member.playerId === this.client.playerId - ) { - const removeIcon = document.createElement('div'); - removeIcon.classList.add('remove-icon'); - removeIcon.title = 'Remove from party'; - removeIcon.addEventListener('click', () => { - this.client.socialController.removePartyMember(member.playerId); - }); - memberDiv.appendChild(removeIcon); - } - - this.memberList.appendChild(memberDiv); - } - } -} diff --git a/src/ui/quest-dialog/index.ts b/src/ui/quest-dialog/index.ts deleted file mode 100644 index 01373d8f..00000000 --- a/src/ui/quest-dialog/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './quest-dialog'; diff --git a/src/ui/quest-dialog/quest-dialog.css b/src/ui/quest-dialog/quest-dialog.css deleted file mode 100644 index 0a9c9609..00000000 --- a/src/ui/quest-dialog/quest-dialog.css +++ /dev/null @@ -1,71 +0,0 @@ -#quest-dialog { - position: relative; - background-color: #000; - background-image: url("/gfx/gfx002/167.png"); - width: 284px; - height: 190px; - user-select: none; - z-index: 1020; -} - -#quest-dialog .title { - position: absolute; - top: 18px; - left: 18px; - color: #fff; -} - -#quest-dialog .entries { - list-style: none; - margin: 0; - padding: 0; - position: absolute; - top: 48px; - left: 20px; - color: #fff; - font-size: 12px; - width: 224px; -} - -#quest-dialog li { - position: relative; -} - -#quest-dialog li.link { - text-decoration: underline; -} - -#quest-dialog li::before { - content: ""; - position: absolute; - inset: 0; - background-color: rgba(248, 248, 190, 0.3); - opacity: 0; - pointer-events: none; - z-index: 0; - box-sizing: border-box; -} - -#quest-dialog li:hover::before { - opacity: 0.3 !important; -} - -#quest-dialog button[data-id="quest-select"] { - position: absolute; - right: 17px; - top: 16px; -} - -#quest-dialog button[data-id="cancel"], -#quest-dialog button[data-id="back"] { - position: absolute; - right: 104px; - bottom: 10px; -} - -#quest-dialog button[data-id="next"], -#quest-dialog button[data-id="ok"] { - position: absolute; - right: 10px; - bottom: 10px; -} diff --git a/src/ui/quest-dialog/quest-dialog.ts b/src/ui/quest-dialog/quest-dialog.ts deleted file mode 100644 index 6c19b030..00000000 --- a/src/ui/quest-dialog/quest-dialog.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { - type DialogEntry, - DialogEntryType, - type DialogQuestEntry, -} from 'eolib'; -import mitt from 'mitt'; -import type { Client } from '@/client'; -import { playSfxById, SfxId } from '@/sfx'; -import { Base } from '@/ui/base-ui'; - -import './quest-dialog.css'; - -type Events = { - reply: { questId: number; dialogId: number; action: number | null }; - cancel: undefined; -}; - -export class QuestDialog extends Base { - protected container = document.getElementById('quest-dialog')!; - private emitter = mitt(); - private cover: HTMLDivElement = document.querySelector('#cover')!; - private dialogs = document.getElementById('dialogs'); - private txtTitle: HTMLSpanElement = this.container!.querySelector('.title')!; - private btnQuestSelect: HTMLButtonElement = this.container!.querySelector( - 'button[data-id="quest-select"]', - )!; - private entries: HTMLUListElement = - this.container!.querySelector('.entries')!; - private btnCancel: HTMLButtonElement = this.container!.querySelector( - 'button[data-id="cancel"]', - )!; - private btnBack: HTMLButtonElement = this.container!.querySelector( - 'button[data-id="back"]', - )!; - private btnNext: HTMLButtonElement = this.container!.querySelector( - 'button[data-id="next"]', - )!; - private btnOk: HTMLButtonElement = this.container!.querySelector( - 'button[data-id="ok"]', - )!; - - private questId = 0; - private dialogId = 0; - private name = ''; - private title = ''; - private quests: DialogQuestEntry[] = []; - private dialog: DialogEntry[] = []; - private dialogIndex = 0; - private state: 'dialog' | 'quest-picker' = 'dialog'; - - on( - event: Event, - handler: (data: Events[Event]) => void, - ) { - this.emitter.on(event, handler); - } - - setData( - questId: number, - dialogId: number, - name: string, - quests: DialogQuestEntry[], - dialog: DialogEntry[], - ) { - this.questId = questId; - this.dialogId = dialogId; - this.name = name; - this.quests = quests; - this.dialog = dialog; - this.dialogIndex = 0; - this.updateQuestTitle(); - - this.btnQuestSelect.classList.add('hidden'); - if (this.quests.length > 1) { - this.btnQuestSelect.classList.remove('hidden'); - } - - this.render(); - } - - private updateQuestTitle() { - const quest = this.quests.find((q) => q.questId === this.questId); - if (!quest) { - this.title = 'Unknown'; - return; - } - - this.title = `${this.name} - ${quest.questName}`; - } - - private render() { - this.entries.innerHTML = ''; - this.btnBack.classList.add('hidden'); - this.btnCancel.classList.add('hidden'); - this.btnOk.classList.add('hidden'); - this.btnNext.classList.add('hidden'); - - if (this.state === 'dialog') { - this.renderDialog(); - } else { - this.renderQuestPicker(); - } - } - - private renderDialog() { - this.txtTitle.innerText = this.title; - - const entry = this.dialog[this.dialogIndex]; - if (!entry) { - return; - } - - const li = document.createElement('li'); - li.innerText = entry.line; - this.entries.appendChild(li); - - let i = 1; - while (true) { - const nextEntry = this.dialog[this.dialogIndex + i]; - if (!nextEntry || nextEntry.entryType === DialogEntryType.Text) { - break; - } - - if (i === 1) { - const blank = document.createElement('li'); - blank.innerText = '\xa0'; - this.entries.appendChild(blank); - } - - const link = document.createElement('li'); - link.classList.add('link'); - link.innerText = nextEntry.line; - const data = nextEntry.entryTypeData as DialogEntry.EntryTypeDataLink; - link.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.hide(); - this.cover.classList.add('hidden'); - this.emitter.emit('reply', { - questId: this.questId, - dialogId: this.dialogId, - action: data.linkId, - }); - }); - this.entries.appendChild(link); - - i++; - } - - if (this.dialogIndex === 0) { - this.btnCancel.classList.remove('hidden'); - } else { - this.btnBack.classList.remove('hidden'); - } - - const textDialogsCount = this.dialog.filter( - (d) => d.entryType === DialogEntryType.Text, - ).length; - if (this.dialogIndex === textDialogsCount - 1) { - this.btnOk.classList.remove('hidden'); - } else { - this.btnNext.classList.remove('hidden'); - } - } - - private renderQuestPicker() { - this.txtTitle.innerText = 'Select a quest'; - this.btnCancel.classList.remove('hidden'); - } - - show(): void { - this.cover.classList.remove('hidden'); - this.container!.classList.remove('hidden'); - this.dialogs!.classList.remove('hidden'); - this.client.typing = true; - } - - hide(): void { - this.cover.classList.add('hidden'); - this.container!.classList.add('hidden'); - - if (!document.querySelector('#dialogs > div:not(.hidden)')) { - this.dialogs!.classList.add('hidden'); - this.client.typing = false; - } - } - - private client: Client; - - constructor(client: Client) { - super(); - this.client = client; - - this.btnCancel.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.hide(); - this.cover.classList.add('hidden'); - this.emitter.emit('cancel', undefined); - }); - - this.btnBack.addEventListener('click', () => { - playSfxById(SfxId.TextBoxFocus); - this.dialogIndex = Math.max(this.dialogIndex - 1, 0); - this.render(); - }); - - this.btnNext.addEventListener('click', () => { - playSfxById(SfxId.TextBoxFocus); - this.dialogIndex = Math.min(this.dialogIndex + 1, this.dialog.length - 1); - this.render(); - }); - - this.btnOk.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.hide(); - this.cover.classList.add('hidden'); - this.emitter.emit('reply', { - questId: this.questId, - dialogId: this.dialogIndex, - action: null, - }); - }); - } -} diff --git a/src/ui/shop-dialog/index.ts b/src/ui/shop-dialog/index.ts deleted file mode 100644 index b9b4f2a3..00000000 --- a/src/ui/shop-dialog/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './shop-dialog'; diff --git a/src/ui/shop-dialog/shop-dialog.css b/src/ui/shop-dialog/shop-dialog.css deleted file mode 100644 index e69de29b..00000000 diff --git a/src/ui/shop-dialog/shop-dialog.ts b/src/ui/shop-dialog/shop-dialog.ts deleted file mode 100644 index 65175161..00000000 --- a/src/ui/shop-dialog/shop-dialog.ts +++ /dev/null @@ -1,330 +0,0 @@ -import { - type CharItem, - Gender, - ItemType, - type ShopCraftItem, - type ShopTradeItem, -} from 'eolib'; -import mitt from 'mitt'; -import type { Client } from '@/client'; -import { EOResourceID } from '@/edf'; -import { playSfxById, SfxId } from '@/sfx'; -import { Base } from '@/ui/base-ui'; -import { createIconMenuItem, createItemMenuItem } from '@/ui/utils'; - -import './shop-dialog.css'; -import { DialogIcon } from '@/ui/ui-types'; - -enum State { - Initial = 0, - Buy = 1, - Sell = 2, - Craft = 3, -} - -type Events = { - buyItem: { id: number; name: string; price: number; max: number }; - sellItem: { id: number; name: string; price: number }; - craftItem: { - id: number; - name: string; - ingredients: CharItem[]; - }; -}; - -export class ShopDialog extends Base { - private client: Client; - private emitter = mitt(); - protected container = document.getElementById('shop')!; - private dialogs = document.getElementById('dialogs'); - private cover = document.querySelector('#cover'); - private btnCancel = this.container!.querySelector( - 'button[data-id="cancel"]', - ); - private btnBack = this.container!.querySelector( - 'button[data-id="back"]', - ); - private txtName = - this.container!.querySelector('.shop-name'); - private itemList = - this.container!.querySelector('.item-list'); - private scrollHandle = - this.container!.querySelector('.scroll-handle'); - private name = ''; - private craftItems: ShopCraftItem[] = []; - private tradeItems: ShopTradeItem[] = []; - private state = State.Initial; - - constructor(client: Client) { - super(); - this.client = client; - - this.btnCancel!.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.hide(); - }); - - this.btnBack!.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.changeState(State.Initial); - }); - - this.itemList!.addEventListener('scroll', () => { - this.setScrollThumbPosition(); - }); - - this.client.on('itemBought', () => { - this.render(); - }); - - this.client.on('itemSold', () => { - this.render(); - }); - - this.scrollHandle!.addEventListener('pointerdown', () => { - const onPointerMove = (e: PointerEvent) => { - const rect = this.itemList!.getBoundingClientRect(); - const min = 30; - const max = 212; - const clampedY = Math.min( - Math.max(e.clientY, rect.top + min), - rect.top + max, - ); - const scrollPercent = (clampedY - rect.top - min) / (max - min); - const scrollHeight = this.itemList!.scrollHeight; - const clientHeight = this.itemList!.clientHeight; - this.itemList!.scrollTop = - scrollPercent * (scrollHeight - clientHeight); - }; - - const onPointerUp = () => { - document.removeEventListener('pointermove', onPointerMove); - document.removeEventListener('pointerup', onPointerUp); - }; - - document.addEventListener('pointermove', onPointerMove); - document.addEventListener('pointerup', onPointerUp); - }); - } - - on( - event: Event, - handler: (data: Events[Event]) => void, - ) { - this.emitter.on(event, handler); - } - - setScrollThumbPosition() { - const min = 60; - const max = 212; - const scrollTop = this.itemList!.scrollTop; - const scrollHeight = this.itemList!.scrollHeight; - const clientHeight = this.itemList!.clientHeight; - const scrollPercent = scrollTop / (scrollHeight - clientHeight); - const clampedPercent = Math.min(Math.max(scrollPercent, 0), 1); - const top = min + (max - min) * clampedPercent || min; - this.scrollHandle!.style.top = `${top}px`; - } - - setData( - name: string, - craftItems: ShopCraftItem[], - tradeItems: ShopTradeItem[], - ) { - this.name = name; - this.craftItems = craftItems; - this.tradeItems = tradeItems; - this.state = State.Initial; - this.render(); - } - - show() { - this.cover!.classList.remove('hidden'); - this.container!.classList.remove('hidden'); - this.dialogs!.classList.remove('hidden'); - this.client.typing = true; - this.setScrollThumbPosition(); - } - - hide() { - this.cover!.classList.add('hidden'); - this.container!.classList.add('hidden'); - - if (!document.querySelector('#dialogs > div:not(.hidden)')) { - this.dialogs!.classList.add('hidden'); - this.client.typing = false; - } - } - - private render() { - this.txtName!.innerText = this.name; - this.btnBack!.classList.add('hidden'); - - switch (this.state) { - case State.Initial: - this.renderInitial(); - return; - case State.Buy: - this.renderBuy(); - return; - case State.Sell: - this.renderSell(); - return; - case State.Craft: - this.renderCraft(); - return; - } - } - - private changeState(state: State) { - this.state = state; - this.render(); - } - - private renderInitial() { - this.itemList!.innerHTML = ''; - - const buys = this.tradeItems.filter((i) => i.buyPrice > 0); - if (buys.length) { - const item = createIconMenuItem( - DialogIcon.Buy, - this.client.getResourceString(EOResourceID.DIALOG_SHOP_BUY_ITEMS), - `${buys.length} ${this.client.getResourceString(EOResourceID.DIALOG_SHOP_ITEMS_IN_STORE)}`, - ); - const click = () => { - this.changeState(State.Buy); - }; - item.addEventListener('click', click); - item.addEventListener('contextmenu', click); - this.itemList!.appendChild(item); - } - - const sells = this.tradeItems.filter( - (i) => - i.sellPrice > 0 && this.client.items.some((i2) => i2.id === i.itemId), - ); - if (sells.length) { - const item = createIconMenuItem( - DialogIcon.Sell, - this.client.getResourceString(EOResourceID.DIALOG_SHOP_SELL_ITEMS), - `${sells.length} ${this.client.getResourceString(EOResourceID.DIALOG_SHOP_ITEMS_ACCEPTED)}`, - ); - const click = () => { - this.changeState(State.Sell); - }; - item.addEventListener('click', click); - item.addEventListener('contextmenu', click); - this.itemList!.appendChild(item); - } - - if (this.craftItems.length) { - const item = createIconMenuItem( - DialogIcon.Craft, - this.client.getResourceString(EOResourceID.DIALOG_SHOP_CRAFT_ITEMS), - `${this.craftItems.length} ${this.client.getResourceString(EOResourceID.DIALOG_SHOP_ITEMS_ACCEPTED)}`, - ); - const click = () => { - this.changeState(State.Craft); - }; - item.addEventListener('click', click); - item.addEventListener('contextmenu', click); - this.itemList!.appendChild(item); - } - } - - renderBuy() { - this.itemList!.innerHTML = ''; - this.btnBack!.classList.remove('hidden'); - const buys = this.tradeItems.filter((i) => i.buyPrice > 0); - for (const buy of buys) { - const record = this.client.getEifRecordById(buy.itemId); - if (!record) { - continue; - } - - const item = createItemMenuItem( - buy.itemId, - record, - record.name, - `${this.client.getResourceString(EOResourceID.DIALOG_SHOP_PRICE)}: ${buy.buyPrice} ${record.type === ItemType.Armor ? `(${record.spec2 === Gender.Female ? this.client.getResourceString(EOResourceID.FEMALE) : this.client.getResourceString(EOResourceID.MALE)})` : ''}`, - ); - const click = () => { - this.emitter.emit('buyItem', { - id: buy.itemId, - name: record.name, - price: buy.buyPrice, - max: buy.maxBuyAmount, - }); - }; - item.addEventListener('click', click); - item.addEventListener('contextmenu', click); - this.itemList!.appendChild(item); - } - } - - renderSell() { - this.itemList!.innerHTML = ''; - this.btnBack!.classList.remove('hidden'); - const sells = this.tradeItems.filter( - (i) => - i.sellPrice > 0 && this.client.items.some((i2) => i2.id === i.itemId), - ); - - if (!sells.length) { - this.changeState(State.Initial); - return; - } - - for (const sell of sells) { - const record = this.client.getEifRecordById(sell.itemId); - if (!record) { - continue; - } - - const item = createItemMenuItem( - sell.itemId, - record, - record.name, - `${this.client.getResourceString(EOResourceID.DIALOG_SHOP_PRICE)}: ${sell.sellPrice} ${record.type === ItemType.Armor ? `(${record.spec2 === Gender.Female ? this.client.getResourceString(EOResourceID.FEMALE) : this.client.getResourceString(EOResourceID.MALE)})` : ''}`, - ); - const click = () => { - this.emitter.emit('sellItem', { - id: sell.itemId, - name: record.name, - price: sell.sellPrice, - }); - }; - item.addEventListener('click', click); - item.addEventListener('contextmenu', click); - this.itemList!.appendChild(item); - } - } - - renderCraft() { - this.itemList!.innerHTML = ''; - this.btnBack!.classList.remove('hidden'); - for (const craft of this.craftItems) { - const record = this.client.getEifRecordById(craft.itemId); - if (!record) { - continue; - } - - const item = createItemMenuItem( - craft.itemId, - record, - record.name, - `${this.client.getResourceString(EOResourceID.DIALOG_SHOP_CRAFT_INGREDIENTS)}: ${craft.ingredients.length} ${record.type === ItemType.Armor ? `(${record.spec2 === Gender.Female ? this.client.getResourceString(EOResourceID.FEMALE) : this.client.getResourceString(EOResourceID.MALE)})` : ''}`, - ); - const click = () => { - this.emitter.emit('craftItem', { - id: craft.itemId, - name: record.name, - ingredients: craft.ingredients, - }); - }; - item.addEventListener('click', click); - item.addEventListener('contextmenu', click); - this.itemList!.appendChild(item); - } - } -} diff --git a/src/ui/skill-master-dialog/index.ts b/src/ui/skill-master-dialog/index.ts deleted file mode 100644 index 2a3cf122..00000000 --- a/src/ui/skill-master-dialog/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './skill-master-dialog'; diff --git a/src/ui/skill-master-dialog/skill-master-dialog.css b/src/ui/skill-master-dialog/skill-master-dialog.css deleted file mode 100644 index 607ad885..00000000 --- a/src/ui/skill-master-dialog/skill-master-dialog.css +++ /dev/null @@ -1,56 +0,0 @@ -#skill-master { - position: relative; - user-select: none; - box-sizing: border-box; - width: 284px; - height: 290px; - background-color: #000; - background-image: url("/gfx/gfx002/152.png"); -} - -#skill-master .name { - position: absolute; - color: #fff; - font-size: 12px; - left: 24px; - top: 18px; -} - -#skill-master .buttons { - position: absolute; - bottom: 10px; - left: 10px; - width: 260px; - display: flex; - gap: 3px; - justify-content: center; -} - -#skill-master .scroll-handle { - position: absolute; - right: 16px; - width: 16px; - height: 15px; - background: url("/gfx/gfx002/129.png") 0 -75px; - touch-action: none; - user-select: none; -} - -#skill-master .item-list { - position: absolute; - top: 50px; - left: 22px; - width: 232px; - height: 193px; - display: flex; - flex-direction: column; - gap: 2px; - overflow-y: scroll; - overflow-x: hidden; - -ms-overflow-style: none; - scrollbar-width: none; -} - -#skill-master .item-list::-webkit-scrollbar { - display: none; -} diff --git a/src/ui/skill-master-dialog/skill-master-dialog.ts b/src/ui/skill-master-dialog/skill-master-dialog.ts deleted file mode 100644 index e1f43e96..00000000 --- a/src/ui/skill-master-dialog/skill-master-dialog.ts +++ /dev/null @@ -1,550 +0,0 @@ -import type { SkillLearn } from 'eolib'; -import mitt from 'mitt'; -import type { Client } from '@/client'; -import { DialogResourceID, EOResourceID } from '@/edf'; -import { playSfxById, SfxId } from '@/sfx'; -import { Base } from '@/ui/base-ui'; -import { - createIconMenuItem, - createSkillMenuItem, - createTextMenuItem, -} from '@/ui/utils'; - -import './skill-master-dialog.css'; -import { DialogIcon } from '@/ui/ui-types'; - -enum State { - Initial = 0, - Learn = 1, - Forget = 2, - ForgetAll = 3, - Requirements = 4, -} - -type Events = { - learnSkill: number; - forgetSkill: number; - forgetAllSkills: undefined; -}; - -export class SkillMasterDialog extends Base { - private client: Client; - private emitter = mitt(); - protected container = document.getElementById('skill-master')!; - private dialogs = document.getElementById('dialogs'); - private cover = document.querySelector('#cover'); - private btnCancel = this.container!.querySelector( - 'button[data-id="cancel"]', - ); - private btnBack = this.container!.querySelector( - 'button[data-id="back"]', - ); - private txtName = this.container!.querySelector('.name'); - private itemList = - this.container!.querySelector('.item-list'); - private scrollHandle = - this.container!.querySelector('.scroll-handle'); - private name = ''; - private skills: SkillLearn[] = []; - private state = [State.Initial]; - private skillId = 0; - private open = false; - - constructor(client: Client) { - super(); - this.client = client; - - this.btnCancel!.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.hide(); - }); - - this.btnBack!.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.state.pop(); - this.render(); - }); - - this.itemList!.addEventListener('scroll', () => { - this.setScrollThumbPosition(); - }); - - this.client.on('itemBought', () => { - this.render(); - }); - - this.client.on('itemSold', () => { - this.render(); - }); - - this.scrollHandle!.addEventListener('pointerdown', () => { - const onPointerMove = (e: PointerEvent) => { - const rect = this.itemList!.getBoundingClientRect(); - const min = 30; - const max = 212; - const clampedY = Math.min( - Math.max(e.clientY, rect.top + min), - rect.top + max, - ); - const scrollPercent = (clampedY - rect.top - min) / (max - min); - const scrollHeight = this.itemList!.scrollHeight; - const clientHeight = this.itemList!.clientHeight; - this.itemList!.scrollTop = - scrollPercent * (scrollHeight - clientHeight); - }; - - const onPointerUp = () => { - document.removeEventListener('pointermove', onPointerMove); - document.removeEventListener('pointerup', onPointerUp); - }; - - document.addEventListener('pointermove', onPointerMove); - document.addEventListener('pointerup', onPointerUp); - }); - } - - on( - event: Event, - handler: (data: Events[Event]) => void, - ) { - this.emitter.on(event, handler); - } - - setScrollThumbPosition() { - const min = 60; - const max = 212; - const scrollTop = this.itemList!.scrollTop; - const scrollHeight = this.itemList!.scrollHeight; - const clientHeight = this.itemList!.clientHeight; - const scrollPercent = scrollTop / (scrollHeight - clientHeight); - const clampedPercent = Math.min(Math.max(scrollPercent, 0), 1); - const top = min + (max - min) * clampedPercent || min; - this.scrollHandle!.style.top = `${top}px`; - } - - setData(name: string, skills: SkillLearn[]) { - this.name = name; - this.skills = skills; - this.reset(); - } - - private reset() { - this.skillId = 0; - this.state = [State.Initial]; - this.render(); - } - - refresh() { - if (this.open) { - this.render(); - } - } - - show() { - this.cover!.classList.remove('hidden'); - this.container!.classList.remove('hidden'); - this.dialogs!.classList.remove('hidden'); - this.client.typing = true; - this.setScrollThumbPosition(); - this.open = true; - } - - hide() { - this.cover!.classList.add('hidden'); - this.container!.classList.add('hidden'); - - if (!document.querySelector('#dialogs > div:not(.hidden)')) { - this.dialogs!.classList.add('hidden'); - this.client.typing = false; - } - this.open = false; - } - - private render() { - this.txtName!.innerText = this.name; - this.btnBack!.classList.add('hidden'); - - switch (this.state[this.state.length - 1]) { - case State.Initial: - this.renderInitial(); - return; - case State.Learn: - this.renderLearn(); - return; - case State.Forget: - this.renderForget(); - return; - case State.ForgetAll: - this.renderForgetAll(); - return; - case State.Requirements: - this.renderRequirements(); - return; - } - } - - private changeState(state: State) { - this.state.push(state); - this.render(); - } - - private renderInitial() { - this.itemList!.innerHTML = ''; - - const learnCount = this.skills.filter((s) => - this.client.spells.every((p) => p.id !== s.id), - ).length; - const forgetCount = this.client.spells.length; - - const learnItem = createIconMenuItem( - DialogIcon.Learn, - this.client.getResourceString(EOResourceID.SKILLMASTER_WORD_LEARN), - `${learnCount}${this.client.getResourceString(EOResourceID.SKILLMASTER_ITEMS_TO_LEARN)}`, - ); - const clickLearn = () => { - if (!learnCount) { - const strings = this.client.getDialogStrings( - DialogResourceID.SKILL_NOTHING_MORE_TO_LEARN, - ); - this.client.showError(strings[1], strings[0]); - return; - } - this.changeState(State.Learn); - }; - learnItem.addEventListener('click', clickLearn); - learnItem.addEventListener('contextmenu', clickLearn); - this.itemList!.appendChild(learnItem); - - if (forgetCount) { - const forgetItem = createIconMenuItem( - DialogIcon.Forget, - this.client.getResourceString(EOResourceID.SKILLMASTER_WORD_FORGET), - `${forgetCount}${this.client.getResourceString(EOResourceID.SKILLMASTER_ITEMS_LEARNED)}`, - ); - const clickForget = () => { - if (!forgetCount) { - return; - } - this.changeState(State.Forget); - }; - forgetItem.addEventListener('click', clickForget); - forgetItem.addEventListener('contextmenu', clickForget); - this.itemList!.appendChild(forgetItem); - } - - const forgetAllItem = createIconMenuItem( - DialogIcon.Forget, - this.client.getResourceString(EOResourceID.SKILLMASTER_FORGET_ALL), - this.client.getResourceString( - EOResourceID.SKILLMASTER_RESET_YOUR_CHARACTER, - ), - ); - const clickForgetAll = () => { - this.changeState(State.ForgetAll); - }; - forgetAllItem.addEventListener('click', clickForgetAll); - forgetAllItem.addEventListener('contextmenu', clickForgetAll); - this.itemList!.appendChild(forgetAllItem); - } - - renderLearn() { - this.itemList!.innerHTML = ''; - let skillCount = 0; - for (const skill of this.skills) { - if (this.client.spells.some((s) => s.id === skill.id)) { - continue; - } - - skillCount++; - - const record = this.client.getEsfRecordById(skill.id); - const learn = this.skills.find((s) => s.id === skill.id); - if (!record || !learn) { - continue; - } - - const item = createSkillMenuItem( - record, - record.name, - this.client.getResourceString( - EOResourceID.SKILLMASTER_WORD_REQUIREMENTS, - ), - () => { - this.skillId = skill.id; - this.changeState(State.Requirements); - }, - ); - const click = () => { - if ( - learn.classRequirement && - this.client.classId !== learn.classRequirement - ) { - const strings = this.client.getDialogStrings( - DialogResourceID.SKILL_LEARN_WRONG_CLASS, - ); - this.client.showError( - `${strings[1]} ${this.getClassName(learn.classRequirement)}`, - strings[0], - ); - return; - } - - let skillRequirementsMet = true; - for (const req of learn.skillRequirements) { - if (req && this.client.spells.every((s) => s.id !== req)) { - skillRequirementsMet = false; - break; - } - } - - const gold = this.client.items.find((i) => i.id === 1); - - const requirementsMet = - skillRequirementsMet && - this.client.level >= learn.levelRequirement && - gold && - gold.amount >= learn.cost && - this.client.baseStats.str >= learn.statRequirements.str && - this.client.baseStats.intl >= learn.statRequirements.intl && - this.client.baseStats.wis >= learn.statRequirements.wis && - this.client.baseStats.agi >= learn.statRequirements.agi && - this.client.baseStats.con >= learn.statRequirements.con && - this.client.baseStats.cha >= learn.statRequirements.cha; - - if (!requirementsMet) { - const strings = this.client.getDialogStrings( - DialogResourceID.SKILL_LEARN_REQS_NOT_MET, - ); - this.client.showError(strings[1], strings[0]); - return; - } - - const strings = this.client.getDialogStrings( - DialogResourceID.SKILL_LEARN_CONFIRMATION, - ); - this.client.showConfirmation( - `${strings[1]} ${record.name}`, - strings[0], - () => { - this.client.statSkillController.learnSkill(skill.id); - }, - ); - }; - - item.addEventListener('click', click); - item.addEventListener('contextmenu', click); - this.itemList!.appendChild(item); - } - - if (!skillCount) { - this.reset(); - return; - } - - this.btnBack!.classList.remove('hidden'); - } - - renderForget() { - this.itemList!.innerHTML = ''; - - if (!this.client.spells.length) { - this.reset(); - return; - } - - for (const skill of this.client.spells) { - const record = this.client.getEsfRecordById(skill.id); - if (!record) { - continue; - } - - const item = createSkillMenuItem(record, record.name, ''); - - const click = () => { - const strings = this.client.getDialogStrings( - DialogResourceID.SKILL_PROMPT_TO_FORGET, - ); - this.client.showConfirmation(strings[1], strings[0], () => { - this.client.statSkillController.forgetSkill(skill.id); - }); - }; - - item.addEventListener('click', click); - item.addEventListener('contextmenu', click); - this.itemList!.appendChild(item); - } - - this.btnBack!.classList.remove('hidden'); - } - - renderForgetAll() { - this.itemList!.innerHTML = ''; - - this.itemList!.appendChild( - createTextMenuItem( - this.client.getResourceString(EOResourceID.SKILLMASTER_FORGET_ALL), - ), - ); - - this.itemList!.appendChild(createTextMenuItem()); - this.itemList!.append( - createTextMenuItem( - this.client.getResourceString( - EOResourceID.SKILLMASTER_FORGET_ALL_MSG_1, - ), - ), - ); - this.itemList!.appendChild(createTextMenuItem()); - this.itemList!.append( - createTextMenuItem( - this.client.getResourceString( - EOResourceID.SKILLMASTER_FORGET_ALL_MSG_2, - ), - ), - ); - this.itemList!.appendChild(createTextMenuItem()); - this.itemList!.append( - createTextMenuItem( - this.client.getResourceString( - EOResourceID.SKILLMASTER_FORGET_ALL_MSG_3, - ), - ), - ); - this.itemList!.appendChild(createTextMenuItem()); - - this.itemList!.appendChild( - createTextMenuItem( - this.client.getResourceString( - EOResourceID.SKILLMASTER_CLICK_HERE_TO_FORGET_ALL, - ), - () => { - const lines = this.client.getDialogStrings( - DialogResourceID.SKILL_RESET_CHARACTER_CONFIRMATION, - ); - this.client.showConfirmation(lines![1], lines![0], () => { - this.client.statSkillController.resetCharacter(); - }); - }, - ), - ); - - this.btnBack!.classList.remove('hidden'); - } - - renderRequirements() { - this.itemList!.innerHTML = ''; - - if (!this.skillId) { - this.reset(); - return; - } - - const learn = this.skills.find((s) => s.id === this.skillId); - if (!learn) { - this.reset(); - return; - } - - const record = this.client.getEsfRecordById(this.skillId); - const goldRecord = this.client.getEifRecordById(1); - if (!record || !goldRecord) { - this.reset(); - return; - } - - const classRequirement = learn.classRequirement - ? `[${this.getClassName(learn.classRequirement)}]` - : ''; - this.itemList!.appendChild( - createTextMenuItem(`${record.name} ${classRequirement}`), - ); - - this.itemList!.appendChild(createTextMenuItem()); - - const skillRequirements = learn.skillRequirements.filter((id) => !!id); - if (skillRequirements.length) { - const wordSkill = this.client.getResourceString( - EOResourceID.SKILLMASTER_WORD_SKILL, - ); - for (const req of skillRequirements) { - const reqRecord = this.client.getEsfRecordById(req); - if (!reqRecord) { - continue; - } - - this.itemList!.appendChild( - createTextMenuItem(`${wordSkill}: ${reqRecord.name}`), - ); - } - - this.itemList!.appendChild(createTextMenuItem()); - } - - if (learn.statRequirements.str) { - this.itemList!.appendChild( - createTextMenuItem( - `${learn.statRequirements.str} ${this.client.getResourceString(EOResourceID.SKILLMASTER_WORD_STRENGTH)}`, - ), - ); - } - - if (learn.statRequirements.intl) { - this.itemList!.appendChild( - createTextMenuItem( - `${learn.statRequirements.intl} ${this.client.getResourceString(EOResourceID.SKILLMASTER_WORD_INTELLIGENCE)}`, - ), - ); - } - - if (learn.statRequirements.wis) { - this.itemList!.appendChild( - createTextMenuItem( - `${learn.statRequirements.wis} ${this.client.getResourceString(EOResourceID.SKILLMASTER_WORD_WISDOM)}`, - ), - ); - } - - if (learn.statRequirements.agi) { - this.itemList!.appendChild( - createTextMenuItem( - `${learn.statRequirements.agi} ${this.client.getResourceString(EOResourceID.SKILLMASTER_WORD_AGILITY)}`, - ), - ); - } - - if (learn.statRequirements.con) { - this.itemList!.appendChild( - createTextMenuItem( - `${learn.statRequirements.con} ${this.client.getResourceString(EOResourceID.SKILLMASTER_WORD_CONSTITUTION)}`, - ), - ); - } - - if (learn.statRequirements.cha) { - this.itemList!.appendChild( - createTextMenuItem( - `${learn.statRequirements.cha} ${this.client.getResourceString(EOResourceID.SKILLMASTER_WORD_CHARISMA)}`, - ), - ); - } - - this.itemList!.appendChild(createTextMenuItem()); - - this.itemList!.appendChild( - createTextMenuItem( - `${learn.levelRequirement} ${this.client.getResourceString(EOResourceID.SKILLMASTER_WORD_LEVEL)}`, - ), - ); - - this.itemList!.appendChild( - createTextMenuItem(`${learn.cost} ${goldRecord.name}`), - ); - - this.btnBack!.classList.remove('hidden'); - } - - private getClassName(classId: number): string { - const classRecord = this.client.getEcfRecordById(classId); - return classRecord ? classRecord.name : ''; - } -} diff --git a/src/ui/small-alert-large-header/index.ts b/src/ui/small-alert-large-header/index.ts deleted file mode 100644 index 62064eff..00000000 --- a/src/ui/small-alert-large-header/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './small-alert-large-header'; diff --git a/src/ui/small-alert-large-header/small-alert-large-header.css b/src/ui/small-alert-large-header/small-alert-large-header.css deleted file mode 100644 index b8d3d424..00000000 --- a/src/ui/small-alert-large-header/small-alert-large-header.css +++ /dev/null @@ -1,30 +0,0 @@ -#small-alert { - position: absolute; - background: url("/gfx/gfx001/118.png"); - background-color: #000; - width: 290px; - height: 157px; - user-select: none; - z-index: 1020; -} - -#small-alert .title { - position: absolute; - left: 58px; - top: 22px; - width: 210px; -} - -#small-alert .message { - position: absolute; - left: 20px; - top: 56px; - width: 252px; - height: 55px; -} - -#small-alert button { - position: absolute; - top: 113px; - right: 18px; -} diff --git a/src/ui/small-alert-large-header/small-alert-large-header.ts b/src/ui/small-alert-large-header/small-alert-large-header.ts deleted file mode 100644 index d4677f13..00000000 --- a/src/ui/small-alert-large-header/small-alert-large-header.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { playSfxById, SfxId } from '@/sfx'; -import { Base } from '@/ui/base-ui'; - -import './small-alert-large-header.css'; - -export class SmallAlertLargeHeader extends Base { - protected container = document.getElementById('small-alert')!; - private cover = document.getElementById('cover'); - private title: HTMLSpanElement = this.container!.querySelector('.title')!; - private message: HTMLSpanElement = this.container!.querySelector('.message')!; - private btnCancel: HTMLButtonElement = this.container!.querySelector( - 'button[data-id="ok"]', - )!; - - show() { - this.cover!.classList.remove('hidden'); - this.container!.classList.remove('hidden'); - this.container!.style.left = - `${Math.floor(window.innerWidth / 2 - this.container!.clientWidth / 2)}px`; - this.container!.style.top = - `${Math.floor(window.innerHeight / 2 - this.container!.clientHeight / 2)}px`; - } - - constructor() { - super(); - this.btnCancel.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.hide(); - this.cover!.classList.add('hidden'); - }); - } - - setContent(message: string, title = 'Error') { - this.title.innerText = title; - this.message.innerText = message; - } -} diff --git a/src/ui/small-alert-small-header/index.ts b/src/ui/small-alert-small-header/index.ts deleted file mode 100644 index 87368c80..00000000 --- a/src/ui/small-alert-small-header/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './small-alert-small-header'; diff --git a/src/ui/small-alert-small-header/small-alert-small-header.css b/src/ui/small-alert-small-header/small-alert-small-header.css deleted file mode 100644 index 85ace0e7..00000000 --- a/src/ui/small-alert-small-header/small-alert-small-header.css +++ /dev/null @@ -1,30 +0,0 @@ -#small-alert-small-header { - position: absolute; - background: url("/gfx/gfx001/123.png"); - background-color: #000; - width: 290px; - height: 125px; - user-select: none; - z-index: 1020; -} - -#small-alert-small-header .title { - position: absolute; - left: 18px; - top: 15px; - width: 258px; -} - -#small-alert-small-header .message { - position: absolute; - left: 18px; - top: 40px; - width: 255px; - height: 40px; -} - -#small-alert-small-header button { - position: absolute; - top: 80px; - right: 18px; -} diff --git a/src/ui/small-alert-small-header/small-alert-small-header.ts b/src/ui/small-alert-small-header/small-alert-small-header.ts deleted file mode 100644 index f0037c98..00000000 --- a/src/ui/small-alert-small-header/small-alert-small-header.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { playSfxById, SfxId } from '@/sfx'; -import { Base } from '@/ui/base-ui'; - -import './small-alert-small-header.css'; - -export class SmallAlertSmallHeader extends Base { - protected container = document.getElementById('small-alert-small-header')!; - private cover = document.getElementById('cover'); - private title: HTMLSpanElement = this.container!.querySelector('.title')!; - private message: HTMLSpanElement = this.container!.querySelector('.message')!; - private btnCancel: HTMLButtonElement = this.container!.querySelector( - 'button[data-id="ok"]', - )!; - - show() { - this.cover!.classList.remove('hidden'); - this.container!.classList.remove('hidden'); - this.container!.style.left = - `${Math.floor(window.innerWidth / 2 - this.container!.clientWidth / 2)}px`; - this.container!.style.top = - `${Math.floor(window.innerHeight / 2 - this.container!.clientHeight / 2)}px`; - } - - constructor() { - super(); - this.btnCancel.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.hide(); - this.cover!.classList.add('hidden'); - }); - } - - setContent(message: string, title = 'Error') { - this.title.innerText = title; - this.message.innerText = message; - } -} diff --git a/src/ui/small-confirm/index.ts b/src/ui/small-confirm/index.ts deleted file mode 100644 index 2e3504cc..00000000 --- a/src/ui/small-confirm/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './small-confirm'; diff --git a/src/ui/small-confirm/small-confirm.css b/src/ui/small-confirm/small-confirm.css deleted file mode 100644 index a7413c17..00000000 --- a/src/ui/small-confirm/small-confirm.css +++ /dev/null @@ -1,32 +0,0 @@ -#small-confirm { - position: absolute; - background: url("/gfx/gfx001/123.png"); - background-color: #000; - width: 290px; - height: 125px; - user-select: none; - z-index: 10000; -} - -#small-confirm .title { - position: absolute; - left: 18px; - top: 15px; - width: 258px; -} - -#small-confirm .message { - position: absolute; - left: 18px; - top: 40px; - width: 255px; - height: 40px; -} - -#small-confirm .buttons { - position: absolute; - bottom: 14px; - right: 17px; - display: flex; - gap: 2px; -} diff --git a/src/ui/small-confirm/small-confirm.ts b/src/ui/small-confirm/small-confirm.ts deleted file mode 100644 index 26a30310..00000000 --- a/src/ui/small-confirm/small-confirm.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { playSfxById, SfxId } from '@/sfx'; -import { Base } from '@/ui/base-ui'; - -import './small-confirm.css'; - -export class SmallConfirm extends Base { - protected container = document.getElementById('small-confirm')!; - private cover = document.getElementById('cover'); - private title: HTMLSpanElement = this.container!.querySelector('.title')!; - private message: HTMLSpanElement = this.container!.querySelector('.message')!; - private btnOk: HTMLButtonElement = this.container!.querySelector( - 'button[data-id="ok"]', - )!; - private btnCancel: HTMLButtonElement = this.container!.querySelector( - 'button[data-id="cancel"]', - )!; - private callback: (() => undefined) | null = null; - private keepOpen = false; - - show() { - this.cover!.classList.remove('hidden'); - this.container!.classList.remove('hidden'); - this.container!.style.left = - `${Math.floor(window.innerWidth / 2 - this.container!.clientWidth / 2)}px`; - this.container!.style.top = - `${Math.floor(window.innerHeight / 2 - this.container!.clientHeight / 2)}px`; - } - - constructor() { - super(); - this.btnCancel.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.hide(); - this.cover!.classList.add('hidden'); - }); - - this.btnOk.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - if (!this.keepOpen) { - this.hide(); - this.cover!.classList.add('hidden'); - } - - if (this.callback) { - this.callback(); - } - }); - } - - setContent(message: string, title = 'Error') { - this.title.innerText = title; - this.message.innerText = message; - } - - setCallback(callback: () => undefined, keepOpen = false) { - this.callback = callback; - this.keepOpen = keepOpen; - } -} diff --git a/src/ui/spell-book/index.ts b/src/ui/spell-book/index.ts deleted file mode 100644 index 42869ce5..00000000 --- a/src/ui/spell-book/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './spell-book'; diff --git a/src/ui/spell-book/spell-book.css b/src/ui/spell-book/spell-book.css deleted file mode 100644 index c9159474..00000000 --- a/src/ui/spell-book/spell-book.css +++ /dev/null @@ -1,3 +0,0 @@ -#spell-book.dialog-md { - background-image: url("/gfx/gfx002/164.png"); -} diff --git a/src/ui/spell-book/spell-book.ts b/src/ui/spell-book/spell-book.ts deleted file mode 100644 index a2ab084e..00000000 --- a/src/ui/spell-book/spell-book.ts +++ /dev/null @@ -1,178 +0,0 @@ -import type { Client } from '@/client'; -import { playSfxById, SfxId } from '@/sfx'; -import { BaseDialogMd } from '@/ui/base-dialog-md'; -import { setSkillBackgroundFromGfx } from '@/ui/utils'; - -import './spell-book.css'; - -type Events = { - assignToSlot: { spellId: number; slotIndex: number }; -}; - -export class SpellBook extends BaseDialogMd { - protected container: HTMLDivElement = document.querySelector('#spell-book')!; - private spellGrid: HTMLDivElement = - this.container.querySelector('.spell-grid')!; - - private dragging: { - spellId: number; - el: HTMLElement; - pointerId: number; - ghost: HTMLElement; - offsetX: number; - offsetY: number; - } | null = null; - - constructor(client: Client) { - super(client, document.querySelector('#spell-book')!, 'Spell Book'); - } - - public render() { - this.spellGrid.innerHTML = ''; - - this.updateLabelText( - `Spell Book (${this.client.spells.length}) Points (${this.client.skillPoints})`, - ); - - for (const spell of this.client.spells) { - const record = this.client.getEsfRecordById(spell.id); - if (!record) continue; - - const spellElement = document.createElement('div'); - const icon = document.createElement('div'); - icon.classList.add('spell-icon'); - void setSkillBackgroundFromGfx(icon, record.iconId); - - icon.addEventListener('pointerdown', (e) => { - this.onPointerDown(e, icon, spell.id); - }); - - spellElement.appendChild(icon); - - const click = () => { - this.client.showConfirmation( - `Do you want to level up '${record.name}' to level ${spell.level + 1} for 1 skill point?`, - 'Spell training', - () => {}, - ); - }; - - const name = document.createElement('span'); - name.classList.add('spell-name'); - name.innerText = record.name; - name.addEventListener('click', click); - spellElement.appendChild(name); - - const level = document.createElement('span'); - level.classList.add('spell-level'); - level.innerText = `Lvl: ${spell.level}`; - level.addEventListener('click', click); - spellElement.appendChild(level); - - this.spellGrid.appendChild(spellElement); - } - } - - private onPointerDown(e: PointerEvent, el: HTMLDivElement, spellId: number) { - if (e.button !== 0 && e.pointerType !== 'touch') return; - - (e.target as Element).setPointerCapture(e.pointerId); - - const rect = el.getBoundingClientRect(); - const offsetX = e.clientX - rect.left; - const offsetY = e.clientY - rect.top; - - const ghost = el.cloneNode(true) as HTMLElement; - ghost.style.position = 'fixed'; - ghost.style.pointerEvents = 'none'; - ghost.style.margin = '0'; - ghost.style.inset = 'auto'; - ghost.style.left = '0'; - ghost.style.top = '0'; - ghost.style.width = `${rect.width}px`; - ghost.style.height = `${rect.height}px`; - ghost.style.backgroundPositionX = `-${rect.width}px`; - ghost.style.transform = `translate(${e.clientX - offsetX}px, ${e.clientY - offsetY}px)`; - ghost.style.opacity = '0.9'; - ghost.style.willChange = 'transform'; - ghost.style.zIndex = '9999'; - - document.body.appendChild(ghost); - - this.dragging = { - spellId, - el, - pointerId: e.pointerId, - ghost, - offsetX, - offsetY, - }; - - playSfxById(SfxId.InventoryPickup); - - window.addEventListener('pointermove', this.onPointerMove.bind(this), { - passive: false, - }); - window.addEventListener('pointerup', this.onPointerUp.bind(this), { - passive: false, - }); - window.addEventListener('pointercancel', this.onPointerCancel.bind(this), { - passive: false, - }); - } - - private onPointerMove(e: PointerEvent) { - if (!this.dragging || e.pointerId !== this.dragging.pointerId) return; - - // keep ghost under the finger/cursor - const { ghost, offsetX, offsetY } = this.dragging; - ghost.style.transform = `translate(${e.clientX - offsetX}px, ${e.clientY - offsetY}px)`; - - // prevent page scrolling while dragging on mobile - e.preventDefault(); - } - - private onPointerUp(e: PointerEvent) { - if (!this.dragging || e.pointerId !== this.dragging.pointerId) return; - - const { el, ghost, spellId } = this.dragging; - - const target = document.elementFromPoint(e.clientX, e.clientY); - - playSfxById(SfxId.InventoryPlace); - ghost.remove(); - el.style.display = 'flex'; - this.teardownDragListeners(); - this.dragging = null; - - if (!target) return; - - const slot = target.closest('.slot') as HTMLDivElement; - if (!slot) return; - - const slots = document.querySelectorAll('#hotbar .slot'); - const slotIndex = Array.from(slots).indexOf(slot); - if (slotIndex === -1) return; - - this.emitter.emit('assignToSlot', { - spellId, - slotIndex, - }); - } - - private onPointerCancel() { - if (!this.dragging) return; - - const { el, ghost } = this.dragging; - ghost.remove(); - el.style.opacity = '1'; - this.teardownDragListeners(); - this.dragging = null; - } - - private teardownDragListeners() { - window.removeEventListener('pointermove', this.onPointerMove); - window.removeEventListener('pointerup', this.onPointerUp); - window.removeEventListener('pointercancel', this.onPointerCancel); - } -} diff --git a/src/ui/stats/index.ts b/src/ui/stats/index.ts deleted file mode 100644 index c7e87e1f..00000000 --- a/src/ui/stats/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './stats'; diff --git a/src/ui/stats/stats.css b/src/ui/stats/stats.css deleted file mode 100644 index 6ca532db..00000000 --- a/src/ui/stats/stats.css +++ /dev/null @@ -1,197 +0,0 @@ -#stats { - position: relative; - width: 240px; - height: 233px; - background: #000; - color: #fff; - font-size: 12px; - background-position: -3px 0; - border-radius: 5px; - z-index: 9999; - user-select: none; -} - -#stats > div:first-child { - position: absolute; - top: 0; - width: 240px; - height: 120px; - background-image: url(/gfx/gfx002/134.png); -} - -#stats > div:nth-child(2) { - position: absolute; - width: 240px; - height: 115px; - top: 119px; - background-image: url(/gfx/gfx002/134.png); - background-position: -239px -4px; -} - -#stats button[data-id="cancel"] { - position: absolute; - bottom: -32px; - left: 76px; -} - -#stats .stat { - position: absolute; -} - -#stats .stat[data-id="str"] { - left: 52px; - top: 10px; - width: 75px; -} - -#stats .upgrade[data-target="str"] { - position: absolute; - left: 102px; - top: 7px; -} - -#stats .stat[data-id="int"] { - left: 52px; - top: 28px; - width: 75px; -} - -#stats .upgrade[data-target="int"] { - position: absolute; - left: 102px; - top: 25px; -} - -#stats .stat[data-id="wis"] { - left: 52px; - top: 46px; - width: 75px; -} - -#stats .upgrade[data-target="wis"] { - position: absolute; - left: 102px; - top: 43px; -} - -#stats .stat[data-id="agi"] { - left: 52px; - top: 64px; - width: 75px; -} - -#stats .upgrade[data-target="agi"] { - position: absolute; - left: 102px; - top: 61px; -} - -#stats .stat[data-id="con"] { - left: 52px; - top: 82px; - width: 75px; -} - -#stats .upgrade[data-target="con"] { - position: absolute; - left: 102px; - top: 79px; -} - -#stats .stat[data-id="cha"] { - left: 52px; - top: 100px; - width: 75px; -} - -#stats .upgrade[data-target="cha"] { - position: absolute; - left: 102px; - top: 97px; -} - -#stats .stat[data-id="hp"] { - left: 160px; - top: 10px; - width: 75px; -} - -#stats .stat[data-id="tp"] { - left: 160px; - top: 28px; - width: 75px; -} - -#stats .stat[data-id="dam"] { - left: 160px; - top: 46px; - width: 75px; -} - -#stats .stat[data-id="acc"] { - left: 160px; - top: 64px; - width: 75px; -} - -#stats .stat[data-id="arm"] { - left: 160px; - top: 82px; - width: 75px; -} - -#stats .stat[data-id="eva"] { - left: 160px; - top: 100px; - width: 75px; -} - -#stats .stat[data-id="name"] { - left: 44px; - top: 6px; -} - -#stats .stat[data-id="lvl"] { - left: 215px; - top: 6px; -} - -#stats .stat[data-id="guild"] { - left: 44px; - top: 24px; -} - -#stats .stat[data-id="weight"] { - left: 44px; - top: 43px; -} - -#stats .stat[data-id="stat-points"] { - left: 44px; - top: 61px; -} - -#stats .stat[data-id="skill-points"] { - left: 44px; - top: 79px; -} - -#stats .stat[data-id="gold"] { - left: 142px; - top: 43px; -} - -#stats .stat[data-id="exp"] { - left: 142px; - top: 61px; -} - -#stats .stat[data-id="tnl"] { - left: 142px; - top: 79px; -} - -#stats .stat[data-id="karma"] { - left: 142px; - top: 97px; -} diff --git a/src/ui/stats/stats.ts b/src/ui/stats/stats.ts deleted file mode 100644 index e8c4e6a6..00000000 --- a/src/ui/stats/stats.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { StatId } from 'eolib'; -import mitt from 'mitt'; -import type { Client } from '@/client'; -import { playSfxById, SfxId } from '@/sfx'; -import { Base } from '@/ui/base-ui'; -import { calculateTnl, capitalize } from '@/utils'; - -import './stats.css'; - -type Events = { - confirmTraining: undefined; -}; - -class StatItem { - private container: HTMLSpanElement; - - setValue(value: string) { - this.container.textContent = value; - } - - getValue(): string { - return this.container.textContent || ''; - } - - constructor(id: string) { - this.container = document.querySelector(`#stats span[data-id="${id}"]`)!; - } -} - -class StatUpgradeButton { - private container: HTMLButtonElement; - - constructor(target: string, callback: () => void) { - this.container = document.querySelector( - `#stats button[data-target="${target}"]`, - )!; - this.container.addEventListener('click', () => { - callback(); - }); - } - - show() { - this.container.classList.remove('hidden'); - } - - hide() { - this.container.classList.add('hidden'); - } -} - -export class Stats extends Base { - protected container: HTMLDivElement = document.querySelector('#stats')!; - private dialogs = document.getElementById('dialogs'); - private client: Client; - private statItems: { [key: string]: StatItem }; - private statButtons: { [key: string]: StatUpgradeButton }; - private open = false; - private confirmedTraining = false; - private emitter = mitt(); - - constructor(client: Client) { - super(); - this.client = client; - this.statItems = { - str: new StatItem('str'), - int: new StatItem('int'), - wis: new StatItem('wis'), - agi: new StatItem('agi'), - con: new StatItem('con'), - cha: new StatItem('cha'), - hp: new StatItem('hp'), - tp: new StatItem('tp'), - dam: new StatItem('dam'), - acc: new StatItem('acc'), - arm: new StatItem('arm'), - eva: new StatItem('eva'), - name: new StatItem('name'), - level: new StatItem('lvl'), - guild: new StatItem('guild'), - weight: new StatItem('weight'), - statPoints: new StatItem('stat-points'), - skillPoints: new StatItem('skill-points'), - gold: new StatItem('gold'), - exp: new StatItem('exp'), - tnl: new StatItem('tnl'), - karma: new StatItem('karma'), - }; - - this.statButtons = { - str: new StatUpgradeButton('str', () => { - this.upgradeStat(StatId.Str); - }), - int: new StatUpgradeButton('int', () => { - this.upgradeStat(StatId.Int); - }), - wis: new StatUpgradeButton('wis', () => { - this.upgradeStat(StatId.Wis); - }), - agi: new StatUpgradeButton('agi', () => { - this.upgradeStat(StatId.Agi); - }), - con: new StatUpgradeButton('con', () => { - this.upgradeStat(StatId.Con); - }), - cha: new StatUpgradeButton('cha', () => { - this.upgradeStat(StatId.Cha); - }), - }; - - const btnBack = this.container.querySelector( - 'button[data-id="cancel"]', - ); - if (btnBack) { - btnBack.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.hide(); - }); - } - } - - private upgradeStat(statId: StatId) { - if (!this.confirmedTraining) { - this.emitter.emit('confirmTraining'); - return; - } - - this.client.statSkillController.trainStat(statId); - } - - on( - event: Event, - handler: (data: Events[Event]) => void, - ) { - this.emitter.on(event, handler); - } - - setTrainingConfirmed() { - this.confirmedTraining = true; - } - - render() { - if (!this.open) return; - - if (this.statItems.level.getValue() !== this.client.level.toString()) { - this.confirmedTraining = false; - } - - const goldItem = this.client.items.find((i) => i.id === 1); - const goldAmount = goldItem ? goldItem.amount : 0; - - this.statItems.str.setValue(this.client.baseStats.str.toString()); - this.statItems.int.setValue(this.client.baseStats.intl.toString()); - this.statItems.wis.setValue(this.client.baseStats.wis.toString()); - this.statItems.agi.setValue(this.client.baseStats.agi.toString()); - this.statItems.con.setValue(this.client.baseStats.con.toString()); - this.statItems.cha.setValue(this.client.baseStats.cha.toString()); - this.statItems.hp.setValue(this.client.hp.toString()); - this.statItems.tp.setValue(this.client.tp.toString()); - this.statItems.dam.setValue( - `${this.client.secondaryStats.minDamage} - ${this.client.secondaryStats.maxDamage}`, - ); - this.statItems.acc.setValue(this.client.secondaryStats.accuracy.toString()); - this.statItems.arm.setValue(this.client.secondaryStats.armor.toString()); - this.statItems.eva.setValue(this.client.secondaryStats.evade.toString()); - this.statItems.name.setValue(capitalize(this.client.name)); - this.statItems.level.setValue(this.client.level.toString()); - this.statItems.guild.setValue(this.client.guildName || ''); - this.statItems.weight.setValue( - `${this.client.weight.current} / ${this.client.weight.max}`, - ); - this.statItems.statPoints.setValue(this.client.statPoints.toString()); - this.statItems.skillPoints.setValue(this.client.skillPoints.toString()); - this.statItems.gold.setValue(goldAmount.toString()); - this.statItems.exp.setValue(this.client.experience.toString()); - this.statItems.tnl.setValue( - calculateTnl(this.client.experience).toString(), - ); - this.statItems.karma.setValue(this.getKarmaString(this.client.karma)); - - if (this.client.statPoints > 0) { - for (const button of Object.values(this.statButtons)) { - button.show(); - } - } else { - for (const button of Object.values(this.statButtons)) { - button.hide(); - } - } - } - - private getKarmaString(value: number): string { - if (value >= 0) { - if (value <= 100) return 'Demonic'; - if (value <= 500) return 'Doomed'; - if (value <= 750) return 'Cursed'; - if (value <= 900) return 'Evil'; - if (value <= 1099) return 'Neutral'; - if (value <= 1249) return 'Good'; - if (value <= 1499) return 'Blessed'; - if (value <= 1899) return 'Saint'; - if (value <= 2000) return 'Pure'; - } - - return ''; - } - - show() { - this.open = true; - this.render(); - this.container.classList.remove('hidden'); - this.dialogs!.classList.remove('hidden'); - } - - hide() { - this.open = false; - this.container.classList.add('hidden'); - - if (!document.querySelector('#dialogs > div:not(.hidden)')) { - this.dialogs!.classList.add('hidden'); - this.client.typing = false; - } - } - - toggle() { - if (this.container.classList.contains('hidden')) { - this.show(); - } else { - this.hide(); - } - } -} diff --git a/src/ui/trade-dialog/index.ts b/src/ui/trade-dialog/index.ts deleted file mode 100644 index 14d9b002..00000000 --- a/src/ui/trade-dialog/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { TradeDialog } from './trade-dialog'; diff --git a/src/ui/trade-dialog/trade-dialog.css b/src/ui/trade-dialog/trade-dialog.css deleted file mode 100644 index 8acfc9a6..00000000 --- a/src/ui/trade-dialog/trade-dialog.css +++ /dev/null @@ -1,308 +0,0 @@ -/* Shared button styles for trade dialog and notification */ -.trade-btn { - padding: 4px 16px; - border: 1px solid rgba(212, 184, 150, 0.3); - border-radius: 4px; - background: rgba(212, 184, 150, 0.1); - color: #d4b896; - font-family: inherit; - font-size: 11px; - cursor: pointer; - transition: - background 0.15s, - border-color 0.15s; -} - -.trade-btn:hover { - background: rgba(212, 184, 150, 0.2); - border-color: rgba(212, 184, 150, 0.5); -} - -.trade-btn.primary { - background: rgba(46, 125, 50, 0.3); - border-color: rgba(76, 175, 80, 0.4); - color: #a5d6a7; -} - -.trade-btn.primary:hover { - background: rgba(46, 125, 50, 0.45); - border-color: rgba(76, 175, 80, 0.6); -} - -.trade-btn.primary.active { - background: rgba(46, 125, 50, 0.5); - border-color: rgba(76, 175, 80, 0.7); - box-shadow: 0 0 6px rgba(76, 175, 80, 0.3); -} - -/* Trade dialog */ -#trade-dialog { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 440px; - background: linear-gradient(145deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); - border: 1px solid rgba(212, 184, 150, 0.3); - border-radius: 8px; - box-shadow: - 0 8px 32px rgba(0, 0, 0, 0.6), - inset 0 1px 0 rgba(255, 255, 255, 0.05); - color: #e0daca; - font-family: "MS Sans Serif", Tahoma, sans-serif; - font-size: 12px; - overflow: hidden; - z-index: 100; -} - -#trade-dialog .trade-header { - background: linear-gradient(90deg, rgba(212, 184, 150, 0.15), transparent); - padding: 8px 12px; - border-bottom: 1px solid rgba(212, 184, 150, 0.2); - font-size: 13px; - font-weight: bold; - letter-spacing: 0.5px; - color: #d4b896; -} - -#trade-dialog .trade-columns { - display: flex; - gap: 1px; - background: rgba(212, 184, 150, 0.1); - min-height: 180px; -} - -#trade-dialog .trade-column { - flex: 1; - background: linear-gradient(180deg, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.2)); - padding: 6px 8px; - display: flex; - flex-direction: column; -} - -#trade-dialog .trade-col-header { - display: flex; - justify-content: space-between; - align-items: center; - padding-bottom: 4px; - margin-bottom: 4px; - border-bottom: 1px solid rgba(212, 184, 150, 0.12); -} - -#trade-dialog .trade-col-name { - color: #d4b896; - font-size: 11px; - font-weight: bold; -} - -#trade-dialog .trade-agree-badge { - font-size: 10px; - padding: 1px 6px; - border-radius: 3px; - font-weight: bold; -} - -#trade-dialog .trade-agree-badge.agreed { - background: rgba(76, 175, 80, 0.3); - border: 1px solid rgba(76, 175, 80, 0.4); - color: #a5d6a7; -} - -#trade-dialog .trade-agree-badge.waiting { - background: rgba(255, 193, 7, 0.15); - border: 1px solid rgba(255, 193, 7, 0.25); - color: #ffe082; -} - -#trade-dialog .trade-item-list { - flex: 1; - min-height: 120px; - max-height: 160px; - overflow-y: auto; -} - -#trade-dialog .trade-item-row { - display: flex; - justify-content: space-between; - align-items: center; - padding: 3px 4px; - border-radius: 3px; - font-size: 11px; -} - -#trade-dialog .trade-item-row:nth-child(odd) { - background: rgba(255, 255, 255, 0.03); -} - -#trade-dialog .trade-item-row.removable { - cursor: pointer; -} - -#trade-dialog .trade-item-row.removable:hover { - background: rgba(212, 184, 150, 0.1); -} - -#trade-dialog .trade-item-row .item-name { - color: #e0daca; -} - -#trade-dialog .trade-item-row .item-amount { - color: #a89b8c; - font-size: 10px; -} - -#trade-dialog .trade-empty { - color: #6b6055; - font-size: 11px; - text-align: center; - padding: 16px 0; - font-style: italic; -} - -#trade-dialog .trade-footer { - display: flex; - justify-content: space-between; - align-items: center; - gap: 8px; - padding: 8px 12px; - border-top: 1px solid rgba(212, 184, 150, 0.15); - background: rgba(0, 0, 0, 0.15); -} - -#trade-dialog .trade-footer-left { - display: flex; - gap: 6px; -} - -/* Item selection overlay */ -#trade-dialog .trade-overlay { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.8); - z-index: 5; - display: flex; - flex-direction: column; - padding: 8px; - overflow-y: auto; -} - -#trade-dialog .trade-overlay-title { - color: #d4b896; - font-weight: bold; - padding: 4px 0 8px; - text-align: center; -} - -#trade-dialog .trade-overlay-row { - display: flex; - justify-content: space-between; - padding: 4px 8px; - cursor: pointer; - border-radius: 3px; - color: #e0daca; - font-size: 11px; -} - -#trade-dialog .trade-overlay-row:hover { - background: rgba(212, 184, 150, 0.15); -} - -#trade-dialog .trade-overlay-row .item-amount { - color: #a89b8c; -} - -#trade-dialog .trade-overlay-back { - margin-top: 8px; - align-self: center; -} - -/* Amount prompt overlay */ -#trade-dialog .trade-prompt { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.8); - z-index: 5; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 16px; -} - -#trade-dialog .trade-prompt-label { - color: #d4b896; - margin-bottom: 12px; -} - -#trade-dialog .trade-prompt-input { - width: 100px; - padding: 4px 8px; - border: 1px solid rgba(212, 184, 150, 0.3); - border-radius: 3px; - background: rgba(0, 0, 0, 0.3); - color: #e0daca; - font-family: inherit; - font-size: 12px; - text-align: center; -} - -#trade-dialog .trade-prompt-buttons { - display: flex; - gap: 8px; - margin-top: 12px; -} - -/* Validation message */ -#trade-dialog .trade-message { - position: absolute; - bottom: 44px; - left: 0; - right: 0; - text-align: center; - padding: 4px 8px; - background: rgba(183, 28, 28, 0.3); - border-top: 1px solid rgba(244, 67, 54, 0.3); - color: #ef9a9a; - font-size: 11px; -} - -/* Trade request notification */ -#trade-request-notification { - position: absolute; - top: 120px; - left: 50%; - transform: translateX(-50%); - background: linear-gradient(145deg, #1a1a2e, #16213e); - border: 1px solid rgba(212, 184, 150, 0.3); - border-radius: 8px; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6); - padding: 12px 20px; - color: #e0daca; - font-family: "MS Sans Serif", Tahoma, sans-serif; - font-size: 12px; - z-index: 120; - text-align: center; - min-width: 200px; -} - -#trade-request-notification .trade-req-text { - margin-bottom: 10px; - color: #a89b8c; -} - -#trade-request-notification .trade-req-name { - color: #d4b896; - font-weight: bold; -} - -#trade-request-notification .trade-req-buttons { - display: flex; - justify-content: center; - gap: 10px; -} diff --git a/src/ui/trade-dialog/trade-dialog.ts b/src/ui/trade-dialog/trade-dialog.ts deleted file mode 100644 index b1e0dcd2..00000000 --- a/src/ui/trade-dialog/trade-dialog.ts +++ /dev/null @@ -1,373 +0,0 @@ -import { type Item, ItemSpecial } from 'eolib'; -import type { Client } from '@/client'; -import { playSfxById, SfxId } from '@/sfx'; -import { Base } from '@/ui/base-ui'; - -import './trade-dialog.css'; -import { DialogResourceID } from '@/edf'; -import { TradeState } from '@/game-state'; -import { capitalize } from '@/utils'; - -export class TradeDialog extends Base { - private client: Client; - protected container = document.getElementById('trade-dialog')!; - private dialogs = document.getElementById('dialogs')!; - private cover = document.querySelector('#cover')!; - private notification = document.getElementById('trade-request-notification')!; - private tradeRequestText = - this.notification.querySelector('.trade-req-text')!; - private columns = this.container.querySelector('.trade-columns')!; - private btnAgree = this.container.querySelector( - 'button[data-id="agree"]', - )!; - - private _open = false; - - get isOpen() { - return this._open; - } - - constructor(client: Client) { - super(); - this.client = client; - - // Static footer button wiring - this.container - .querySelector('button[data-id="add-item"]')! - .addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.showAddItemMenu(); - }); - - this.btnAgree.addEventListener('click', () => { - if (!this.client.tradeController.playerAgreed) { - if (this.client.tradeController.playerItems.length === 0) { - this.showTradeMessage('You must offer at least one item.'); - return; - } - if (this.client.tradeController.partnerItems.length === 0) { - this.showTradeMessage( - 'Your trade partner has not offered any items.', - ); - return; - } - } - - this.client.tradeController.agreeTrade(true); - }); - - this.container - .querySelector('button[data-id="cancel"]')! - .addEventListener('click', () => { - if (this.client.tradeController.playerAgreed) { - this.client.tradeController.agreeTrade(false); - } else { - this.client.tradeController.cancel(); - this.close(); - } - }); - - // Static notification button wiring - this.notification - .querySelector('button[data-id="decline"]')! - .addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.notification.classList.add('hidden'); - this.client.tradeController.reset(); - }); - - this.notification - .querySelector('button[data-id="accept"]')! - .addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - this.notification.classList.add('hidden'); - this.client.tradeController.acceptTradeRequest(); - }); - - // Client event listener - this.client.on('tradeUpdated', () => { - if (this.client.tradeController.state === TradeState.Pending) { - this.showRequest(); - return; - } - - if ( - this.client.tradeController.state === TradeState.Open && - !this._open - ) { - this.open(); - return; - } - - if (this.client.tradeController.state === TradeState.None && this._open) { - this.close(); - return; - } - - if (this.client.tradeController.scam) { - const strings = this.client.getDialogStrings( - DialogResourceID.TRADE_OTHER_PLAYER_TRICK_YOU, - ); - this.showTradeMessage(strings[1]); - this.client.tradeController.scam = false; - } - - this.renderColumns(); - this.updateAgreeButton(); - }); - } - - private showRequest() { - const strings = this.client.getDialogStrings( - DialogResourceID.TRADE_REQUEST, - ); - this.tradeRequestText.textContent = `${capitalize(this.client.tradeController.partnerName)} ${strings[1]}`; - this.notification.classList.remove('hidden'); - } - - private open() { - this._open = true; - - this.container.querySelector('.trade-header')!.textContent = - `Trade with ${capitalize(this.client.tradeController.partnerName)}`; - - this.renderColumns(); - this.updateAgreeButton(); - this.cover.classList.remove('hidden'); - this.container.classList.remove('hidden'); - this.dialogs.classList.remove('hidden'); - this.client.typing = true; - } - - private close() { - this._open = false; - this.cover.classList.add('hidden'); - this.container.classList.add('hidden'); - if (!document.querySelector('#dialogs > div:not(.hidden)')) { - this.dialogs.classList.add('hidden'); - this.client.typing = false; - } - } - - offerItem(itemId: number) { - if (!this._open) return; - const item = this.client.items.find((i) => i.id === itemId); - if (!item) return; - const record = this.client.getEifRecordById(itemId); - if (!record) return; - if (record.special === ItemSpecial.Lore) return; - this.promptAmount(itemId, item.amount); - } - - private updateAgreeButton() { - if (this.client.tradeController.playerAgreed) { - this.btnAgree.textContent = 'Agreed ✓'; - this.btnAgree.classList.add('active'); - } else { - this.btnAgree.textContent = 'Agree'; - this.btnAgree.classList.remove('active'); - } - } - - private renderColumns() { - if (!this._open) return; - this.columns.innerHTML = ''; - - this.columns.appendChild( - this.createColumn( - this.client.name, - this.client.tradeController.playerItems, - this.client.tradeController.playerAgreed, - true, - ), - ); - this.columns.appendChild( - this.createColumn( - this.client.tradeController.partnerName, - this.client.tradeController.partnerItems, - this.client.tradeController.partnerAgreed, - false, - ), - ); - } - - private createColumn( - name: string, - items: Item[], - agreed: boolean, - isLocal: boolean, - ): HTMLDivElement { - const col = document.createElement('div'); - col.className = 'trade-column'; - - const header = document.createElement('div'); - header.className = 'trade-col-header'; - - const nameEl = document.createElement('span'); - nameEl.className = 'trade-col-name'; - nameEl.textContent = isLocal ? 'You' : name; - header.appendChild(nameEl); - - const badge = document.createElement('span'); - badge.className = `trade-agree-badge ${agreed ? 'agreed' : 'waiting'}`; - badge.textContent = agreed ? 'Agreed' : 'Waiting'; - header.appendChild(badge); - - col.appendChild(header); - - const list = document.createElement('div'); - list.className = 'trade-item-list'; - - if (items.length === 0) { - const empty = document.createElement('div'); - empty.className = 'trade-empty'; - empty.textContent = 'No items offered'; - list.appendChild(empty); - } else { - for (const item of items) { - const row = document.createElement('div'); - row.className = `trade-item-row${isLocal ? ' removable' : ''}`; - - const record = this.client.getEifRecordById(item.id); - const nameSpan = document.createElement('span'); - nameSpan.className = 'item-name'; - nameSpan.textContent = record?.name ?? `Item #${item.id}`; - row.appendChild(nameSpan); - - const amountSpan = document.createElement('span'); - amountSpan.className = 'item-amount'; - amountSpan.textContent = `x${item.amount}`; - row.appendChild(amountSpan); - - if (isLocal) { - row.title = 'Click to remove'; - row.addEventListener('click', () => { - this.client.tradeController.removeItem(item.id); - }); - } - - list.appendChild(row); - } - } - - col.appendChild(list); - return col; - } - - private showAddItemMenu() { - const items = this.client.items.filter((i) => { - const record = this.client.getEifRecordById(i.id); - if (!record) return false; - return record.special !== 1; - }); - - if (items.length === 0) return; - - const overlay = document.createElement('div'); - overlay.className = 'trade-overlay'; - - const title = document.createElement('div'); - title.className = 'trade-overlay-title'; - title.textContent = 'Select item to offer'; - overlay.appendChild(title); - - for (const item of items) { - const record = this.client.getEifRecordById(item.id); - const row = document.createElement('div'); - row.className = 'trade-overlay-row'; - - const nameSpan = document.createElement('span'); - nameSpan.textContent = record?.name ?? `Item #${item.id}`; - row.appendChild(nameSpan); - - const amountSpan = document.createElement('span'); - amountSpan.className = 'item-amount'; - amountSpan.textContent = `x${item.amount}`; - row.appendChild(amountSpan); - - row.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - overlay.remove(); - this.promptAmount(item.id, item.amount); - }); - - overlay.appendChild(row); - } - - const btnClose = document.createElement('button'); - btnClose.className = 'trade-btn trade-overlay-back'; - btnClose.textContent = 'Back'; - btnClose.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - overlay.remove(); - }); - overlay.appendChild(btnClose); - - this.container.appendChild(overlay); - } - - private promptAmount(itemId: number, maxAmount: number) { - if (maxAmount === 1) { - this.client.tradeController.addItem(itemId, 1); - return; - } - - const overlay = document.createElement('div'); - overlay.className = 'trade-prompt'; - - const record = this.client.getEifRecordById(itemId); - const label = document.createElement('div'); - label.className = 'trade-prompt-label'; - label.textContent = `How many ${record?.name ?? 'items'}? (max ${maxAmount})`; - overlay.appendChild(label); - - const input = document.createElement('input'); - input.type = 'number'; - input.min = '1'; - input.max = String(maxAmount); - input.value = '1'; - input.className = 'trade-prompt-input'; - overlay.appendChild(input); - - const buttons = document.createElement('div'); - buttons.className = 'trade-prompt-buttons'; - - const btnCancel = document.createElement('button'); - btnCancel.className = 'trade-btn'; - btnCancel.textContent = 'Cancel'; - btnCancel.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - overlay.remove(); - }); - buttons.appendChild(btnCancel); - - const btnOk = document.createElement('button'); - btnOk.className = 'trade-btn primary'; - btnOk.textContent = 'OK'; - btnOk.addEventListener('click', () => { - playSfxById(SfxId.ButtonClick); - let amount = Number.parseInt(input.value, 10); - if (Number.isNaN(amount) || amount < 1) amount = 1; - if (amount > maxAmount) amount = maxAmount; - overlay.remove(); - this.client.tradeController.addItem(itemId, amount); - }); - buttons.appendChild(btnOk); - overlay.appendChild(buttons); - - this.container.appendChild(overlay); - input.focus(); - input.select(); - } - - private showTradeMessage(message: string) { - const existing = this.container.querySelector('.trade-message'); - if (existing) existing.remove(); - - const msg = document.createElement('div'); - msg.className = 'trade-message'; - msg.textContent = message; - this.container.appendChild(msg); - setTimeout(() => msg.remove(), 3000); - } -} diff --git a/src/ui/ui.tsx b/src/ui/ui.tsx new file mode 100644 index 00000000..505253fe --- /dev/null +++ b/src/ui/ui.tsx @@ -0,0 +1,5 @@ +import type { Client } from '@/client'; + +export function Ui({ client: _client }: { client: Client }) { + return

UI

; +} diff --git a/src/ui/utils/character-icon-to-chat-icon.ts b/src/ui/utils/character-icon-to-chat-icon.ts deleted file mode 100644 index 7ca512a7..00000000 --- a/src/ui/utils/character-icon-to-chat-icon.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { CharacterIcon } from 'eolib'; -import { ChatIcon } from '@/ui/ui-types'; - -export function characterIconToChatIcon(icon: CharacterIcon): ChatIcon { - switch (icon) { - case CharacterIcon.Player: - return ChatIcon.Player; - case CharacterIcon.Party: - return ChatIcon.PlayerParty; - case CharacterIcon.Gm: - return ChatIcon.GM; - case CharacterIcon.GmParty: - return ChatIcon.GMParty; - case CharacterIcon.Hgm: - return ChatIcon.HGM; - case CharacterIcon.HgmParty: - return ChatIcon.HGMParty; - } -} diff --git a/src/ui/utils/create-menu-item.ts b/src/ui/utils/create-menu-item.ts deleted file mode 100644 index 7de96d8d..00000000 --- a/src/ui/utils/create-menu-item.ts +++ /dev/null @@ -1,120 +0,0 @@ -import type { EifRecord, EsfRecord } from 'eolib'; -import type { DialogIcon } from '@/ui/ui-types'; -import { getItemMeta } from '@/utils'; -import { setItemImageFromGfx, setSkillBackgroundFromGfx } from './gfx-resource'; - -export function createIconMenuItem( - icon: DialogIcon, - label: string, - description: string, -) { - const menuItem = document.createElement('div'); - menuItem.classList.add('menu-item'); - - const menuIcon = document.createElement('div'); - menuIcon.classList.add('menu-item-icon'); - menuIcon.setAttribute('data-id', `${icon}`); - menuItem.appendChild(menuIcon); - - const menuLabel = document.createElement('div'); - menuLabel.classList.add('menu-label'); - menuLabel.innerText = label; - menuItem.appendChild(menuLabel); - - const menuDescription = document.createElement('div'); - menuDescription.classList.add('menu-description'); - menuDescription.innerText = description; - menuItem.appendChild(menuDescription); - - return menuItem; -} - -export function createItemMenuItem( - itemId: number, - record: EifRecord, - label: string, - description: string, - itemAmount = 1, -) { - const menuItem = document.createElement('div'); - menuItem.classList.add('menu-item', 'item'); - - const menuImg = document.createElement('img'); - void setItemImageFromGfx(menuImg, itemId, record.graphicId, itemAmount); - menuImg.classList.add('menu-item-img'); - menuItem.appendChild(menuImg); - - const tooltip = document.createElement('div'); - tooltip.classList.add('tooltip'); - const meta = getItemMeta(record); - tooltip.innerText = `${record.name}\n${meta.join('\n')}`; - menuItem.appendChild(tooltip); - - const menuLabel = document.createElement('div'); - menuLabel.classList.add('menu-label'); - menuLabel.innerText = label; - menuItem.appendChild(menuLabel); - - const menuDescription = document.createElement('div'); - menuDescription.classList.add('menu-description'); - menuDescription.innerText = description; - menuItem.appendChild(menuDescription); - - return menuItem; -} - -export function createSkillMenuItem( - record: EsfRecord, - label: string, - description: string, - onRequirementsClick: (() => void) | null = null, -) { - const menuItem = document.createElement('div'); - menuItem.classList.add('menu-item'); - - const menuIcon = document.createElement('div'); - menuIcon.classList.add('menu-item-icon', 'skill-icon'); - void setSkillBackgroundFromGfx(menuIcon, record.iconId); - menuIcon.style.width = '33px'; - menuIcon.style.height = '31px'; - menuItem.appendChild(menuIcon); - - const menuLabel = document.createElement('div'); - menuLabel.classList.add('menu-label'); - menuLabel.innerText = label; - menuItem.appendChild(menuLabel); - - if (description) { - const menuDescription = document.createElement('div'); - menuDescription.classList.add('menu-description', 'link'); - menuDescription.innerText = description; - menuDescription.addEventListener('click', (e) => { - e.stopPropagation(); - if (onRequirementsClick) { - onRequirementsClick(); - } - }); - menuItem.appendChild(menuDescription); - } - - return menuItem; -} - -export function createTextMenuItem( - text = ' ', - onClick: (() => void) | null = null, -) { - const menuItem = document.createElement('div'); - menuItem.classList.add('menu-item', 'text'); - menuItem.innerText = text; - - if (onClick) { - menuItem.classList.add('link'); - menuItem.addEventListener('click', (e) => { - e.stopPropagation(); - onClick(); - }); - } - - return menuItem; -} diff --git a/src/ui/utils/gfx-resource.ts b/src/ui/utils/gfx-resource.ts deleted file mode 100644 index 6e338349..00000000 --- a/src/ui/utils/gfx-resource.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { GfxLoader } from '@/gfx'; -import { getItemGraphicId } from '@/utils'; - -const ITEM_FILE_ID = 23; -const SKILL_FILE_ID = 25; -const URL_CACHE_LIMIT = 500; - -const gfxLoader = new GfxLoader(); -const urlCache = new Map(); -const pendingUrls = new Map>(); -const elementTokens = new WeakMap(); -let nextToken = 0; - -function createElementToken(el: HTMLElement): number { - const token = ++nextToken; - elementTokens.set(el, token); - return token; -} - -function isCurrentToken(el: HTMLElement, token: number): boolean { - return elementTokens.get(el) === token; -} - -function touchCachedUrl(cacheKey: string, url: string): void { - urlCache.delete(cacheKey); - urlCache.set(cacheKey, url); -} - -function addCachedUrl(cacheKey: string, url: string): void { - if (urlCache.has(cacheKey)) { - URL.revokeObjectURL(urlCache.get(cacheKey)!); - } - urlCache.set(cacheKey, url); - - while (urlCache.size > URL_CACHE_LIMIT) { - const oldest = urlCache.entries().next().value as [string, string]; - if (!oldest) break; - const [oldKey, oldUrl] = oldest; - URL.revokeObjectURL(oldUrl); - urlCache.delete(oldKey); - } -} - -async function imageBitmapToObjectUrl(bitmap: ImageBitmap): Promise { - if ('OffscreenCanvas' in self) { - const offscreen = new OffscreenCanvas(bitmap.width, bitmap.height); - const context = offscreen.getContext('2d'); - if (!context) { - throw new Error('Failed to create offscreen 2D context.'); - } - - context.drawImage(bitmap, 0, 0); - const blob = await offscreen.convertToBlob({ type: 'image/png' }); - return URL.createObjectURL(blob); - } - - const canvas = document.createElement('canvas'); - canvas.width = bitmap.width; - canvas.height = bitmap.height; - - const context = canvas.getContext('2d'); - if (!context) { - throw new Error('Failed to create canvas 2D context.'); - } - - context.drawImage(bitmap, 0, 0); - const blob = await new Promise((resolve, reject) => { - canvas.toBlob((result) => { - if (result) { - resolve(result); - } else { - reject(new Error('Failed to encode image blob.')); - } - }, 'image/png'); - }); - - return URL.createObjectURL(blob); -} - -async function getResourceUrl( - fileId: number, - resourceId: number, -): Promise { - const cacheKey = `${fileId}:${resourceId}`; - - const cached = urlCache.get(cacheKey); - if (cached) { - touchCachedUrl(cacheKey, cached); - return cached; - } - - const pending = pendingUrls.get(cacheKey); - if (pending) { - return pending; - } - - const promise = (async () => { - const bitmap = await gfxLoader.loadResource(fileId, resourceId); - const url = await imageBitmapToObjectUrl(bitmap); - addCachedUrl(cacheKey, url); - return url; - })(); - - pendingUrls.set(cacheKey, promise); - try { - return await promise; - } catch (error) { - console.error(`Failed to load gfx resource ${cacheKey}`, error); - throw error; - } finally { - pendingUrls.delete(cacheKey); - } -} - -async function setImageFromGfx( - image: HTMLImageElement, - fileId: number, - resourceId: number, -): Promise { - const token = createElementToken(image); - - try { - const url = await getResourceUrl(fileId, resourceId); - if (!isCurrentToken(image, token)) { - return; - } - image.src = url; - } catch (error) { - console.error('Failed to set image resource.', error); - } -} - -async function setBackgroundImageFromGfx( - el: HTMLElement, - fileId: number, - resourceId: number, -): Promise { - const token = createElementToken(el); - - try { - const url = await getResourceUrl(fileId, resourceId); - if (!isCurrentToken(el, token)) { - return; - } - el.style.backgroundImage = `url("${url}")`; - } catch (error) { - console.error('Failed to set background image resource.', error); - } -} - -export async function setItemImageFromGfx( - image: HTMLImageElement, - itemId: number, - graphicId: number, - amount = 1, -): Promise { - const resourceId = 100 + getItemGraphicId(itemId, graphicId, amount); - return setImageFromGfx(image, ITEM_FILE_ID, resourceId); -} - -export async function setItemGridImageFromGfx( - image: HTMLImageElement, - graphicId: number, -): Promise { - const resourceId = 100 + graphicId * 2; - return setImageFromGfx(image, ITEM_FILE_ID, resourceId); -} - -export async function setSkillBackgroundFromGfx( - el: HTMLElement, - iconId: number, -): Promise { - const resourceId = iconId + 100; - return setBackgroundImageFromGfx(el, SKILL_FILE_ID, resourceId); -} diff --git a/src/ui/utils/index.ts b/src/ui/utils/index.ts deleted file mode 100644 index 7bbb5ac6..00000000 --- a/src/ui/utils/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -export { characterIconToChatIcon } from './character-icon-to-chat-icon'; -export { - createIconMenuItem, - createItemMenuItem, - createSkillMenuItem, - createTextMenuItem, -} from './create-menu-item'; -export { - setItemGridImageFromGfx, - setItemImageFromGfx, - setSkillBackgroundFromGfx, -} from './gfx-resource'; diff --git a/src/wiring/client-events.ts b/src/wiring/client-events.ts deleted file mode 100644 index 5f674638..00000000 --- a/src/wiring/client-events.ts +++ /dev/null @@ -1,285 +0,0 @@ -import type { Client } from '@/client'; -import { DialogResourceID, EOResourceID } from '@/edf'; -import { GameState } from '@/game-state'; -import { playSfxById, SfxId } from '@/sfx'; -import { ChatIcon, ChatTab } from '@/ui/ui-types'; - -interface ClientEventDeps { - client: Client; - smallAlertLargeHeader: { - setContent(msg: string, title: string): void; - show(): void; - }; - smallConfirm: { - setContent(msg: string, title: string): void; - setCallback(cb: () => void): void; - show(): void; - }; - smallAlert: { setContent(msg: string, title: string): void; show(): void }; - createAccountForm: { hide(): void }; - mainMenu: { show(): void; hide(): void }; - loginForm: { hide(): void }; - characterSelect: { - setCharacters(chars: unknown): void; - hide(): void; - show(): void; - }; - createCharacterForm: { hide(): void }; - changePasswordForm: { hide(): void }; - chat: { - clear(): void; - show(): void; - addMessage(tab: ChatTab, msg: string, icon: ChatIcon, name?: string): void; - setMessage(msg: string): void; - }; - hud: { setStats(client: Client): void; show(): void }; - hotbar: { show(): void; refresh(): void }; - inGameMenu: { show(): void }; - exitGame: { show(): void }; - inventory: { loadPositions(): void; show(): void }; - stats: { render(): void }; - questDialog: { - setData( - questId: number, - dialogId: number, - name: string, - quests: unknown, - dialog: unknown, - ): void; - show(): void; - }; - paperdoll: { - setData(icon: unknown, details: unknown, equipment: unknown): void; - show(): void; - }; - chestDialog: { setItems(items: unknown): void; show(): void }; - shopDialog: { - setData(name: string, craftItems: unknown, tradeItems: unknown): void; - show(): void; - }; - bankDialog: { show(): void }; - barberDialog: { show(): void }; - boardDialog: { setData(posts: unknown): void; show(): void }; - lockerDialog: { setItems(items: unknown): void; show(): void }; - skillMasterDialog: { - setData(name: string, skills: unknown): void; - show(): void; - refresh(): void; - }; - partyDialog: { refresh(): void }; - mobileControls: { show(): void; hide(): void }; - initializeSocket: (next?: 'login' | 'create' | '') => void; -} - -let reconnectAttempts = 0; - -export function resetReconnectAttempts() { - reconnectAttempts = 0; -} - -export function getReconnectAttempts() { - return reconnectAttempts; -} - -export function incrementReconnectAttempts() { - reconnectAttempts++; - return reconnectAttempts; -} - -export function wireClientEvents(deps: ClientEventDeps): void { - const { client } = deps; - - client.on('error', ({ title, message }) => { - deps.smallAlertLargeHeader.setContent(message, title || 'Error'); - deps.smallAlertLargeHeader.show(); - }); - - client.on('confirmation', ({ title, message, onConfirm }) => { - deps.smallConfirm.setContent(message, title); - deps.smallConfirm.setCallback(() => { - onConfirm(); - }); - deps.smallConfirm.show(); - }); - - client.on('smallAlert', ({ title, message }) => { - deps.smallAlert.setContent(message, title); - deps.smallAlert.show(); - }); - - client.on('debug', (_message) => {}); - - client.on('accountCreated', () => { - const text = client.getDialogStrings( - DialogResourceID.ACCOUNT_CREATE_SUCCESS_WELCOME, - ); - deps.smallAlertLargeHeader.setContent(text[1], text[0]); - deps.smallAlertLargeHeader.show(); - deps.createAccountForm.hide(); - deps.mainMenu.show(); - }); - - client.on('login', (characters) => { - playSfxById(SfxId.Login); - deps.loginForm.hide(); - deps.characterSelect.setCharacters(characters); - deps.mainMenu.hide(); - deps.characterSelect.show(); - }); - - client.on('serverChat', ({ message, sfxId, icon }) => { - client.emit('chat', { - tab: ChatTab.Local, - name: client.getResourceString(EOResourceID.STRING_SERVER), - message, - icon: icon || ChatIcon.Exclamation, - }); - playSfxById(sfxId || SfxId.ServerMessage); - }); - - client.on('characterCreated', (characters) => { - deps.createCharacterForm.hide(); - const text = client.getDialogStrings( - DialogResourceID.CHARACTER_CREATE_SUCCESS, - ); - deps.smallAlertLargeHeader.setContent(text[1], text[0]); - deps.smallAlertLargeHeader.show(); - deps.characterSelect.setCharacters(characters); - }); - - client.on('characterDeleted', (characters) => { - deps.characterSelect.setCharacters(characters); - }); - - client.on('selectCharacter', () => {}); - - client.on('chat', ({ icon, tab, message, name }) => { - deps.chat.addMessage(tab, message, icon || ChatIcon.None, name); - }); - - client.on('enterGame', ({ news }) => { - deps.mainMenu.hide(); - deps.chat.clear(); - for (const line of news) { - if (line) { - deps.chat.addMessage(ChatTab.Local, line, ChatIcon.None); - } - } - - deps.characterSelect.hide(); - deps.exitGame.show(); - deps.chat.show(); - deps.hud.setStats(client); - deps.hud.show(); - deps.hotbar.show(); - deps.inGameMenu.show(); - deps.client.viewportController.resizeCanvases(); - deps.inventory.loadPositions(); - if (!client.viewportController.isMobile()) { - deps.inventory.show(); - } - }); - - client.on('passwordChanged', () => { - deps.changePasswordForm.hide(); - const text = client.getDialogStrings( - DialogResourceID.CHANGE_PASSWORD_SUCCESS, - ); - deps.smallAlertLargeHeader.setContent(text[1], text[0]); - deps.smallAlertLargeHeader.show(); - }); - - client.on('statsUpdate', () => { - deps.hud.setStats(client); - deps.stats.render(); - }); - - client.on('reconnect', () => { - deps.initializeSocket('login'); - }); - - client.on('openQuestDialog', (data) => { - client.typing = true; - deps.questDialog.setData( - data.questId, - data.dialogId, - data.name, - data.quests, - data.dialog, - ); - deps.questDialog.show(); - }); - - client.on('openPaperdoll', ({ icon, equipment, details }) => { - deps.paperdoll.setData(icon, details, equipment); - deps.paperdoll.show(); - }); - - client.on('chestOpened', ({ items }) => { - deps.chestDialog.setItems(items); - deps.chestDialog.show(); - }); - - client.on('chestChanged', ({ items }) => { - deps.chestDialog.setItems(items); - }); - - client.on('shopOpened', (data) => { - deps.shopDialog.setData(data.name, data.craftItems, data.tradeItems); - deps.shopDialog.show(); - }); - - client.on('bankOpened', () => { - deps.bankDialog.show(); - }); - - client.on('barberOpened', () => { - deps.barberDialog.show(); - }); - - client.on('boardOpened', ({ posts }) => { - deps.boardDialog.setData(posts); - deps.boardDialog.show(); - }); - - client.on('lockerOpened', ({ items }) => { - deps.lockerDialog.setItems(items); - deps.lockerDialog.show(); - }); - - client.on('lockerChanged', ({ items }) => { - deps.lockerDialog.setItems(items); - }); - - client.on('skillMasterOpened', ({ name, skills }) => { - deps.skillMasterDialog.setData(name, skills); - deps.skillMasterDialog.show(); - }); - - client.on('skillsChanged', () => { - deps.skillMasterDialog.refresh(); - }); - - client.on('spellQueued', () => { - deps.hotbar.refresh(); - }); - - client.on('setChat', (message) => { - deps.chat.setMessage(message); - }); - - client.on('partyUpdated', () => { - deps.partyDialog.refresh(); - }); - - client.on('resize', () => { - if ( - client.state === GameState.InGame && - client.viewportController.isMobile() - ) { - deps.mobileControls.show(); - } else { - deps.mobileControls.hide(); - } - }); -} diff --git a/src/wiring/index.ts b/src/wiring/index.ts deleted file mode 100644 index fe3d83c0..00000000 --- a/src/wiring/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { - getReconnectAttempts, - incrementReconnectAttempts, - resetReconnectAttempts, - wireClientEvents, -} from './client-events'; -export { wireUiEvents } from './ui-events'; diff --git a/src/wiring/ui-events.ts b/src/wiring/ui-events.ts deleted file mode 100644 index 29b5f05f..00000000 --- a/src/wiring/ui-events.ts +++ /dev/null @@ -1,860 +0,0 @@ -import type { ItemSpecial } from 'eolib'; -import type { Client } from '@/client'; -import { - LOCKER_MAX_ITEM_AMOUNT, - LOCKER_UPGRADE_BASE_COST, - LOCKER_UPGRADE_COST_STEP, - MAX_LOCKER_UPGRADES, -} from '@/consts'; -import { DialogResourceID, EOResourceID } from '@/edf'; -import { GameState } from '@/game-state'; -import { ChatIcon, ChatTab, SlotType } from '@/ui/ui-types'; -import { capitalize } from '@/utils'; - -// biome-ignore lint/suspicious/noExplicitAny: Event emitter callbacks require flexible argument types -type EventCallback = (...args: any[]) => void; - -interface UiEventDeps { - client: Client; - mainMenu: { - show(): void; - hide(): void; - on(event: string, cb: EventCallback): void; - }; - loginForm: { - show(): void; - hide(): void; - on(event: string, cb: EventCallback): void; - }; - createAccountForm: { - show(): void; - hide(): void; - on(event: string, cb: EventCallback): void; - }; - characterSelect: { - show(): void; - hide(): void; - confirmed: boolean; - on(event: string, cb: EventCallback): void; - selectCharacter(index: number): void; - isOpen?(): boolean; - }; - createCharacterForm: { - show(): void; - hide(): void; - on(event: string, cb: EventCallback): void; - isOpen(): boolean; - }; - changePasswordForm: { - show(): void; - hide(): void; - on(event: string, cb: EventCallback): void; - isOpen(): boolean; - }; - exitGame: { - show(): void; - on(event: string, cb: EventCallback): void; - }; - chat: { - focus(): void; - addMessage(tab: ChatTab, msg: string, icon: ChatIcon, name?: string): void; - on(event: string, cb: EventCallback): void; - show(): void; - }; - smallConfirm: { - setContent(msg: string, title: string): void; - setCallback(cb: () => void, hideOnCallback?: boolean): void; - show(): void; - }; - smallAlertLargeHeader: { - setContent(msg: string, title: string): void; - show(): void; - }; - smallAlert: { setContent(msg: string, title: string): void; show(): void }; - largeAlertSmallHeader: { - setContent(msg: string, title: string): void; - show(): void; - }; - largeConfirmSmallHeader: { - setContent(msg: string, title: string): void; - setCallback(cb: () => void): void; - show(): void; - hide(): void; - }; - inventory: { - toggle(): void; - show(): void; - on(event: string, cb: EventCallback): void; - }; - stats: { - toggle(): void; - setTrainingConfirmed(): void; - on(event: string, cb: EventCallback): void; - }; - spellBook: { - toggle(): void; - on(event: string, cb: EventCallback): void; - }; - onlineList: { toggle(): void }; - inGameMenu: { on(event: string, cb: EventCallback): void }; - questDialog: { on(event: string, cb: EventCallback): void }; - shopDialog: { on(event: string, cb: EventCallback): void }; - bankDialog: { on(event: string, cb: EventCallback): void }; - lockerDialog: { - getItemAmount(id: number): number; - }; - hotbar: { setSlot(index: number, type: SlotType, id: number): void }; - itemAmountDialog: { - setMaxAmount(max: number): void; - setHeader(h: string): void; - setLabel(l: string): void; - setCallback(cb: (amount: number) => void, onCancel?: () => void): void; - show(): void; - hide(): void; - }; - partyDialog: { toggle(): void }; - hideAllUi: () => void; - initializeSocket: (next?: 'login' | 'create' | '') => void; -} - -export function wireUiEvents(deps: UiEventDeps): void { - const { client } = deps; - - // Exit game - deps.exitGame.on('click', () => { - const text = client.getDialogStrings( - DialogResourceID.EXIT_GAME_ARE_YOU_SURE, - ); - deps.smallConfirm.setContent(text[1], text[0]); - deps.smallConfirm.setCallback(() => { - client.disconnect(); - deps.hideAllUi(); - deps.mainMenu.show(); - }); - deps.smallConfirm.show(); - }); - - // Main menu - deps.mainMenu.on('play-game', () => { - if (client.state === GameState.Initial) { - deps.initializeSocket('login'); - } else { - deps.mainMenu.hide(); - deps.loginForm.show(); - } - }); - - deps.mainMenu.on('create-account', () => { - if (client.state === GameState.Initial) { - deps.initializeSocket('create'); - } else { - deps.mainMenu.hide(); - deps.createAccountForm.show(); - } - }); - - deps.mainMenu.on('view-credits', () => { - window.open(client.config.creditsUrl, '_blank'); - }); - - deps.mainMenu.on('host-change', (host: unknown) => { - client.config.host = host as string; - client.disconnect(); - }); - - // Create account - deps.createAccountForm.on('cancel', () => { - deps.createAccountForm.hide(); - deps.mainMenu.show(); - }); - - deps.createAccountForm.on( - 'error', - ({ title, message }: { title: string; message: string }) => { - deps.smallAlertLargeHeader.setContent(message, title); - deps.smallAlertLargeHeader.show(); - }, - ); - - deps.createAccountForm.on('create', (data: unknown) => { - client.authenticationController.requestAccountCreation( - data as Parameters< - typeof client.authenticationController.requestAccountCreation - >[0], - ); - }); - - // Login - deps.loginForm.on( - 'login', - ({ - username, - password, - rememberMe, - }: { - username: string; - password: string; - rememberMe: boolean; - }) => { - client.authenticationController.login(username, password, rememberMe); - }, - ); - - deps.loginForm.on('cancel', () => { - deps.loginForm.hide(); - deps.mainMenu.show(); - }); - - // Character select - deps.characterSelect.on('cancel', () => { - client.disconnect(); - deps.characterSelect.hide(); - deps.mainMenu.show(); - }); - - deps.characterSelect.on('changePassword', () => { - deps.changePasswordForm.show(); - }); - - deps.characterSelect.on('selectCharacter', (id: unknown) => { - client.authenticationController.selectCharacter(id as number); - }); - - deps.characterSelect.on( - 'requestCharacterDeletion', - ({ id, name }: { id: number; name: string }) => { - const strings = client.getDialogStrings( - DialogResourceID.CHARACTER_DELETE_FIRST_CHECK, - ); - deps.smallConfirm.setContent( - `${capitalize(name)} ${strings[1]}`, - strings[0], - ); - deps.smallConfirm.setCallback(() => { - client.authenticationController.requestCharacterDeletion(id); - deps.characterSelect.confirmed = true; - }); - deps.smallConfirm.show(); - }, - ); - - deps.characterSelect.on( - 'deleteCharacter', - ({ id, name }: { id: number; name: string }) => { - const strings = client.getDialogStrings( - DialogResourceID.CHARACTER_DELETE_CONFIRM, - ); - deps.smallConfirm.setContent( - `${capitalize(name)} ${strings[1]}`, - strings[0], - ); - deps.smallConfirm.setCallback(() => { - client.authenticationController.deleteCharacter(id); - }); - deps.smallConfirm.show(); - }, - ); - - deps.characterSelect.on( - 'error', - ({ title, message }: { title: string; message: string }) => { - deps.smallAlertLargeHeader.setContent(message, title); - deps.smallAlertLargeHeader.show(); - }, - ); - - deps.characterSelect.on('create', () => { - deps.createCharacterForm.show(); - }); - - // Character creation - deps.createCharacterForm.on('create', (data: unknown) => { - client.authenticationController.requestCharacterCreation( - data as Parameters< - typeof client.authenticationController.requestCharacterCreation - >[0], - ); - }); - - // Change password - deps.changePasswordForm.on( - 'error', - ({ title, message }: { title: string; message: string }) => { - deps.smallAlertLargeHeader.setContent(message, title); - deps.smallAlertLargeHeader.show(); - }, - ); - - deps.changePasswordForm.on( - 'changePassword', - ({ - username, - oldPassword, - newPassword, - }: { - username: string; - oldPassword: string; - newPassword: string; - }) => { - client.authenticationController.changePassword( - username, - oldPassword, - newPassword, - ); - }, - ); - - // Chat - deps.chat.on('chat', (message: unknown) => { - client.chatController.chat(message as string); - }); - - deps.chat.on('focus', () => { - client.typing = true; - }); - - deps.chat.on('blur', () => { - client.typing = false; - }); - - // In-game menu toggles - const handleToggle = (which: unknown) => { - switch (which as string) { - case 'inventory': - deps.inventory.toggle(); - break; - case 'map': - client.toggleMinimap(); - break; - case 'stats': - deps.stats.toggle(); - break; - case 'spells': - deps.spellBook.toggle(); - break; - case 'party': - deps.partyDialog.toggle(); - break; - case 'online': - deps.onlineList.toggle(); - break; - } - }; - - deps.inGameMenu.on('toggle', handleToggle); - - // Inventory events - wireInventoryEvents(deps); - - // Quest dialog - deps.questDialog.on( - 'reply', - ({ - questId, - dialogId, - action, - }: { - questId: number; - dialogId: number; - action: number | null; - }) => { - client.questController.questReply(questId, dialogId, action); - client.typing = false; - }, - ); - - deps.questDialog.on('cancel', () => { - client.typing = false; - }); - - // Shop dialog - wireShopEvents(deps); - - // Bank dialog - wireBankEvents(deps); - - // Stats - deps.stats.on('confirmTraining', () => { - deps.smallConfirm.setContent('Do you want to train?', 'Character training'); - deps.smallConfirm.setCallback(() => { - deps.stats.setTrainingConfirmed(); - }); - deps.smallConfirm.show(); - }); - - // Spell book - deps.spellBook.on( - 'assignToSlot', - ({ spellId, slotIndex }: { spellId: number; slotIndex: number }) => { - deps.hotbar.setSlot(slotIndex, SlotType.Skill, spellId); - }, - ); - - // Inventory assign to slot - deps.inventory.on( - 'assignToSlot', - ({ itemId, slotIndex }: { itemId: number; slotIndex: number }) => { - deps.hotbar.setSlot(slotIndex, SlotType.Item, itemId); - }, - ); - - deps.inventory.on('openPaperdoll', () => { - client.socialController.requestPaperdoll(client.playerId); - }); - - deps.inventory.on( - 'equipItem', - ({ slot, itemId }: { slot: unknown; itemId: number }) => { - client.inventoryController.equipItem( - slot as Parameters[0], - itemId, - ); - }, - ); - - deps.inventory.on('useItem', (itemId: unknown) => { - client.inventoryController.useItem(itemId as number); - }); - - // Keyboard - window.addEventListener('keyup', (e) => { - if (client.state === GameState.InGame && e.key === 'Enter') { - deps.chat.focus(); - } - - if ( - client.state === GameState.LoggedIn && - !deps.changePasswordForm.isOpen() && - !deps.createCharacterForm.isOpen() && - ['1', '2', '3'].includes(e.key) - ) { - deps.characterSelect.selectCharacter(Number.parseInt(e.key, 10)); - } - }); -} - -function wireInventoryEvents(deps: UiEventDeps): void { - const { client } = deps; - - deps.inventory.on( - 'dropItem', - ({ at, itemId }: { at: string; itemId: number }) => { - const item = client.items.find((i) => i.id === itemId); - if (!item) return; - - if (at === 'cursor' && !client.mapController.cursorInDropRange()) { - client.setStatusLabel( - EOResourceID.STATUS_LABEL_TYPE_WARNING, - client.getResourceString( - EOResourceID.STATUS_LABEL_ITEM_DROP_OUT_OF_RANGE, - ), - ); - deps.chat.addMessage( - ChatTab.System, - client.getResourceString( - EOResourceID.STATUS_LABEL_ITEM_DROP_OUT_OF_RANGE, - ), - ChatIcon.DotDotDotDot, - ); - return; - } - - const playerAt = client.getPlayerCoords(); - const coords = at === 'cursor' ? client.mouseCoords : playerAt; - if ( - client.nearby.items.some( - (i) => - i.coords.x === coords!.x! && - i.coords.y === coords!.y! && - i.id === itemId, - ) - ) { - return; - } - - const record = client.getEifRecordById(itemId); - if (!record) return; - - if ((record as { special: ItemSpecial }).special === 1) { - // ItemSpecial.Lore - const strings = client.getDialogStrings( - DialogResourceID.ITEM_IS_LORE_ITEM, - ); - deps.smallAlert.setContent(strings[1], strings[0]); - deps.smallAlert.show(); - return; - } - - if (item.amount > 1) { - client.typing = true; - deps.itemAmountDialog.setMaxAmount(item.amount); - deps.itemAmountDialog.setHeader('drop'); - deps.itemAmountDialog.setLabel( - `${client.getResourceString(EOResourceID.DIALOG_TRANSFER_HOW_MUCH)} ${record.name} ${client.getResourceString(EOResourceID.DIALOG_TRANSFER_DROP)}`, - ); - deps.itemAmountDialog.setCallback( - (amount) => { - client.inventoryController.dropItem(itemId, amount, coords!); - client.typing = false; - }, - () => { - client.typing = false; - }, - ); - deps.itemAmountDialog.show(); - } else { - client.inventoryController.dropItem(itemId, 1, coords!); - } - }, - ); - - deps.inventory.on('junkItem', (itemId: unknown) => { - const id = itemId as number; - const item = client.items.find((i) => i.id === id); - if (!item) return; - - const record = client.getEifRecordById(id); - if (!record) return; - - if (item.amount > 1) { - client.typing = true; - deps.itemAmountDialog.setMaxAmount(item.amount); - deps.itemAmountDialog.setHeader('junk'); - deps.itemAmountDialog.setLabel( - `${client.getResourceString(EOResourceID.DIALOG_TRANSFER_HOW_MUCH)} ${record.name} ${client.getResourceString(EOResourceID.DIALOG_TRANSFER_JUNK)}`, - ); - deps.itemAmountDialog.setCallback( - (amount) => { - client.inventoryController.junkItem(id, amount); - client.typing = false; - }, - () => { - client.typing = false; - }, - ); - deps.itemAmountDialog.show(); - } else { - client.inventoryController.junkItem(id, 1); - } - }); - - deps.inventory.on('addChestItem', (itemId: unknown) => { - const id = itemId as number; - const item = client.items.find((i) => i.id === id); - if (!item) return; - - const record = client.getEifRecordById(id); - if (!record) return; - - if (item.amount > 1) { - client.typing = true; - deps.itemAmountDialog.setMaxAmount(item.amount); - deps.itemAmountDialog.setHeader('drop'); - deps.itemAmountDialog.setLabel( - `${client.getResourceString(EOResourceID.DIALOG_TRANSFER_HOW_MUCH)} ${record.name} ${client.getResourceString(EOResourceID.DIALOG_TRANSFER_DROP)}`, - ); - deps.itemAmountDialog.setCallback( - (amount) => { - client.chestController.addItem(id, amount); - client.typing = false; - }, - () => { - client.typing = false; - }, - ); - deps.itemAmountDialog.show(); - } else { - client.chestController.addItem(id, 1); - } - }); - - deps.inventory.on('addLockerItem', (itemId: unknown) => { - const id = itemId as number; - const item = client.items.find((i) => i.id === id); - if (!item) return; - - const record = client.getEifRecordById(id); - if (!record) return; - - if (id === 1) { - const strings = client.getDialogStrings( - DialogResourceID.LOCKER_DEPOSIT_GOLD_ERROR, - ); - deps.smallAlert.setContent(strings[1], strings[0]); - deps.smallAlert.show(); - return; - } - - const itemAmount = deps.lockerDialog.getItemAmount(id); - if (itemAmount >= LOCKER_MAX_ITEM_AMOUNT) { - const strings = client.getDialogStrings( - DialogResourceID.LOCKER_FULL_SINGLE_ITEM_MAX, - ); - deps.smallAlert.setContent(strings[1], strings[0]); - deps.smallAlert.show(); - return; - } - - if (item.amount > 1) { - client.typing = true; - deps.itemAmountDialog.setMaxAmount( - Math.min(item.amount, LOCKER_MAX_ITEM_AMOUNT - itemAmount), - ); - deps.itemAmountDialog.setHeader('bank'); - deps.itemAmountDialog.setLabel( - `${client.getResourceString(EOResourceID.DIALOG_TRANSFER_HOW_MUCH)} ${record.name} ${client.getResourceString(EOResourceID.DIALOG_TRANSFER_DEPOSIT)}`, - ); - deps.itemAmountDialog.setCallback( - (amount) => { - client.lockerController.addItem(id, amount); - client.typing = false; - }, - () => { - client.typing = false; - }, - ); - deps.itemAmountDialog.show(); - } else { - client.lockerController.addItem(id, 1); - } - }); -} - -function wireShopEvents(deps: UiEventDeps): void { - const { client } = deps; - - deps.shopDialog.on('buyItem', (item: unknown) => { - const shopItem = item as { - id: number; - name: string; - price: number; - max: number; - }; - const goldAmount = client.items.find((i) => i.id === 1)!.amount; - if (shopItem.price > goldAmount) { - const text = client.getDialogStrings( - DialogResourceID.WARNING_YOU_HAVE_NOT_ENOUGH, - ); - deps.smallAlert.setContent(text[1], text[0]); - deps.smallAlert.show(); - return; - } - - deps.itemAmountDialog.setHeader('shop'); - deps.itemAmountDialog.setMaxAmount(shopItem.max); - deps.itemAmountDialog.setLabel( - `${client.getResourceString(EOResourceID.DIALOG_TRANSFER_HOW_MUCH)} ${shopItem.name} ${client.getResourceString(EOResourceID.DIALOG_TRANSFER_BUY)}`, - ); - deps.itemAmountDialog.setCallback( - (amount) => { - const total = amount * shopItem.price; - const goldAmount = client.items.find((i) => i.id === 1)!.amount; - deps.itemAmountDialog.hide(); - if (total > goldAmount) { - const text = client.getDialogStrings( - DialogResourceID.WARNING_YOU_HAVE_NOT_ENOUGH, - ); - deps.smallAlert.setContent(text[1], text[0]); - deps.smallAlert.show(); - } else { - const wordBuy = client.getResourceString( - EOResourceID.DIALOG_WORD_BUY, - ); - const wordFor = client.getResourceString( - EOResourceID.DIALOG_WORD_FOR, - ); - const goldRecord = client.getEifRecordById(1); - deps.smallConfirm.setContent( - `${wordBuy} ${amount} ${shopItem.name} ${wordFor} ${total} ${goldRecord!.name!} ?`, - client.getResourceString(EOResourceID.DIALOG_SHOP_BUY_ITEMS) ?? '', - ); - deps.smallConfirm.setCallback(() => { - client.shopController.buyItem(shopItem.id, amount); - }, true); - deps.smallConfirm.show(); - } - }, - () => {}, - ); - deps.itemAmountDialog.show(); - }); - - deps.shopDialog.on('sellItem', (item: unknown) => { - const shopItem = item as { id: number; name: string; price: number }; - const itemAmount = client.items.find((i) => i.id === shopItem.id)!.amount; - const showConfirm = (amount: number, total: number) => { - const wordSell = client.getResourceString(EOResourceID.DIALOG_WORD_SELL); - const wordFor = client.getResourceString(EOResourceID.DIALOG_WORD_FOR); - const goldRecord = client.getEifRecordById(1); - deps.smallConfirm.setContent( - `${wordSell} ${amount} ${shopItem.name} ${wordFor} ${total} ${goldRecord!.name!} ?`, - client.getResourceString(EOResourceID.DIALOG_SHOP_SELL_ITEMS) ?? '', - ); - deps.smallConfirm.setCallback(() => { - client.shopController.sellItem(shopItem.id, amount); - }); - deps.smallConfirm.show(); - }; - - if (itemAmount === 1) { - showConfirm(1, shopItem.price); - return; - } - - deps.itemAmountDialog.setHeader('shop'); - deps.itemAmountDialog.setMaxAmount(itemAmount); - deps.itemAmountDialog.setLabel( - `${client.getResourceString(EOResourceID.DIALOG_TRANSFER_HOW_MUCH)} ${shopItem.name} ${client.getResourceString(EOResourceID.DIALOG_TRANSFER_SELL)}`, - ); - deps.itemAmountDialog.setCallback((amount) => { - const total = amount * shopItem.price; - deps.itemAmountDialog.hide(); - showConfirm(amount, total); - }); - deps.itemAmountDialog.show(); - }); - - deps.shopDialog.on('craftItem', (item: unknown) => { - const craftItem = item as { - id: number; - name: string; - ingredients: { id: number; amount: number }[]; - }; - const missing = craftItem.ingredients.some((ingredient) => { - if (!ingredient.amount) return false; - const item = client.items.find((i) => i.id === ingredient.id); - return !item || item.amount < ingredient.amount; - }); - - const lines = craftItem.ingredients - .map((ingredient) => { - if (!ingredient.id) return ''; - const record = client.getEifRecordById(ingredient.id); - if (!record) return ''; - return `+ ${ingredient.amount} ${record.name}`; - }) - .filter((l) => !!l); - - if (missing) { - deps.largeAlertSmallHeader.setContent( - `${client.getResourceString(EOResourceID.DIALOG_SHOP_CRAFT_MISSING_INGREDIENTS)}\n\n${lines.join('\n')}`, - `${client.getResourceString(EOResourceID.DIALOG_SHOP_CRAFT_INGREDIENTS)} ${craftItem.name}`, - ); - deps.largeAlertSmallHeader.show(); - return; - } - - deps.largeConfirmSmallHeader.setContent( - `${client.getResourceString(EOResourceID.DIALOG_SHOP_CRAFT_PUT_INGREDIENTS_TOGETHER)}\n\n${lines.join('\n')}`, - `${client.getResourceString(EOResourceID.DIALOG_SHOP_CRAFT_INGREDIENTS)} ${craftItem.name}`, - ); - deps.largeConfirmSmallHeader.setCallback(() => { - client.shopController.craftItem(craftItem.id); - deps.largeConfirmSmallHeader.hide(); - }); - deps.largeConfirmSmallHeader.show(); - }); -} - -function wireBankEvents(deps: UiEventDeps): void { - const { client } = deps; - - deps.bankDialog.on('deposit', () => { - const gold = client.items.find((i) => i.id === 1); - if (!gold || gold.amount <= 0) { - const strings = client.getDialogStrings( - DialogResourceID.BANK_ACCOUNT_UNABLE_TO_DEPOSIT, - ); - deps.smallAlert.setContent(strings[1], strings[0]); - deps.smallAlert.show(); - return; - } - - if (gold.amount > 1) { - const record = client.getEifRecordById(1); - if (!record) throw new Error('Failed to fetch gold record'); - deps.itemAmountDialog.setHeader('bank'); - deps.itemAmountDialog.setMaxAmount(gold.amount); - deps.itemAmountDialog.setLabel( - `${client.getResourceString(EOResourceID.DIALOG_TRANSFER_HOW_MUCH)} ${record.name} ${client.getResourceString(EOResourceID.DIALOG_TRANSFER_DEPOSIT)}`, - ); - deps.itemAmountDialog.setCallback((amount) => { - client.bankController.depositGold(amount); - deps.itemAmountDialog.hide(); - }); - deps.itemAmountDialog.show(); - return; - } - - client.bankController.depositGold(1); - }); - - deps.bankDialog.on('withdraw', () => { - if (client.bankController.goldBank <= 0) { - const strings = client.getDialogStrings( - DialogResourceID.BANK_ACCOUNT_UNABLE_TO_WITHDRAW, - ); - deps.smallAlert.setContent(strings[1], strings[0]); - deps.smallAlert.show(); - return; - } - - if (client.bankController.goldBank > 1) { - const record = client.getEifRecordById(1); - if (!record) throw new Error('Failed to fetch gold record'); - deps.itemAmountDialog.setHeader('bank'); - deps.itemAmountDialog.setMaxAmount(client.bankController.goldBank); - deps.itemAmountDialog.setLabel( - `${client.getResourceString(EOResourceID.DIALOG_TRANSFER_HOW_MUCH)} ${record.name} ${client.getResourceString(EOResourceID.DIALOG_TRANSFER_WITHDRAW)}`, - ); - deps.itemAmountDialog.setCallback((amount) => { - client.bankController.withdrawGold(amount); - deps.itemAmountDialog.hide(); - }); - deps.itemAmountDialog.show(); - return; - } - - client.bankController.withdrawGold(1); - }); - - deps.bankDialog.on('upgrade', () => { - if (client.bankController.lockerUpgrades >= MAX_LOCKER_UPGRADES) { - const strings = client.getDialogStrings( - DialogResourceID.LOCKER_UPGRADE_IMPOSSIBLE, - ); - deps.smallAlert.setContent(strings[1], strings[0]); - deps.smallAlert.show(); - return; - } - - const requiredGold = - LOCKER_UPGRADE_BASE_COST + - LOCKER_UPGRADE_COST_STEP * client.bankController.lockerUpgrades; - const gold = client.items.find((i) => i.id === 1)?.amount || 0; - - const record = client.getEifRecordById(1); - if (!record) throw new Error('Failed to fetch gold record'); - - if (gold < requiredGold) { - const strings = client.getDialogStrings( - DialogResourceID.WARNING_YOU_HAVE_NOT_ENOUGH, - ); - deps.smallAlert.setContent(`${strings[1]} ${record.name}`, strings[0]); - deps.smallAlert.show(); - return; - } - - const strings = client.getDialogStrings( - DialogResourceID.LOCKER_UPGRADE_UNIT, - ); - deps.smallConfirm.setContent( - `${strings[1]} ${requiredGold} ${record.name}`, - strings[0], - ); - deps.smallConfirm.setCallback(() => { - client.lockerController.upgradeLocker(); - }); - deps.smallConfirm.show(); - }); -} diff --git a/tsconfig.json b/tsconfig.json index 3aea92bd..df155207 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,10 @@ /* Path aliases */ "paths": { "@/*": ["./src/*"] - } + }, + + "jsx": "react-jsx", + "jsxImportSource": "preact" }, "include": ["src"], "exclude": [ diff --git a/vite.config.ts b/vite.config.ts index 468a7de3..407457e0 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,9 @@ import path from 'node:path'; +import preact from '@preact/preset-vite'; import { defineConfig } from 'vite'; export default defineConfig({ + plugins: [preact()], resolve: { alias: { '@': path.resolve(__dirname, 'src'), From dd281723abf7106404f405cc3a8a416009790df5 Mon Sep 17 00:00:00 2001 From: Richard Leek Date: Thu, 2 Apr 2026 10:17:42 -0400 Subject: [PATCH 002/103] Fix missing dom errors --- src/client/client.ts | 14 ++++++++++---- src/controllers/keyboard-controller.ts | 8 +++++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/client/client.ts b/src/client/client.ts index c703a82a..5f6b158d 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -290,15 +290,21 @@ export class Client { this.config = config; const txtHost = document.querySelector('input[name="host"]')!; - if (this.config.staticHost) { - txtHost!.classList.add('hidden'); + + if (txtHost) { + if (this.config.staticHost) { + txtHost!.classList.add('hidden'); + } + txtHost!.value = config.host; } - txtHost!.value = config.host; + document.title = config.title; const mainMenuLogo = document.querySelector('#main-menu-logo')!; - mainMenuLogo!.setAttribute('data-slogan', config.slogan); + if (mainMenuLogo) { + mainMenuLogo!.setAttribute('data-slogan', config.slogan); + } }); this.atlas = new Atlas(this); this.sans11 = new Sans11Font(this.atlas); diff --git a/src/controllers/keyboard-controller.ts b/src/controllers/keyboard-controller.ts index b9f11aef..f216d41c 100644 --- a/src/controllers/keyboard-controller.ts +++ b/src/controllers/keyboard-controller.ts @@ -62,7 +62,7 @@ function inputToDirection(input: Input): Direction | null { } const WALK_TICKS = WALK_ANIMATION_TICKS - 1; -const DRAG_THRESHOLD = 30; +//const DRAG_THRESHOLD = 30; export class KeyboardController { private client: Client; @@ -78,11 +78,13 @@ export class KeyboardController { private held: boolean[] = []; private lastInputHeld: Input[] = []; + /* private touchStartX: number | null = null; private touchStartY: number | null = null; private touchId: number | null = null; private activeTouchDir: Input | null = null; private inputVector = { x: 0, y: 0 }; + */ constructor(client: Client) { this.client = client; @@ -275,6 +277,7 @@ export class KeyboardController { { passive: false }, ); + /* const joystickContainer = document.getElementById('joystick-container'); const thumb = document.getElementById('joystick-thumb'); const maxRadius = 40; @@ -319,8 +322,10 @@ export class KeyboardController { btnSit!.addEventListener('touchend', () => { this.updateInputHeld(Input.SitStand, false); }); + */ } + /* private handleTouchMove( e: TouchEvent, joystickContainer: HTMLElement, @@ -380,6 +385,7 @@ export class KeyboardController { } return dy < 0 ? Input.Up : Input.Down; } + */ private updateInputHeld(input: Input, down: boolean) { this.held[input] = down; From ff61a4a2d144aed00140836a51489afcfb400577 Mon Sep 17 00:00:00 2001 From: Richard Leek Date: Thu, 2 Apr 2026 10:31:15 -0400 Subject: [PATCH 003/103] Add tailwind/daisyui --- biome.json | 13 + package.json | 3 + pnpm-lock.yaml | 201 +++++++++ src/css/style.css | 1050 +-------------------------------------------- vite.config.ts | 3 +- 5 files changed, 221 insertions(+), 1049 deletions(-) diff --git a/biome.json b/biome.json index 9d95d857..24962637 100644 --- a/biome.json +++ b/biome.json @@ -87,5 +87,18 @@ "quoteStyle": "single", "jsxQuoteStyle": "single" } + }, + "css": { + "parser": { + "tailwindDirectives": true + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } } } diff --git a/package.json b/package.json index 40d5ed8e..cae0dba4 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,12 @@ "devDependencies": { "@biomejs/biome": "2.4.10", "@preact/preset-vite": "^2.10.5", + "@tailwindcss/vite": "^4.2.2", "@types/node": "^25.5.0", + "daisyui": "^5.5.19", "knip": "^6.2.0", "lefthook": "^2.1.4", + "tailwindcss": "^4.2.2", "typescript": "~6.0.2", "vite": "^8.0.3" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 29f49ee9..50efcfdc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,15 +33,24 @@ importers: '@preact/preset-vite': specifier: ^2.10.5 version: 2.10.5(@babel/core@7.29.0)(preact@10.29.0)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(yaml@2.8.3)) + '@tailwindcss/vite': + specifier: ^4.2.2 + version: 4.2.2(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(yaml@2.8.3)) '@types/node': specifier: ^25.5.0 version: 25.5.0 + daisyui: + specifier: ^5.5.19 + version: 5.5.19 knip: specifier: ^6.2.0 version: 6.2.0 lefthook: specifier: ^2.1.4 version: 2.1.4 + tailwindcss: + specifier: ^4.2.2 + version: 4.2.2 typescript: specifier: ~6.0.2 version: 6.0.2 @@ -866,6 +875,100 @@ packages: rollup: optional: true + '@tailwindcss/node@4.2.2': + resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==} + + '@tailwindcss/oxide-android-arm64@4.2.2': + resolution: {integrity: sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.2.2': + resolution: {integrity: sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.2.2': + resolution: {integrity: sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.2.2': + resolution: {integrity: sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': + resolution: {integrity: sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': + resolution: {integrity: sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.2.2': + resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.2.2': + resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.2.2': + resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.2.2': + resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': + resolution: {integrity: sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.2.2': + resolution: {integrity: sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.2.2': + resolution: {integrity: sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.2.2': + resolution: {integrity: sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -930,6 +1033,9 @@ packages: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} + daisyui@5.5.19: + resolution: {integrity: sha512-pbFAkl1VCEh/MPCeclKL61I/MqRIFFhNU7yiXoDDRapXN4/qNCoMxeCCswyxEEhqL5eiTTfwHvucFtOE71C9sA==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -962,6 +1068,10 @@ packages: electron-to-chromium@1.5.331: resolution: {integrity: sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==} + enhanced-resolve@5.20.1: + resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} + engines: {node: '>=10.13.0'} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -1031,6 +1141,9 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1476,6 +1589,13 @@ packages: resolution: {integrity: sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg==} engines: {node: '>=16.0.0'} + tailwindcss@4.2.2: + resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==} + + tapable@2.3.2: + resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} + engines: {node: '>=6'} + tiny-lru@11.4.7: resolution: {integrity: sha512-w/Te7uMUVeH0CR8vZIjr+XiN41V+30lkDdK+NRIDCUYKKuL9VcmaUEmaPISuwGhLlrTGh5yu18lENtR9axSxYw==} engines: {node: '>=12'} @@ -2169,6 +2289,74 @@ snapshots: estree-walker: 2.0.2 picomatch: 4.0.4 + '@tailwindcss/node@4.2.2': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.20.1 + jiti: 2.6.1 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.2 + + '@tailwindcss/oxide-android-arm64@4.2.2': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.2.2': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.2.2': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.2.2': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.2.2': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.2.2': + optional: true + + '@tailwindcss/oxide@4.2.2': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.2 + '@tailwindcss/oxide-darwin-arm64': 4.2.2 + '@tailwindcss/oxide-darwin-x64': 4.2.2 + '@tailwindcss/oxide-freebsd-x64': 4.2.2 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.2 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.2 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.2 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.2 + '@tailwindcss/oxide-linux-x64-musl': 4.2.2 + '@tailwindcss/oxide-wasm32-wasi': 4.2.2 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.2 + + '@tailwindcss/vite@4.2.2(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(yaml@2.8.3))': + dependencies: + '@tailwindcss/node': 4.2.2 + '@tailwindcss/oxide': 4.2.2 + tailwindcss: 4.2.2 + vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(yaml@2.8.3) + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -2231,6 +2419,8 @@ snapshots: css-what@6.2.2: {} + daisyui@5.5.19: {} + debug@4.4.3: dependencies: ms: 2.1.3 @@ -2259,6 +2449,11 @@ snapshots: electron-to-chromium@1.5.331: {} + enhanced-resolve@5.20.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.2 + entities@4.5.0: {} eolib@2.0.1: {} @@ -2344,6 +2539,8 @@ snapshots: dependencies: is-glob: 4.0.3 + graceful-fs@4.2.11: {} + has-flag@4.0.0: optional: true @@ -2768,6 +2965,10 @@ snapshots: sync-message-port@1.2.0: optional: true + tailwindcss@4.2.2: {} + + tapable@2.3.2: {} + tiny-lru@11.4.7: {} tinyglobby@0.2.15: diff --git a/src/css/style.css b/src/css/style.css index b06cad4b..4c1b0c2f 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -1,1048 +1,2 @@ -:root { - --eo-brown: #7b5a29; - --eo-font-color-light: #f0f0c8; -} - -@font-face { - font-family: w95fa; - src: url("/w95fa.woff2"); - font-weight: normal; - font-style: normal; -} - -body { - padding: 0; - margin: 0; - overflow: hidden; -} - -* { - /* biome-ignore lint/a11y/useGenericFontNames: Only custom font!! */ - font-family: w95fa !important; - -webkit-touch-callout: none; /* Disable “Save Image” / “Copy” popup */ - -webkit-user-select: none; /* Disable text selection */ - user-select: none; -} - -::selection { - background: #e4cdbc; - color: #000; -} - -@media (max-aspect-ratio: 4 / 3) { - #container { - height: 100vh; - width: calc(100vh * 4 / 3); - max-width: 100vw; - } -} - -#container { - position: relative; - width: 100vw; - height: calc(100vw * 3 / 4); - max-height: 100vh; -} - -@media (max-aspect-ratio: 4 / 3) { - #container { - height: 100vh; - } -} - -#game { - position: absolute; - top: 0; - display: block; - image-rendering: pixelated; - left: 50%; - transform: translateX(-50%); - z-index: 0; -} - -@media (min-aspect-ratio: 16 / 9) { - #container { - height: 100vh; - } - - #game { - transform: translate(-50%); - } -} - -#ui { - position: absolute; - display: flex; - width: 100%; - height: 100%; - z-index: 1; -} - -#dialogs { - display: flex; - gap: 1em; - width: 100%; - height: 100%; - justify-content: center; - align-items: center; - position: absolute; - top: 0; - z-index: 1010; -} - -.hidden { - display: none !important; -} - -@keyframes pulse { - 0% { - font-size: 1em; - rotate: -1deg; - } - - 50% { - font-size: 1.1em; - rotate: 2deg; - } - - 100% { - font-size: 1em; - rotate: -1deg; - } -} - -#cover { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.1); - /* optional dimming effect */ - z-index: 1000; - /* higher than game UI, lower than modal */ -} - -.title { - font-size: 13px; - color: #f0f0c8; -} - -.message { - font-size: 12px; - color: #fff; -} - -#btn-attack { - background-image: url("/attack.png"); -} - -#btn-toggle-sit { - background-image: url("/sit.png"); -} - -div.icon { - background: url("/gfx/gfx002/132.png"); - width: 13px; - height: 13px; -} - -div.icon[data-id="-1"] { - background: transparent; -} - -div.icon[data-id="1"] { - background-position-y: -13px; -} - -div.icon[data-id="2"] { - background-position-y: -26px; -} - -div.icon[data-id="3"] { - background-position-y: -39px; -} - -div.icon[data-id="4"] { - background-position-y: -52px; -} - -div.icon[data-id="5"] { - background-position-y: -65px; -} - -div.icon[data-id="6"] { - background-position-y: -78px; -} - -div.icon[data-id="7"] { - background-position-y: -91px; -} - -div.icon[data-id="8"] { - background-position-y: -104px; -} - -div.icon[data-id="9"] { - background-position-y: -117px; -} - -div.icon[data-id="10"] { - background-position-y: -130px; -} - -div.icon[data-id="11"] { - background-position-y: -143px; -} - -div.icon[data-id="12"] { - background-position-y: -156px; -} - -div.icon[data-id="13"] { - background-position-y: -169px; -} - -div.icon[data-id="14"] { - background-position-y: -182px; -} - -div.icon[data-id="15"] { - background-position-y: -195px; -} - -div.icon[data-id="16"] { - background-position-y: -208px; -} - -div.icon[data-id="17"] { - background-position-y: -221px; -} - -div.icon[data-id="18"] { - background-position-y: -234px; -} - -div.icon[data-id="19"] { - background-position-y: -247px; -} - -div.icon[data-id="20"] { - background-position-y: -260px; -} - -div.icon[data-id="21"] { - background-position-y: -273px; -} - -div.icon[data-id="22"] { - background-position-y: -286px; -} - -div.icon[data-id="23"] { - background-position-y: -299px; -} - -.img-label { - --bg-url: ""; - --bg-x: 0; - --bg-y: 0; - --width: 0; - --height: 0; - - display: block; - background-color: transparent; - /* biome-ignore lint/suspicious/noShorthandPropertyOverrides: CSS variables */ - background: var(--bg-url) var(--bg-x) var(--bg-y); - width: var(--width); - height: var(--height); - user-select: none; -} - -.img-label[data-id="account-name"] { - --bg-url: url("/gfx/gfx001/112.png"); - --bg-x: 0; - --bg-y: 0; - --width: 149px; - --height: 16px; -} - -.img-label[data-id="password"] { - --bg-url: url("/gfx/gfx001/112.png"); - --bg-x: 0; - --bg-y: -17px; - --width: 149px; - --height: 12px; -} - -.img-label[data-id="confirm-password"] { - --bg-url: url("/gfx/gfx001/112.png"); - --bg-x: 0; - --bg-y: -29px; - --width: 149px; - --height: 16px; -} - -.img-label[data-id="real-name"] { - --bg-url: url("/gfx/gfx001/112.png"); - --bg-x: 0; - --bg-y: -44px; - --width: 149px; - --height: 16px; -} - -.img-label[data-id="location"] { - --bg-url: url("/gfx/gfx001/112.png"); - --bg-x: 0; - --bg-y: -58px; - --width: 149px; - --height: 16px; -} - -.img-label[data-id="email"] { - --bg-url: url("/gfx/gfx001/112.png"); - --bg-x: 0; - --bg-y: -76px; - --width: 149px; - --height: 16px; -} - -.img-label[data-id="gender"] { - --bg-url: url("/gfx/gfx001/122.png"); - --bg-x: 0; - --bg-y: -38px; - --width: 23px; - --height: 18px; -} - -.img-label[data-id="hair-color"] { - --bg-url: url("/gfx/gfx001/122.png"); - --bg-x: 0; - --bg-y: 0; - --width: 23px; - --height: 18px; -} - -.img-label[data-id="hair-style"] { - --bg-url: url("/gfx/gfx001/122.png"); - --bg-x: 0; - --bg-y: -20px; - --width: 23px; - --height: 18px; -} - -.img-label[data-id="skin"] { - --bg-url: url("/gfx/gfx001/122.png"); - --bg-x: -46px; - --bg-y: -38px; - --width: 23px; - --height: 18px; -} - -.img-btn { - --bg-url: ""; - --bg-x: 0; - --bg-y: 0; - --hover-x: 0; - --hover-y: 0; - --width: 0; - --height: 0; - - background-color: transparent; - border: none; - outline: none; - /* biome-ignore lint/suspicious/noShorthandPropertyOverrides: CSS variables */ - background: var(--bg-url) var(--bg-x) var(--bg-y); - width: var(--width); - height: var(--height); - cursor: pointer; -} - -.img-btn:hover { - background-position: var(--hover-x) var(--hover-y); -} - -/* Specific buttons */ -.img-btn[data-id="create-account"] { - --bg-url: url("/gfx/gfx001/113.png"); - --bg-x: 0; - --bg-y: 0; - --hover-x: -180px; - --hover-y: 0; - --width: 180px; - --height: 40px; -} - -.img-btn[data-id="play-game"] { - --bg-url: url("/gfx/gfx001/113.png"); - --bg-x: 0; - --bg-y: -40px; - --hover-x: -180px; - --hover-y: -40px; - --width: 180px; - --height: 40px; -} - -.img-btn[data-id="view-credits"] { - --bg-url: url("/gfx/gfx001/113.png"); - --bg-x: 0; - --bg-y: -80px; - --hover-x: -180px; - --hover-y: -80px; - --width: 180px; - --height: 40px; -} - -.img-btn[data-id="create"] { - --bg-url: url("/gfx/gfx001/114.png"); - --bg-x: 0; - --bg-y: 0; - --hover-x: -120px; - --hover-y: 0; - --width: 120px; - --height: 40px; -} - -.img-btn[data-id="cancel-big"] { - --bg-url: url("/gfx/gfx001/114.png"); - --bg-x: 0; - --bg-y: -40px; - --hover-x: -120px; - --hover-y: -40px; - --width: 120px; - --height: 40px; -} - -.img-btn[data-id="connect-big"] { - --bg-url: url("/gfx/gfx001/114.png"); - --bg-x: 0; - --bg-y: -80px; - --hover-x: -120px; - --hover-y: -80px; - --width: 120px; - --height: 40px; -} - -.img-btn[data-id="password"] { - --bg-url: url("/gfx/gfx001/114.png"); - --bg-x: 0; - --bg-y: -120px; - --hover-x: -120px; - --hover-y: -120px; - --width: 120px; - --height: 40px; -} - -.img-btn[data-id="play-game"] { - --bg-url: url("/gfx/gfx001/113.png"); - --bg-x: 0; - --bg-y: -40px; - --hover-x: -180px; - --hover-y: -40px; - --width: 180px; - --height: 40px; -} - -.img-btn[data-id="connect"] { - --bg-url: url("/gfx/gfx001/115.png"); - --bg-x: 0; - --bg-y: 0; - --hover-x: -91px; - --hover-y: -0; - --width: 91px; - --height: 29px; -} - -.img-btn[data-id="cancel"] { - --bg-url: url("/gfx/gfx001/115.png"); - --bg-x: 0; - --bg-y: -29px; - --hover-x: -91px; - --hover-y: -29px; - --width: 91px; - --height: 29px; -} - -.img-btn[data-id="login"] { - --bg-url: url("/gfx/gfx001/115.png"); - --bg-x: 0; - --bg-y: -58px; - --hover-x: -91px; - --hover-y: -58px; - --width: 91px; - --height: 29px; -} - -.img-btn[data-id="delete"] { - --bg-url: url("/gfx/gfx001/115.png"); - --bg-x: 0; - --bg-y: -87px; - --hover-x: -91px; - --hover-y: -87px; - --width: 91px; - --height: 29px; -} - -.img-btn[data-id="ok"] { - --bg-url: url("/gfx/gfx001/115.png"); - --bg-x: 0; - --bg-y: -116px; - --hover-x: -91px; - --hover-y: -116px; - --width: 91px; - --height: 29px; -} - -.img-btn[data-id="back"] { - --bg-url: url("/gfx/gfx001/115.png"); - --bg-x: 0; - --bg-y: -145px; - --hover-x: -91px; - --hover-y: -145px; - --width: 91px; - --height: 29px; -} - -.img-btn[data-id="next"] { - --bg-url: url("/gfx/gfx001/115.png"); - --bg-x: 0; - --bg-y: -203px; - --hover-x: -91px; - --hover-y: -203px; - --width: 91px; - --height: 29px; -} - -.img-btn[data-id="add"] { - --bg-url: url("/gfx/gfx001/115.png"); - --bg-x: 0; - --bg-y: -174px; - --hover-x: -91px; - --hover-y: -174px; - --width: 91px; - --height: 29px; -} - -.img-btn[data-id="exit-game"] { - --bg-url: url("/gfx/gfx001/124.png"); - --bg-x: 0; - --bg-y: 0; - --hover-x: 0; - --hover-y: -53px; - --width: 51px; - --height: 53px; -} - -.img-btn[data-id="toggle-arrow"] { - background-color: #000; - --bg-url: url("/gfx/gfx001/122.png"); - --bg-x: -184px; - --bg-y: -39px; - --hover-x: -205px; - --hover-y: -39px; - --width: 21px; - --height: 18px; -} - -.img-btn[data-id="inventory"] { - background-color: #000; - --bg-url: url("/gfx/gfx002/125.png"); - --bg-x: 0; - --bg-y: 0; - --hover-x: -36px; - --hover-y: 0; - --width: 36px; - --height: 19px; -} - -.img-btn[data-id="map"] { - background-color: #000; - --bg-url: url("/gfx/gfx002/125.png"); - --bg-x: 0; - --bg-y: -19px; - --hover-x: -36px; - --hover-y: -19px; - --width: 36px; - --height: 19px; -} - -.img-btn[data-id="spells"] { - background-color: #000; - --bg-url: url("/gfx/gfx002/125.png"); - --bg-x: 0; - --bg-y: -38px; - --hover-x: -36px; - --hover-y: -38px; - --width: 36px; - --height: 19px; -} - -.img-btn[data-id="stats"] { - background-color: #000; - --bg-url: url("/gfx/gfx002/125.png"); - --bg-x: 0; - --bg-y: -95px; - --hover-x: -36px; - --hover-y: -95px; - --width: 36px; - --height: 19px; -} - -.img-btn[data-id="online"] { - background-color: #000; - --bg-url: url("/gfx/gfx002/125.png"); - --bg-x: 0; - --bg-y: -114px; - --hover-x: -36px; - --hover-y: -114px; - --width: 36px; - --height: 19px; -} - -.img-btn[data-id="party"] { - background-color: #000; - --bg-url: url("/gfx/gfx002/125.png"); - --bg-x: 0; - --bg-y: -133px; - --hover-x: -36px; - --hover-y: -133px; - --width: 36px; - --height: 19px; -} - -.img-btn[data-id="quest-select"] { - background-color: #000; - --bg-url: url("/gfx/gfx002/127.png"); - --bg-x: -303px; - --bg-y: -242px; - --hover-x: -303px; - --hover-y: -242px; - --width: 16px; - --height: 15px; -} - -.img-btn[data-id="upgrade-stat"] { - background-color: #000; - --bg-url: url("/gfx/gfx002/127.png"); - --bg-x: -215px; - --bg-y: -386px; - --hover-x: -234px; - --hover-y: -386px; - --width: 19px; - --height: 15px; -} - -.slider-container { - position: absolute; - width: 123px; - height: 16px; - background: transparent; - left: 24px; - top: 96px; -} - -.slider-thumb { - position: absolute; - top: 0; - width: 16px; - height: 15px; - background: url("/gfx/gfx002/129.png") 0 -75px; - touch-action: none; - user-select: none; -} - -.slider-thumb:hover { - background-position: 0 -90px; -} - -.hidden { - display: none !important; -} - -:root { - --ui-scale: 1; -} - -@media (max-width: 768px) { - :root { - --ui-scale: 0.8; - } - - .stat-label { - font-size: 10px; - } -} - -@media (max-width: 480px) { - :root { - --ui-scale: 0.6; - } - - .stat-label { - font-size: 9px; - } - - #hud { - top: 20px; - } -} - -.heart-sprite.low-health { - animation: pulse-sprite 1s ease-in-out infinite; -} - -@keyframes pulse-sprite { - 0%, - 100% { - opacity: 1; - } - - 50% { - opacity: 0.7; - } -} - -#shop { - position: relative; - user-select: none; - box-sizing: border-box; - width: 284px; - height: 290px; - background-color: #000; - background-image: url("/gfx/gfx002/152.png"); -} - -#shop .shop-name { - position: absolute; - color: #fff; - font-size: 12px; - left: 24px; - top: 18px; -} - -#shop .buttons { - position: absolute; - bottom: 10px; - left: 10px; - width: 260px; - display: flex; - gap: 3px; - justify-content: center; -} - -.scroll-handle { - position: absolute; - right: 16px; - width: 16px; - height: 15px; - background: url("/gfx/gfx002/129.png") 0 -75px; - touch-action: none; - user-select: none; -} - -#shop .item-list { - position: absolute; - top: 50px; - left: 22px; - width: 232px; - height: 193px; - display: flex; - flex-direction: column; - gap: 2px; - overflow-y: scroll; - overflow-x: hidden; - -ms-overflow-style: none; - scrollbar-width: none; -} - -#shop .item-list::-webkit-scrollbar { - display: none; -} - -.menu-item { - color: #fff; - font-size: 12px; - position: relative; - width: 224px; -} - -.menu-item::before { - content: ""; - position: absolute; - inset: 0; - background-color: rgba(255, 255, 255, 0.05); - opacity: 0; - pointer-events: none; - z-index: 0; - box-sizing: border-box; - left: -8px; -} - -.menu-item:hover::before { - opacity: 1; -} - -.menu-label { - position: absolute; - top: 6px; - left: 53px; -} - -.menu-item.item .menu-label, -.menu-item.item .menu-description { - left: 62px; -} - -.menu-description { - position: absolute; - top: 19px; - left: 53px; -} - -.link { - text-decoration: underline; -} - -.menu-item.item { - position: relative; - display: grid; - grid-template-columns: 48px auto; - align-items: center; - padding: 0 0.5rem; - cursor: pointer; - height: 38px; - background-image: url("/gfx/gfx003/100.png"); - background-repeat: no-repeat; - background-position: left center; - background-size: auto; - image-rendering: pixelated; - width: 210px; -} - -.menu-item.item .tooltip { - position: absolute; - top: 0; - left: 25%; - background-color: black; - color: white; - padding: 4px 8px; - border-radius: 4px; - font-size: 12px; - white-space: nowrap; - opacity: 0; - pointer-events: none; - z-index: 10; -} - -.menu-item.item img:hover ~ .tooltip { - opacity: 0.9; -} - -.menu-item-img { - max-width: 48px; - max-height: 48px; - object-fit: contain; - image-rendering: pixelated; - position: relative; - z-index: 1; - justify-self: center; -} - -.menu-item.text { - min-height: 12px; -} - -.menu-item-icon { - background-color: #000; - background-image: url("/gfx/gfx002/127.png"); - width: 31px; - height: 31px; -} - -.menu-item-icon[data-id="0"] { - background-position-y: -291px; -} - -.menu-item-icon[data-id="1"] { - background-position-y: -291px; - background-position-x: -31px; -} - -.menu-item-icon[data-id="2"] { - background-position-y: -291px; - background-position-x: -63px; -} - -.menu-item-icon[data-id="3"] { - background-position-y: -291px; - background-position-x: -94px; -} - -.menu-item-icon[data-id="4"] { - background-position-y: -291px; - background-position-x: -124px; -} - -.menu-item-icon[data-id="5"] { - background-position-y: -291px; - background-position-x: -155px; -} - -.menu-item-icon[data-id="20"] { - background-position-y: -353px; - background-position-x: -63px; -} - -.menu-item-icon[data-id="21"] { - background-position-y: -353px; - background-position-x: -94px; -} - -.notyf__wrapper { - background: #000; - opacity: 0.6; - padding: 0 3px !important; - font-size: 0.8rem; -} - -.notyf__toast--upper { - margin-bottom: 10px !important; -} - -.debug { - width: 100%; - height: auto; - position: static; -} - -.dialog-md { - position: relative; - background-color: #000; - background-image: url("/gfx/gfx002/152.png"); - color: #ddd; - width: 284px; - height: 290px; - user-select: none; -} - -.dialog-md .dialog-contents .player { - flex-direction: column; - width: 103px; - font-size: 12px; - color: #b4a08c; - align-items: center; - padding: 5px; - z-index: 1; -} - -.dialog-md .dialog-contents .player:hover { - position: relative; -} - -.dialog-md .dialog-contents .player:hover::before { - content: ""; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(123, 90, 41, 0.5); - pointer-events: none; - z-index: -1; -} - -.dialog-md .dialog-contents .player .nameplate { - display: flex; - flex-direction: row; -} - -.dialog-md .dialog-contents .player .nameplate .name { - text-transform: capitalize; - font-weight: bold; - color: #f0f0c8; - padding: 0 2px 0 2px; -} - -.dialog-md .dialog-contents .player .nameplate .guild, -.dialog-md .dialog-contents .player .nameplate .name, -.dialog-md .dialog-contents .player .nameplate .icon { - margin: 0 1px 0 1px; -} - -.dialog-md .dialog-contents .player .title { - font-style: italic; - text-align: center; -} - -.dialog-md .dialog-contents { - position: absolute; - top: 44px; - left: 15px; - width: 233px; - height: 198px; - display: flex; - flex-wrap: wrap; - justify-content: space-evenly; - gap: 6px; - overflow-y: scroll; - overflow-x: hidden; - -ms-overflow-style: none; - scrollbar-width: none; - touch-action: pan-y; -} - -.dialog-md .dialog-contents > div { - display: flex; - flex-direction: column; - align-items: center; -} - -.dialog-md .spell-name { - color: #fff; - font-size: 10px; - max-width: 32px; - text-align: center; -} - -.dialog-md .spell-level { - color: #b4a08c; - font-size: 9px; - position: relative; -} - -.dialog-md .spell-icon { - width: 34px; - height: 32px; -} - -.dialog-md .spell-icon:hover { - background-position-x: -34px; -} - -.dialog-md .spell-grid::-webkit-scrollbar { - display: none; -} - -.dialog-md .scroll-handle { - position: absolute; - right: 16px; - width: 16px; - height: 15px; - background: url("/gfx/gfx002/129.png") 0 -75px; - touch-action: none; - user-select: none; -} - -.dialog-md .label { - position: absolute; - top: 18px; - left: 24px; - color: #fff; - font-size: 12px; -} - -.dialog-md button[data-id="cancel"] { - position: absolute; - bottom: 10px; - left: 98px; -} +@import "tailwindcss"; +@plugin "daisyui"; diff --git a/vite.config.ts b/vite.config.ts index 407457e0..0402b487 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,9 +1,10 @@ import path from 'node:path'; import preact from '@preact/preset-vite'; +import tailwind from '@tailwindcss/vite'; import { defineConfig } from 'vite'; export default defineConfig({ - plugins: [preact()], + plugins: [preact(), tailwind()], resolve: { alias: { '@': path.resolve(__dirname, 'src'), From 7efb2a292b214d41e5ff1f17d4498b46c7f497e3 Mon Sep 17 00:00:00 2001 From: Richard Leek Date: Thu, 2 Apr 2026 18:26:55 -0400 Subject: [PATCH 004/103] Main menu and connection working --- index.html | 6 +-- src/bus.ts | 71 ++++++++++++++++++++++----- src/client/client.ts | 58 +++++++++++++++++----- src/client/events.ts | 2 + src/consts.ts | 4 +- src/controllers/alert-controller.ts | 15 ++++++ src/controllers/index.ts | 1 + src/css/style.css | 6 ++- src/game-state.ts | 2 +- src/handlers/init.ts | 51 +++++++++++++------ src/main.tsx | 4 +- src/ui/components/alert.tsx | 32 ++++++++++++ src/ui/components/backdrop.tsx | 11 +++++ src/ui/components/button.tsx | 60 ++++++++++++++++++++++ src/ui/components/index.ts | 4 ++ src/ui/components/input.tsx | 56 +++++++++++++++++++++ src/ui/containers/alert-container.tsx | 36 ++++++++++++++ src/ui/containers/index.ts | 1 + src/ui/context/client.tsx | 24 +++++++++ src/ui/context/index.ts | 2 + src/ui/context/locale.tsx | 35 +++++++++++++ src/ui/locale.ts | 14 ++++++ src/ui/main-menu.tsx | 40 +++++++++++++++ src/ui/ui.tsx | 25 +++++++++- 24 files changed, 508 insertions(+), 52 deletions(-) create mode 100644 src/controllers/alert-controller.ts create mode 100644 src/ui/components/alert.tsx create mode 100644 src/ui/components/backdrop.tsx create mode 100644 src/ui/components/button.tsx create mode 100644 src/ui/components/index.ts create mode 100644 src/ui/components/input.tsx create mode 100644 src/ui/containers/alert-container.tsx create mode 100644 src/ui/containers/index.ts create mode 100644 src/ui/context/client.tsx create mode 100644 src/ui/context/index.ts create mode 100644 src/ui/context/locale.tsx create mode 100644 src/ui/locale.ts create mode 100644 src/ui/main-menu.tsx diff --git a/index.html b/index.html index 9d4b52e3..efd6c3d2 100644 --- a/index.html +++ b/index.html @@ -14,9 +14,9 @@ -
- -
+
+ +
diff --git a/src/bus.ts b/src/bus.ts index 057fdeea..cf3e562e 100644 --- a/src/bus.ts +++ b/src/bus.ts @@ -14,7 +14,7 @@ import { swapMultiples, } from 'eolib'; export class PacketBus { - private socket: WebSocket; + private socket?: WebSocket; private sequencer: PacketSequencer; private encodeMultiple = 0; private decodeMultiple = 0; @@ -22,23 +22,68 @@ export class PacketBus { PacketFamily, Map void> > = new Map(); - constructor(socket: WebSocket) { - this.socket = socket; + + constructor() { this.sequencer = new PacketSequencer(SequenceStart.zero()); - this.socket.addEventListener('message', (e) => { - const promise = e.data.arrayBuffer(); - promise - .then((buf: ArrayBuffer) => { - this.handlePacket(new Uint8Array(buf)); - }) - .catch((err: Error) => { - console.error('Failed to get array buffer', err); + } + + async connect(url: string, onClose: () => void): Promise { + return new Promise((resolve, reject) => { + try { + this.socket = new WebSocket(url); + + this.socket.addEventListener('open', () => { + resolve(); + }); + + this.socket.addEventListener('close', () => { + if (this.socket) { + this.socket.close(); + this.socket = undefined; + } + onClose(); }); + + this.socket.addEventListener('error', (err) => { + if (this.socket) { + this.socket.close(); + this.socket = undefined; + } + reject(err); + }); + + setTimeout(() => { + if (!this.socket || this.socket.readyState !== WebSocket.OPEN) { + if (this.socket) { + this.socket.close(); + this.socket = undefined; + } + reject(new Error('Connection timed out')); + } + }, 5000); + + this.socket.addEventListener('message', (e) => { + const promise = e.data.arrayBuffer(); + promise + .then((buf: ArrayBuffer) => { + this.handlePacket(new Uint8Array(buf)); + }) + .catch((err: Error) => { + console.error('Failed to get array buffer', err); + }); + }); + } catch (err) { + reject(err); + return; + } }); } disconnect() { - this.socket.close(); + if (this.socket) { + this.socket.close(); + this.socket = undefined; + } } setSequence(sequence: SequenceStart) { @@ -113,7 +158,7 @@ export class PacketBus { const lengthBytes = encodeNumber(temp.length); const payload = new Uint8Array([lengthBytes[0], lengthBytes[1], ...temp]); - this.socket.send(payload); + this.socket?.send(payload); } registerPacketHandler( diff --git a/src/client/client.ts b/src/client/client.ts index 5f6b158d..913ac265 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -14,6 +14,7 @@ import { type Esf, type EsfRecord, type FileType, + InitInitClientPacket, type Item, type ItemMapInfo, MapType, @@ -32,11 +33,12 @@ import mitt, { type Emitter } from 'mitt'; import { Notyf } from 'notyf'; import { Application, Container } from 'pixi.js'; import { Atlas } from '@/atlas'; -import type { PacketBus } from '@/bus'; +import { PacketBus } from '@/bus'; import { clearRectangles } from '@/collision'; import { getDefaultConfig, loadConfig } from '@/config'; -import { HALF_TILE_HEIGHT, INITIAL_IDLE_TICKS } from '@/consts'; +import { HALF_TILE_HEIGHT, INITIAL_IDLE_TICKS, MAX_CHALLENGE } from '@/consts'; import { + AlertController, AnimationController, AudioController, AuthenticationController, @@ -68,7 +70,7 @@ import { ViewportController, } from '@/controllers'; import { getEcf, getEdf, getEif, getEmf, getEnf, getEsf } from '@/db'; -import { type DialogResourceID, type Edf, EOResourceID } from '@/edf'; +import { DialogResourceID, type Edf, EOResourceID } from '@/edf'; import { Sans11Font } from '@/fonts'; import { GameState } from '@/game-state'; import { registerAllHandlers } from '@/handlers'; @@ -92,6 +94,7 @@ import { getWeaponMetaData, HatMaskType, isoToScreen, + randomRange, screenToIso, } from '@/utils'; import type { Vector2 } from '@/vector'; @@ -108,7 +111,7 @@ export class Client { height = 600; zoom = 1; tickCount = 0; - bus!: PacketBus; + bus = new PacketBus(); config = getDefaultConfig(); version: Version; challenge: number; @@ -142,6 +145,7 @@ export class Client { warpMapId = 0; warpQueued = false; state = GameState.Initial; + postConnectState?: GameState; sessionId = 0; serverSettings: ServerSettings | null = null; motd = ''; @@ -184,6 +188,7 @@ export class Client { usageController: UsageController; tradeController: TradeController; guildController: GuildController; + alertController: AlertController; npcMetadata = getNpcMetaData(); weaponMetadata: Map = new Map(); shieldMetadata = getShieldMetaData(); @@ -220,6 +225,7 @@ export class Client { onlinePlayers: OnlinePlayer[] = []; constructor() { + registerAllHandlers(this); this.container = document.querySelector('#game-container')!; this.emitter = mitt(); this.version = new Version(); @@ -272,6 +278,7 @@ export class Client { this.drunkController = new DrunkController(this); this.inventoryController = new InventoryController(this); this.itemProtectionController = new ItemProtectionController(); + this.alertController = new AlertController(); this.lockerController = new LockerController(this); this.mapController = new MapController(this); this.mouseController = new MouseController(this); @@ -627,15 +634,6 @@ export class Client { this.emitter.on(event, handler); } - setBus(bus: PacketBus) { - this.bus = bus; - registerAllHandlers(this); - } - - clearBus() { - this.bus = null!; - } - setState(state: GameState) { this.state = state; @@ -668,6 +666,40 @@ export class Client { this.onlinePlayers = []; this.inventoryController.equipmentSwap = null; this.itemProtectionController.itemProtectionTimers.clear(); + this.emit('stateChanged', this.state); + } + + connect(nextState: GameState) { + this.postConnectState = nextState; + this.bus + .connect(this.config.host, () => this.handleConnectionClose()) + .then(() => { + this.beginHandshake(); + }) + .catch((err) => { + console.warn('Failed to connect to server', err); + const strings = this.getDialogStrings( + DialogResourceID.CONNECTION_SERVER_NOT_FOUND, + ); + this.alertController.show(strings[0], strings[1]); + this.postConnectState = undefined; + }); + } + + private handleConnectionClose() { + const strings = this.getDialogStrings( + DialogResourceID.CONNECTION_LOST_CONNECTION, + ); + this.alertController.show(strings[0], strings[1]); + this.setState(GameState.Initial); + } + + private beginHandshake() { + const packet = new InitInitClientPacket(); + packet.challenge = this.challenge = randomRange(1, MAX_CHALLENGE); + packet.hdid = String(Math.floor(Math.random() * 2147483647)); + packet.version = this.version; + this.bus.send(packet); } disconnect() { diff --git a/src/client/events.ts b/src/client/events.ts index add37d93..ebd3862b 100644 --- a/src/client/events.ts +++ b/src/client/events.ts @@ -12,10 +12,12 @@ import type { SkillLearn, ThreeItem, } from 'eolib'; +import type { GameState } from '@/game-state'; import type { SfxId } from '@/sfx'; import type { ChatIcon, ChatTab } from '@/ui'; export type ClientEvents = { + stateChanged: GameState; error: { title: string; message: string }; confirmation: { title: string; diff --git a/src/consts.ts b/src/consts.ts index e7686a1f..b4748495 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -1,6 +1,6 @@ export const GAME_FPS = 1000 / 20; -//export const HOST = 'ws://localhost:8077'; -export const HOST = 'wss://ws.reoserv.net'; +export const HOST = 'ws://localhost:8079'; +//export const HOST = 'wss://ws.reoserv.net'; export const TILE_WIDTH = 64; export const TILE_HEIGHT = 32; export const HALF_TILE_WIDTH = TILE_WIDTH >> 1; diff --git a/src/controllers/alert-controller.ts b/src/controllers/alert-controller.ts new file mode 100644 index 00000000..421746c2 --- /dev/null +++ b/src/controllers/alert-controller.ts @@ -0,0 +1,15 @@ +type AlertSubscriber = (title: string, message: string) => void; + +export class AlertController { + private subscribers: AlertSubscriber[] = []; + + subscribe(subscriber: AlertSubscriber) { + this.subscribers.push(subscriber); + } + + show(title: string, message: string) { + for (const subscriber of this.subscribers) { + subscriber(title, message); + } + } +} diff --git a/src/controllers/index.ts b/src/controllers/index.ts index c54973d9..50a0d1d7 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,3 +1,4 @@ +export { AlertController } from './alert-controller'; export { AnimationController } from './animation-controller'; export { AudioController } from './audio-controller'; export { AuthenticationController } from './authentication-controller'; diff --git a/src/css/style.css b/src/css/style.css index 4c1b0c2f..d097362f 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -1,2 +1,6 @@ @import "tailwindcss"; -@plugin "daisyui"; +@plugin "daisyui" { + themes: + dark --default, + light; +} diff --git a/src/game-state.ts b/src/game-state.ts index 8717a308..b1d2d10d 100644 --- a/src/game-state.ts +++ b/src/game-state.ts @@ -1,6 +1,6 @@ export enum GameState { Initial = 0, - Connected = 1, + CreateAccount = 1, Login = 2, LoggedIn = 3, InGame = 4, diff --git a/src/handlers/init.ts b/src/handlers/init.ts index 87acb2b4..4e2b97f2 100644 --- a/src/handlers/init.ts +++ b/src/handlers/init.ts @@ -110,28 +110,38 @@ function handleInitOk( client.playerId = data.playerId; // Hack to keep pre-game UI stable client.nearby.characters[0].playerId = data.playerId; - const bus = client.bus; - if (!bus) { - throw new Error('Bus is null'); - } - - bus.setEncryption( + client.bus.setEncryption( data.clientEncryptionMultiple, data.serverEncryptionMultiple, ); - bus.setSequence(InitSequenceStart.fromInitValues(data.seq1, data.seq2)); + client.bus.setSequence( + InitSequenceStart.fromInitValues(data.seq1, data.seq2), + ); const packet = new ConnectionAcceptClientPacket(); packet.clientEncryptionMultiple = data.clientEncryptionMultiple; packet.serverEncryptionMultiple = data.serverEncryptionMultiple; packet.playerId = data.playerId; - bus.send(packet); - client.setState(GameState.Connected); + client.bus.send(packet); - if (client.rememberMe && client.loginToken) { + if ( + client.postConnectState === GameState.Login && + client.rememberMe && + client.loginToken + ) { const writer = new EoWriter(); writer.addString(client.loginToken); - bus.sendBuf(PacketFamily.Login, PacketAction.Use, writer.toByteArray()); + client.bus.sendBuf( + PacketFamily.Login, + PacketAction.Use, + writer.toByteArray(), + ); + return; + } + + if (client.postConnectState) { + client.setState(client.postConnectState); + client.postConnectState = undefined; } } @@ -147,8 +157,14 @@ function handleInitOutOfDate( client: Client, data: InitInitServerPacket.ReplyCodeDataOutOfDate, ) { - client.version = data.version; - client.emit('reconnect', undefined); + const strings = client.getDialogStrings( + DialogResourceID.CONNECTION_CLIENT_OUT_OF_DATE, + ); + client.alertController.show( + strings[0], + `${strings[1]} ${data.version.major}.${data.version.minor}.${data.version.patch}`, + ); + client.disconnect(); } function handleInitBanned( @@ -159,14 +175,19 @@ function handleInitBanned( const text = client.getDialogStrings( DialogResourceID.CONNECTION_IP_BAN_PERM, ); - client.showError(text[1], text[0]); + client.alertController.show(text[0], text[1]); + client.disconnect(); return; } const banData = data.banTypeData as InitInitServerPacket.ReplyCodeDataBanned.BanTypeData0; const text = client.getDialogStrings(DialogResourceID.CONNECTION_IP_BAN_TEMP); - client.showError(`${text[0]} ${banData.minutesRemaining} minutes`, text[1]); + client.alertController.show( + `${text[0]} ${banData.minutesRemaining} minutes`, + text[1], + ); + client.disconnect(); } function handleInitFileEcf( diff --git a/src/main.tsx b/src/main.tsx index f66cc767..f1eec4a0 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -10,7 +10,7 @@ import { } from 'eolib'; import './css/style.css'; import 'notyf/notyf.min.css'; -import React from 'preact/compat'; +import { render } from 'preact'; import { Client } from '@/client'; import { Ui } from './ui'; @@ -64,5 +64,5 @@ window.addEventListener('DOMContentLoaded', async () => { character.equipment.hat = 0; client.nearby.characters = [character]; client.atlas.refresh(); - React.render(, document.getElementById('ui')!); + render(, document.getElementById('ui')!); }); diff --git a/src/ui/components/alert.tsx b/src/ui/components/alert.tsx new file mode 100644 index 00000000..ccbded86 --- /dev/null +++ b/src/ui/components/alert.tsx @@ -0,0 +1,32 @@ +import { useCallback } from 'preact/hooks'; +import { playSfxById, SfxId } from '@/sfx'; +import { useLocale } from '@/ui/context'; + +type AlertProps = { + title: string; + message: string; + onClose: () => void; +}; + +export function Alert({ title, message, onClose }: AlertProps) { + const { locale } = useLocale(); + + const onClick = useCallback(() => { + playSfxById(SfxId.ButtonClick); + onClose(); + }, [onClose]); + + return ( + + + + ); +} diff --git a/src/ui/components/backdrop.tsx b/src/ui/components/backdrop.tsx new file mode 100644 index 00000000..16bbb58b --- /dev/null +++ b/src/ui/components/backdrop.tsx @@ -0,0 +1,11 @@ +type BackdropProps = { + children: preact.ComponentChildren; +}; + +export function Backdrop({ children }: BackdropProps) { + return ( +
+ {children} +
+ ); +} diff --git a/src/ui/components/button.tsx b/src/ui/components/button.tsx new file mode 100644 index 00000000..57322692 --- /dev/null +++ b/src/ui/components/button.tsx @@ -0,0 +1,60 @@ +import { useCallback } from 'preact/hooks'; +import { playSfxById, SfxId } from '@/sfx'; + +type ButtonVariant = + | '' + | 'neutral' + | 'primary' + | 'secondary' + | 'accent' + | 'info' + | 'success' + | 'warning' + | 'error' + | 'outline' + | 'dash' + | 'soft' + | 'ghost' + | 'link' + | 'active' + | 'disabled' + | 'xs' + | 'sm' + | 'md' + | 'lg' + | 'xl' + | 'wide' + | 'block' + | 'square' + | 'circle'; + +type ButtonProps = { + children: preact.ComponentChildren; + label?: string; + variant?: ButtonVariant; + onClick?: () => void; +}; + +export function Button({ + children, + label, + variant = '', + onClick, +}: ButtonProps) { + const clickHandler = useCallback(() => { + playSfxById(SfxId.ButtonClick); + if (onClick) { + onClick(); + } + }, [onClick]); + + return ( + + ); +} diff --git a/src/ui/components/index.ts b/src/ui/components/index.ts new file mode 100644 index 00000000..e7cab67f --- /dev/null +++ b/src/ui/components/index.ts @@ -0,0 +1,4 @@ +export { Alert } from './alert'; +export { Backdrop } from './backdrop'; +export { Button } from './button'; +export { Input } from './input'; diff --git a/src/ui/components/input.tsx b/src/ui/components/input.tsx new file mode 100644 index 00000000..0f10c4bc --- /dev/null +++ b/src/ui/components/input.tsx @@ -0,0 +1,56 @@ +type InputVariant = + | '' + | 'ghost' + | 'neutral' + | 'primary' + | 'secondary' + | 'accent' + | 'info' + | 'success' + | 'warning' + | 'error' + | 'xs' + | 'sm' + | 'md' + | 'lg' + | 'xl'; + +type InputType = 'text' | 'password' | 'email' | 'number'; + +type InputProps = { + id: string; + value: string; + onChange: (value: string) => void; + variant?: InputVariant; + type?: InputType; + placeholder?: string; + min?: number; + max?: number; + required?: boolean; +}; + +export function Input({ + id, + value, + onChange, + type = 'text', + placeholder, + min, + max, + required, + variant = '', +}: InputProps) { + return ( + onChange((e.target as HTMLInputElement).value)} + placeholder={placeholder} + min={min} + max={max} + required={required} + /> + ); +} diff --git a/src/ui/containers/alert-container.tsx b/src/ui/containers/alert-container.tsx new file mode 100644 index 00000000..ce9f1bb2 --- /dev/null +++ b/src/ui/containers/alert-container.tsx @@ -0,0 +1,36 @@ +import { useEffect, useState } from 'preact/hooks'; +import { Alert, Backdrop } from '@/ui/components'; +import { useClient } from '@/ui/context'; + +type AlertContainerProps = { + children: preact.ComponentChildren; +}; + +export function AlertContainer({ children }: AlertContainerProps) { + const client = useClient(); + const [alert, setAlert] = useState<{ + title: string; + message: string; + } | null>(null); + + useEffect(() => { + client.alertController.subscribe((title, message) => { + setAlert({ title, message }); + }); + }, [client]); + + return ( + <> + {alert && ( + + setAlert(null)} + /> + + )} + {children} + + ); +} diff --git a/src/ui/containers/index.ts b/src/ui/containers/index.ts new file mode 100644 index 00000000..1ab40608 --- /dev/null +++ b/src/ui/containers/index.ts @@ -0,0 +1 @@ +export { AlertContainer } from './alert-container'; diff --git a/src/ui/context/client.tsx b/src/ui/context/client.tsx new file mode 100644 index 00000000..08793a9c --- /dev/null +++ b/src/ui/context/client.tsx @@ -0,0 +1,24 @@ +import { createContext } from 'preact'; +import { useContext } from 'preact/hooks'; +import type { Client } from '@/client'; + +export const ClientContext = createContext(null); + +type ClientProviderProps = { + client: Client; + children: preact.ComponentChildren; +}; + +export function ClientProvider({ client, children }: ClientProviderProps) { + return ( + {children} + ); +} + +export function useClient() { + const client = useContext(ClientContext); + if (!client) { + throw new Error('useClient must be used within a ClientProvider'); + } + return client; +} diff --git a/src/ui/context/index.ts b/src/ui/context/index.ts new file mode 100644 index 00000000..f8a9dbb3 --- /dev/null +++ b/src/ui/context/index.ts @@ -0,0 +1,2 @@ +export { ClientContext, ClientProvider, useClient } from './client'; +export { LocaleContext, LocaleProvider, useLocale } from './locale'; diff --git a/src/ui/context/locale.tsx b/src/ui/context/locale.tsx new file mode 100644 index 00000000..783f5279 --- /dev/null +++ b/src/ui/context/locale.tsx @@ -0,0 +1,35 @@ +import { createContext } from 'preact'; +import { useContext, useState } from 'preact/hooks'; +import { defaultLocale, type LocaleKey, locales } from '@/ui/locale'; + +type LocaleContextProps = { + localeKey: LocaleKey; + locale: (typeof locales)[LocaleKey]; + setLocaleKey: (localeKey: LocaleKey) => void; +}; + +export const LocaleContext = createContext(null); + +type LocaleProviderProps = { + children: preact.ComponentChildren; +}; + +export function LocaleProvider({ children }: LocaleProviderProps) { + const [localeKey, setLocaleKey] = useState(defaultLocale); + + return ( + + {children} + + ); +} + +export function useLocale() { + const localeContext = useContext(LocaleContext); + if (!localeContext) { + throw new Error('useLocale must be used within a LocaleProvider'); + } + return localeContext; +} diff --git a/src/ui/locale.ts b/src/ui/locale.ts new file mode 100644 index 00000000..4eebd69a --- /dev/null +++ b/src/ui/locale.ts @@ -0,0 +1,14 @@ +export const locales = { + en: { + btnOK: 'OK', + btnCreateAccount: 'Create Account', + btnPlayGame: 'Play Game', + btnViewCredits: 'View Credits', + logoAlt: 'Endless Online', + }, +} as const; + +export type LocaleKey = keyof typeof locales; +export type LocaleStrings = (typeof locales)[LocaleKey]; + +export const defaultLocale: LocaleKey = 'en'; diff --git a/src/ui/main-menu.tsx b/src/ui/main-menu.tsx new file mode 100644 index 00000000..5f23cf3d --- /dev/null +++ b/src/ui/main-menu.tsx @@ -0,0 +1,40 @@ +import { useCallback } from 'preact/hooks'; +import { GameState } from '@/game-state'; +import { Button } from '@/ui/components'; +import { useClient, useLocale } from '@/ui/context'; + +export function MainMenu() { + const client = useClient(); + const { locale } = useLocale(); + + const viewCredits = useCallback(() => { + window.open(client.config.creditsUrl, '_blank'); + }, [client.config.creditsUrl]); + + const createAccount = useCallback(() => { + if (client.state === GameState.Initial) { + client.connect(GameState.CreateAccount); + } else { + client.setState(GameState.CreateAccount); + } + }, [client]); + + const playGame = useCallback(() => { + if (client.state === GameState.Initial) { + client.connect(GameState.Login); + } else { + client.setState(GameState.Login); + } + }, [client]); + + return ( +
+ {locale.logoAlt} +
+ + + +
+
+ ); +} diff --git a/src/ui/ui.tsx b/src/ui/ui.tsx index 505253fe..25277c9a 100644 --- a/src/ui/ui.tsx +++ b/src/ui/ui.tsx @@ -1,5 +1,26 @@ +import { useMemo, useState } from 'preact/hooks'; import type { Client } from '@/client'; +import { GameState } from '@/game-state'; +import { AlertContainer } from '@/ui/containers'; +import { ClientProvider, LocaleProvider } from '@/ui/context'; +import { MainMenu } from '@/ui/main-menu'; -export function Ui({ client: _client }: { client: Client }) { - return

UI

; +export function Ui({ client }: { client: Client }) { + const [state, setState] = useState(client.state); + + useMemo(() => { + client.on('stateChanged', (newState) => { + setState(newState); + }); + }, [client]); + + return ( + + + + {state === GameState.Initial && } + + + + ); } From d3787ffbd7e4d8f45407f175908628ddca481db8 Mon Sep 17 00:00:00 2001 From: Richard Leek Date: Fri, 3 Apr 2026 11:00:25 -0400 Subject: [PATCH 005/103] Refactor UI state and authentication flow; add character select/login screens - Move authentication and character selection logic into context and controllers - Add new CharacterSelect, Login, and MainMenu UI containers with DaisyUI styling - Refactor Button, Input, and add Checkbox component for consistent UI/UX - Replace showError with alertController for error dialogs - Improve modal accessibility (focus restore, Escape to close) - Update connection/disconnect logic and GameState enum - Update locale strings for new UI --- index.html | 2 +- src/bus.ts | 1 + src/client/client.ts | 20 +-- src/consts.ts | 4 +- src/controllers/authentication-controller.ts | 152 ++++++++++++++++- src/controllers/keyboard-controller.ts | 3 +- src/game-state.ts | 9 +- src/handlers/account.ts | 6 +- src/handlers/character.ts | 6 +- src/handlers/init.ts | 13 +- src/handlers/login.ts | 52 +----- src/handlers/stat-skill.ts | 6 +- src/handlers/welcome.ts | 2 +- src/ui/components/alert.tsx | 12 +- src/ui/components/button.tsx | 14 +- src/ui/components/checkbox.tsx | 57 +++++++ src/ui/components/index.ts | 1 + src/ui/components/input.tsx | 40 ++++- src/ui/containers/alert-container.tsx | 25 ++- .../character-select/character-select.tsx | 34 ++++ .../containers/character-select/character.tsx | 156 ++++++++++++++++++ src/ui/containers/character-select/index.ts | 1 + src/ui/containers/index.ts | 3 + src/ui/containers/login.tsx | 69 ++++++++ src/ui/{ => containers}/main-menu.tsx | 0 src/ui/context/client.tsx | 48 +++++- src/ui/context/index.ts | 8 +- src/ui/locale.ts | 17 ++ src/ui/ui.tsx | 34 ++-- 29 files changed, 663 insertions(+), 132 deletions(-) create mode 100644 src/ui/components/checkbox.tsx create mode 100644 src/ui/containers/character-select/character-select.tsx create mode 100644 src/ui/containers/character-select/character.tsx create mode 100644 src/ui/containers/character-select/index.ts create mode 100644 src/ui/containers/login.tsx rename src/ui/{ => containers}/main-menu.tsx (100%) diff --git a/index.html b/index.html index efd6c3d2..4801112e 100644 --- a/index.html +++ b/index.html @@ -16,7 +16,7 @@
-
+
diff --git a/src/bus.ts b/src/bus.ts index cf3e562e..725e332d 100644 --- a/src/bus.ts +++ b/src/bus.ts @@ -80,6 +80,7 @@ export class PacketBus { } disconnect() { + this.sequencer = new PacketSequencer(SequenceStart.zero()); if (this.socket) { this.socket.close(); this.socket = undefined; diff --git a/src/client/client.ts b/src/client/client.ts index 913ac265..23735653 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -196,10 +196,6 @@ export class Client { hatMetadata = getHatMetadata(); typing = false; reconnecting = false; - rememberMe = Boolean(localStorage.getItem('remember-me')) || false; - loginToken = localStorage.getItem('login-token'); - lastCharacterId = - Number.parseInt(localStorage.getItem('last-character-id') ?? '', 10) || 0; edfs: { game1: Edf | null; game2: Edf | null; @@ -223,6 +219,7 @@ export class Client { minimapEnabled = false; minimapRenderer: MinimapRenderer; onlinePlayers: OnlinePlayer[] = []; + playerTriggeredDisconnect = false; constructor() { registerAllHandlers(this); @@ -687,6 +684,11 @@ export class Client { } private handleConnectionClose() { + if (this.playerTriggeredDisconnect) { + this.playerTriggeredDisconnect = false; + return; + } + const strings = this.getDialogStrings( DialogResourceID.CONNECTION_LOST_CONNECTION, ); @@ -703,20 +705,14 @@ export class Client { } disconnect() { + this.playerTriggeredDisconnect = true; this.setState(GameState.Initial); - this.clearSession(); + this.authenticationController.clearSession(); if (this.bus) { this.bus.disconnect(); } } - clearSession() { - this.loginToken = ''; - this.lastCharacterId = 0; - localStorage.removeItem('login-token'); - localStorage.removeItem('last-character-id'); - } - setStatusLabel(type: EOResourceID, text: string) { this.notyf.open({ message: `[ ${this.getResourceString(type)} ] ${text}`, diff --git a/src/consts.ts b/src/consts.ts index b4748495..a6d584a3 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -1,6 +1,6 @@ export const GAME_FPS = 1000 / 20; -export const HOST = 'ws://localhost:8079'; -//export const HOST = 'wss://ws.reoserv.net'; +//export const HOST = 'ws://localhost:8079'; +export const HOST = 'wss://ws.reoserv.net'; export const TILE_WIDTH = 64; export const TILE_HEIGHT = 32; export const HALF_TILE_WIDTH = TILE_WIDTH >> 1; diff --git a/src/controllers/authentication-controller.ts b/src/controllers/authentication-controller.ts index 1ecc334b..aec7b09e 100644 --- a/src/controllers/authentication-controller.ts +++ b/src/controllers/authentication-controller.ts @@ -1,16 +1,27 @@ -import type { Gender } from 'eolib'; import { AccountAgreeClientPacket, + AccountReply, AccountRequestClientPacket, + CharacterMapInfo, CharacterRemoveClientPacket, CharacterRequestClientPacket, + type CharacterSelectionListEntry, CharacterTakeClientPacket, + Direction, EoWriter, + EquipmentMapInfo, + type Gender, + LoginReply, LoginRequestClientPacket, + PacketAction, + PacketFamily, WelcomeRequestClientPacket, } from 'eolib'; import type { Client } from '@/client'; import { MAX_CHARACTER_NAME_LENGTH } from '@/consts'; +import { DialogResourceID } from '@/edf'; +import { GameState } from '@/game-state'; +import { playSfxById, SfxId } from '@/sfx'; type AccountCreateData = { username: string; @@ -28,15 +39,152 @@ type CharacterCreateData = { skin: number; }; +const LOGIN_REPLY_DIALOG_IDS: Partial> = { + [LoginReply.Banned]: DialogResourceID.LOGIN_BANNED_FROM_SERVER, + [LoginReply.Busy]: DialogResourceID.CONNECTION_SERVER_BUSY, + [LoginReply.LoggedIn]: DialogResourceID.LOGIN_ACCOUNT_ALREADY_LOGGED_ON, + [LoginReply.WrongUser]: + DialogResourceID.LOGIN_ACCOUNT_NAME_OR_PASSWORD_NOT_FOUND, + [LoginReply.WrongUserPassword]: + DialogResourceID.LOGIN_ACCOUNT_NAME_OR_PASSWORD_NOT_FOUND, +}; + +const ACCOUNT_REPLY_DIALOG_IDS: Partial< + Record +> = { + [AccountReply.ChangeFailed]: DialogResourceID.CHANGE_PASSWORD_MISMATCH, + [AccountReply.Changed]: DialogResourceID.CHANGE_PASSWORD_SUCCESS, + [AccountReply.Created]: DialogResourceID.ACCOUNT_CREATE_SUCCESS_WELCOME, + [AccountReply.Exists]: DialogResourceID.ACCOUNT_CREATE_NAME_EXISTS, + [AccountReply.NotApproved]: DialogResourceID.ACCOUNT_CREATE_NAME_NOT_APPROVED, + [AccountReply.RequestDenied]: DialogResourceID.LOGIN_SERVER_COULD_NOT_PROCESS, +}; + export class AuthenticationController { private client: Client; + private loginToken: string | null = localStorage.getItem('login-token'); + private lastCharacterId = + Number.parseInt(localStorage.getItem('last-character-id') ?? '', 10) || 0; accountCreateData: AccountCreateData | null = null; characterCreateData: CharacterCreateData | null = null; + private loginSubscribers: (( + characters: CharacterSelectionListEntry[], + ) => void)[] = []; + + private loginFailedSubscribers: (() => void)[] = []; + constructor(client: Client) { this.client = client; } + clearSession(): void { + this.loginToken = null; + this.lastCharacterId = 0; + localStorage.removeItem('login-token'); + localStorage.removeItem('last-character-id'); + } + + autoLogin(): boolean { + if (!this.loginToken) { + return false; + } + + const writer = new EoWriter(); + writer.addString(this.loginToken); + this.client.bus.sendBuf( + PacketFamily.Login, + PacketAction.Use, + writer.toByteArray(), + ); + return true; + } + + autoSelectCharacter(characters: CharacterSelectionListEntry[]): boolean { + if ( + !this.loginToken || + !characters.some((c) => c.id === this.lastCharacterId) + ) { + return false; + } + + const packet = new WelcomeRequestClientPacket(); + packet.characterId = this.lastCharacterId; + this.client.bus!.send(packet); + return true; + } + + notifyLoginReply(code: LoginReply): void { + for (const subscriber of this.loginFailedSubscribers) { + subscriber(); + } + + const dialogId = LOGIN_REPLY_DIALOG_IDS[code]; + if (!dialogId) { + console.warn(`No dialog mapped for login reply code: ${code}`); + return; + } + const strings = this.client.getDialogStrings(dialogId); + if (strings) { + this.client.alertController.show(strings[0], strings[1]); + } + } + + notifyAccountReply(code: AccountReply): void { + const dialogId = ACCOUNT_REPLY_DIALOG_IDS[code]; + if (!dialogId) { + console.warn(`No dialog mapped for account reply code: ${code}`); + return; + } + const strings = this.client.getDialogStrings(dialogId); + if (strings) { + this.client.alertController.show(strings[0], strings[1]); + } + } + + subscribeLogin( + subscriber: (characters: CharacterSelectionListEntry[]) => void, + ) { + this.loginSubscribers.push(subscriber); + } + + subscribeLoginFailed(subscriber: () => void) { + this.loginFailedSubscribers.push(subscriber); + } + + notifyLoggedIn(characters: CharacterSelectionListEntry[]): void { + this.client.nearby.characters = this.client.nearby.characters.filter( + (c) => c.playerId === this.client.playerId, + ); + this.client.nearby.characters.push( + ...characters.map((c, i) => { + const info = new CharacterMapInfo(); + info.playerId = this.client.playerId + i + 1; + info.name = c.name; + info.mapId = this.client.mapId; + info.direction = Direction.Down; + info.gender = c.gender; + info.hairStyle = c.hairStyle; + info.hairColor = c.hairColor; + info.skin = c.skin; + info.equipment = new EquipmentMapInfo(); + info.equipment.armor = c.equipment.armor; + info.equipment.weapon = c.equipment.weapon; + info.equipment.boots = c.equipment.boots; + info.equipment.shield = c.equipment.shield; + info.equipment.hat = c.equipment.hat; + return info; + }), + ); + this.client.atlas.refreshAsync().then(() => { + for (const subscriber of this.loginSubscribers) { + subscriber(characters); + } + playSfxById(SfxId.Login); + this.client.setState(GameState.LoggedIn); + }); + } + requestAccountCreation(data: AccountCreateData): void { this.accountCreateData = data; const packet = new AccountRequestClientPacket(); @@ -92,7 +240,7 @@ export class AuthenticationController { packet.characterId = characterId; this.client.bus!.send(packet); - this.client.lastCharacterId = characterId; + this.lastCharacterId = characterId; localStorage.setItem('last-character-id', `${characterId}`); } diff --git a/src/controllers/keyboard-controller.ts b/src/controllers/keyboard-controller.ts index f216d41c..a82f7079 100644 --- a/src/controllers/keyboard-controller.ts +++ b/src/controllers/keyboard-controller.ts @@ -174,7 +174,8 @@ export class KeyboardController { break; case 'Tab': this.updateInputHeld(Input.Tab, true); - if (!this.client.typing) e.preventDefault(); + // TODO: Fix tab capturing later + //if (!this.client.typing) e.preventDefault(); break; case 'KeyR': this.updateInputHeld(Input.Refresh, true); diff --git a/src/game-state.ts b/src/game-state.ts index b1d2d10d..441b4602 100644 --- a/src/game-state.ts +++ b/src/game-state.ts @@ -1,9 +1,10 @@ export enum GameState { Initial = 0, - CreateAccount = 1, - Login = 2, - LoggedIn = 3, - InGame = 4, + Connected = 1, + CreateAccount = 2, + Login = 3, + LoggedIn = 4, + InGame = 5, } export enum SpellTarget { diff --git a/src/handlers/account.ts b/src/handlers/account.ts index 6b62e8c8..775c21c9 100644 --- a/src/handlers/account.ts +++ b/src/handlers/account.ts @@ -17,21 +17,21 @@ function handleAccountReply(client: Client, reader: EoReader) { const text = client.getDialogStrings( DialogResourceID.ACCOUNT_CREATE_NAME_EXISTS, ); - client.showError(text[1], text[0]); + client.alertController.show(text[0], text[1]); return; } case AccountReply.NotApproved: { const text = client.getDialogStrings( DialogResourceID.ACCOUNT_CREATE_NAME_NOT_APPROVED, ); - client.showError(text[1], text[0]); + client.alertController.show(text[0], text[1]); return; } case AccountReply.ChangeFailed: { const text = client.getDialogStrings( DialogResourceID.CHANGE_PASSWORD_MISMATCH, ); - client.showError(text[1], text[0]); + client.alertController.show(text[0], text[1]); return; } case AccountReply.Changed: diff --git a/src/handlers/character.ts b/src/handlers/character.ts index 083e5b07..0f3e33a6 100644 --- a/src/handlers/character.ts +++ b/src/handlers/character.ts @@ -18,14 +18,14 @@ function handleCharacterReply(client: Client, reader: EoReader) { const text = client.getDialogStrings( DialogResourceID.CHARACTER_CREATE_NAME_EXISTS, ); - client.showError(text[1], text[0]); + client.alertController.show(text[0], text[1]); return; } case CharacterReply.NotApproved: { const text = client.getDialogStrings( DialogResourceID.CHARACTER_CREATE_NAME_NOT_APPROVED, ); - client.showError(text[1], text[0]); + client.alertController.show(text[0], text[1]); return; } case CharacterReply.Full: @@ -33,7 +33,7 @@ function handleCharacterReply(client: Client, reader: EoReader) { const text = client.getDialogStrings( DialogResourceID.CHARACTER_CREATE_TOO_MANY_CHARS, ); - client.showError(text[1], text[0]); + client.alertController.show(text[0], text[1]); } return; case CharacterReply.Deleted: { diff --git a/src/handlers/init.ts b/src/handlers/init.ts index 4e2b97f2..c12dc992 100644 --- a/src/handlers/init.ts +++ b/src/handlers/init.ts @@ -5,7 +5,6 @@ import { Emf, Enf, EoReader, - EoWriter, Esf, InitBanType, InitInitServerPacket, @@ -102,7 +101,7 @@ function handleInitOk( const text = client.getDialogStrings( DialogResourceID.CONNECTION_LOST_CONNECTION, ); - client.showError(text[1], text[0]); + client.alertController.show(text[0], text[1]); client.disconnect(); return; } @@ -126,16 +125,8 @@ function handleInitOk( if ( client.postConnectState === GameState.Login && - client.rememberMe && - client.loginToken + client.authenticationController.autoLogin() ) { - const writer = new EoWriter(); - writer.addString(client.loginToken); - client.bus.sendBuf( - PacketFamily.Login, - PacketAction.Use, - writer.toByteArray(), - ); return; } diff --git a/src/handlers/login.ts b/src/handlers/login.ts index 23fdfad1..d1838a5e 100644 --- a/src/handlers/login.ts +++ b/src/handlers/login.ts @@ -4,71 +4,27 @@ import { LoginReplyServerPacket, PacketAction, PacketFamily, - WelcomeRequestClientPacket, } from 'eolib'; import type { Client } from '@/client'; -import { DialogResourceID } from '@/edf'; -import { GameState } from '@/game-state'; function handleLoginReply(client: Client, reader: EoReader) { const packet = LoginReplyServerPacket.deserialize(reader); - if (packet.replyCode === LoginReply.Banned) { - client.clearSession(); - const text = client.getDialogStrings( - DialogResourceID.LOGIN_BANNED_FROM_SERVER, - ); - client.showError(text[1], text[0]); + if (packet.replyCode !== LoginReply.Ok) { + client.authenticationController.notifyLoginReply(packet.replyCode); return; } - if (packet.replyCode === LoginReply.LoggedIn) { - client.clearSession(); - const text = client.getDialogStrings( - DialogResourceID.LOGIN_ACCOUNT_ALREADY_LOGGED_ON, - ); - client.showError(text[1], text[0]); - return; - } - - if ( - packet.replyCode === LoginReply.WrongUser || - packet.replyCode === LoginReply.WrongUserPassword - ) { - client.clearSession(); - const text = client.getDialogStrings( - DialogResourceID.LOGIN_ACCOUNT_NAME_OR_PASSWORD_NOT_FOUND, - ); - client.showError(text[1], text[0]); - return; - } - - if (packet.replyCode === LoginReply.Busy) { - const text = client.getDialogStrings( - DialogResourceID.CONNECTION_SERVER_BUSY, - ); - client.showError(text[1], text[0]); - } - if (reader.remaining > 0) { const token = reader.getFixedString(reader.remaining); localStorage.setItem('login-token', token); } const data = packet.replyCodeData as LoginReplyServerPacket.ReplyCodeDataOk; - client.setState(GameState.LoggedIn); - - if ( - client.rememberMe && - client.loginToken && - data.characters.some((c) => c.id === client.lastCharacterId) - ) { - const packet = new WelcomeRequestClientPacket(); - packet.characterId = client.lastCharacterId; - client.bus!.send(packet); + if (client.authenticationController.autoSelectCharacter(data.characters)) { return; } - client.emit('login', data.characters); + client.authenticationController.notifyLoggedIn(data.characters); } export function registerLoginHandlers(client: Client) { diff --git a/src/handlers/stat-skill.ts b/src/handlers/stat-skill.ts index aa8629fa..4418b5ff 100644 --- a/src/handlers/stat-skill.ts +++ b/src/handlers/stat-skill.ts @@ -53,7 +53,7 @@ function handleStatSkillReply(client: Client, reader: EoReader) { const strings = client.getDialogStrings( DialogResourceID.SKILL_RESET_CHARACTER_CLEAR_PAPERDOLL, ); - client.showError(strings[1], strings[0]); + client.alertController.show(strings[0], strings[1]); return; } case SkillMasterReply.WrongClass: { @@ -97,7 +97,7 @@ function handleStatSkillRemove(client: Client, reader: EoReader) { const strings = client.getDialogStrings( DialogResourceID.SKILL_FORGET_SUCCESS, ); - client.showError(strings[1], strings[0]); + client.alertController.show(strings[0], strings[1]); client.emit('skillsChanged', undefined); } @@ -129,7 +129,7 @@ function handleStatSkillJunk(client: Client, reader: EoReader) { const strings = client.getDialogStrings( DialogResourceID.SKILL_RESET_CHARACTER_COMPLETE, ); - client.showError(strings[1], strings[0]); + client.alertController.show(strings[0], strings[1]); } export function registerStatSkillHandlers(client: Client) { diff --git a/src/handlers/welcome.ts b/src/handlers/welcome.ts index 3860dffb..0a314073 100644 --- a/src/handlers/welcome.ts +++ b/src/handlers/welcome.ts @@ -19,7 +19,7 @@ function handleWelcomeReply(client: Client, reader: EoReader) { const text = client.getDialogStrings( DialogResourceID.CONNECTION_SERVER_BUSY, ); - client.showError(text[1], text[0]); + client.alertController.show(text[0], text[1]); return; } diff --git a/src/ui/components/alert.tsx b/src/ui/components/alert.tsx index ccbded86..395b11b2 100644 --- a/src/ui/components/alert.tsx +++ b/src/ui/components/alert.tsx @@ -1,6 +1,5 @@ -import { useCallback } from 'preact/hooks'; -import { playSfxById, SfxId } from '@/sfx'; import { useLocale } from '@/ui/context'; +import { Button } from './button'; type AlertProps = { title: string; @@ -11,20 +10,15 @@ type AlertProps = { export function Alert({ title, message, onClose }: AlertProps) { const { locale } = useLocale(); - const onClick = useCallback(() => { - playSfxById(SfxId.ButtonClick); - onClose(); - }, [onClose]); - return ( diff --git a/src/ui/components/button.tsx b/src/ui/components/button.tsx index 57322692..d60a90d7 100644 --- a/src/ui/components/button.tsx +++ b/src/ui/components/button.tsx @@ -1,6 +1,8 @@ import { useCallback } from 'preact/hooks'; import { playSfxById, SfxId } from '@/sfx'; +type ButtonType = 'button' | 'submit'; + type ButtonVariant = | '' | 'neutral' @@ -30,8 +32,9 @@ type ButtonVariant = type ButtonProps = { children: preact.ComponentChildren; + type?: ButtonType; label?: string; - variant?: ButtonVariant; + variant?: ButtonVariant | ButtonVariant[]; onClick?: () => void; }; @@ -39,6 +42,7 @@ export function Button({ children, label, variant = '', + type = 'button', onClick, }: ButtonProps) { const clickHandler = useCallback(() => { @@ -48,9 +52,15 @@ export function Button({ } }, [onClick]); + const variantClasses = (Array.isArray(variant) ? variant : [variant]) + .filter(Boolean) + .map((v) => `btn-${v}`) + .join(' '); + return ( + + +
+ + + ); +} diff --git a/src/ui/containers/character-select/character.tsx b/src/ui/containers/character-select/character.tsx new file mode 100644 index 00000000..79ae47cc --- /dev/null +++ b/src/ui/containers/character-select/character.tsx @@ -0,0 +1,156 @@ +import { AdminLevel, type CharacterSelectionListEntry } from 'eolib'; +import { useMemo, useState } from 'preact/hooks'; +import { CharacterFrame } from '@/atlas'; +import { CHARACTER_HEIGHT, CHARACTER_WIDTH } from '@/consts'; +import { Button } from '@/ui/components'; +import { useClient, useLocale } from '@/ui/context'; + +const CANVAS_W = CHARACTER_WIDTH + 40; +const CANVAS_H = CHARACTER_HEIGHT + 40; + +type AdminBadgeColor = + | 'badge-ghost' + | 'badge-info' + | 'badge-success' + | 'badge-warning' + | 'badge-error'; + +type AdminBadgeInfo = { + label: keyof import('@/ui/locale').LocaleStrings; + color: AdminBadgeColor; +}; + +const ADMIN_BADGES: Partial> = { + [AdminLevel.Spy]: { label: 'adminBadgeSpy', color: 'badge-ghost' }, + [AdminLevel.LightGuide]: { + label: 'adminBadgeLightGuide', + color: 'badge-info', + }, + [AdminLevel.Guardian]: { + label: 'adminBadgeGuardian', + color: 'badge-success', + }, + [AdminLevel.GameMaster]: { + label: 'adminBadgeGameMaster', + color: 'badge-warning', + }, + [AdminLevel.HighGameMaster]: { + label: 'adminBadgeHighGameMaster', + color: 'badge-error', + }, +}; + +type CharacterProps = { + character: CharacterSelectionListEntry | undefined; +}; + +export function Character({ character }: CharacterProps) { + const client = useClient(); + const { locale } = useLocale(); + const [previewUrl, setPreviewUrl] = useState(); + + useMemo(() => { + if (!character) { + return; + } + + const canvas = document.createElement('canvas'); + canvas.width = CANVAS_W; + canvas.height = CANVAS_H; + const ctx = canvas.getContext('2d'); + if (!ctx) { + return; + } + + const playerId = client.nearby.characters.find( + (c) => c.name === character.name, + )?.playerId; + if (!playerId) { + return; + } + + const frame = client.atlas.getCharacterFrame( + playerId, + CharacterFrame.StandingDownRight, + ); + if (!frame) { + return; + } + + const atlas = client.atlas.getAtlas(frame.atlasIndex); + if (!atlas) { + return; + } + + ctx.drawImage( + atlas, + frame.x, + frame.y, + frame.w, + frame.h, + Math.floor((canvas.width >> 1) - (frame.w >> 1)), + Math.floor((canvas.height >> 1) - (frame.h >> 1)), + frame.w, + frame.h, + ); + + setPreviewUrl(canvas.toDataURL()); + }, [client, character]); + + if (!character) { + return ( +
+
+
+
+
+
+ {locale.characterEmptySlot} +
+
+ ); + } + + const adminBadge = ADMIN_BADGES[character.admin as AdminLevel]; + + return ( +
+
+ {character.name} + Lvl {character.level} + {adminBadge && ( + + {locale[adminBadge.label]} + + )} +
+
+ {previewUrl ? ( + {character.name} + ) : ( +
+ )} +
+
+ + +
+
+ ); +} diff --git a/src/ui/containers/character-select/index.ts b/src/ui/containers/character-select/index.ts new file mode 100644 index 00000000..6829482d --- /dev/null +++ b/src/ui/containers/character-select/index.ts @@ -0,0 +1 @@ +export { CharacterSelect } from './character-select'; diff --git a/src/ui/containers/index.ts b/src/ui/containers/index.ts index 1ab40608..0138f417 100644 --- a/src/ui/containers/index.ts +++ b/src/ui/containers/index.ts @@ -1 +1,4 @@ export { AlertContainer } from './alert-container'; +export { CharacterSelect } from './character-select'; +export { Login } from './login'; +export { MainMenu } from './main-menu'; diff --git a/src/ui/containers/login.tsx b/src/ui/containers/login.tsx new file mode 100644 index 00000000..5a2d5412 --- /dev/null +++ b/src/ui/containers/login.tsx @@ -0,0 +1,69 @@ +import { useCallback, useMemo, useState } from 'preact/hooks'; +import { GameState } from '@/game-state'; +import { Button, Checkbox, Input } from '@/ui/components'; +import { useClient, useLocale } from '@/ui/context'; + +export function Login() { + const client = useClient(); + const { locale } = useLocale(); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [rememberMe, setRememberMe] = useState( + localStorage.getItem('login-token') !== null, + ); + + useMemo(() => { + client.authenticationController.subscribeLoginFailed(() => { + setPassword(''); + }); + }, [client]); + + const onSubmit = useCallback( + (e: SubmitEvent) => { + client.authenticationController.login(username, password, rememberMe); + e.preventDefault(); + }, + [username, password, rememberMe, client], + ); + + const cancel = useCallback(() => { + client.setState(GameState.Connected); + }, [client]); + + return ( +
+
+
{locale.loginTitle}
+
+ setUsername(val)} + autofocus + /> + setPassword(val)} + /> + +
+ + +
+ +
+
+ ); +} diff --git a/src/ui/main-menu.tsx b/src/ui/containers/main-menu.tsx similarity index 100% rename from src/ui/main-menu.tsx rename to src/ui/containers/main-menu.tsx diff --git a/src/ui/context/client.tsx b/src/ui/context/client.tsx index 08793a9c..f7b53047 100644 --- a/src/ui/context/client.tsx +++ b/src/ui/context/client.tsx @@ -1,8 +1,16 @@ +import type { CharacterSelectionListEntry } from 'eolib'; import { createContext } from 'preact'; -import { useContext } from 'preact/hooks'; +import { useContext, useMemo, useState } from 'preact/hooks'; import type { Client } from '@/client'; +import type { GameState } from '@/game-state'; -export const ClientContext = createContext(null); +type ClientContextProps = { + client: Client; + gameState: GameState; + characters: CharacterSelectionListEntry[]; +}; + +export const ClientContext = createContext(null); type ClientProviderProps = { client: Client; @@ -10,15 +18,39 @@ type ClientProviderProps = { }; export function ClientProvider({ client, children }: ClientProviderProps) { + const [gameState, setGameState] = useState(client.state); + const [characters, setCharacters] = useState( + [], + ); + + useMemo(() => { + client.on('stateChanged', setGameState); + client.authenticationController.subscribeLogin(setCharacters); + }, [client]); + return ( - {children} + + {children} + ); } -export function useClient() { - const client = useContext(ClientContext); - if (!client) { - throw new Error('useClient must be used within a ClientProvider'); +function useClientContext() { + const ctx = useContext(ClientContext); + if (!ctx) { + throw new Error('Must be used within a ClientProvider'); } - return client; + return ctx; +} + +export function useClient() { + return useClientContext().client; +} + +export function useGameState() { + return useClientContext().gameState; +} + +export function useCharacters() { + return useClientContext().characters; } diff --git a/src/ui/context/index.ts b/src/ui/context/index.ts index f8a9dbb3..33c32aab 100644 --- a/src/ui/context/index.ts +++ b/src/ui/context/index.ts @@ -1,2 +1,8 @@ -export { ClientContext, ClientProvider, useClient } from './client'; +export { + ClientContext, + ClientProvider, + useCharacters, + useClient, + useGameState, +} from './client'; export { LocaleContext, LocaleProvider, useLocale } from './locale'; diff --git a/src/ui/locale.ts b/src/ui/locale.ts index 4eebd69a..f5d399ad 100644 --- a/src/ui/locale.ts +++ b/src/ui/locale.ts @@ -1,10 +1,27 @@ export const locales = { en: { btnOK: 'OK', + btnCancel: 'Cancel', btnCreateAccount: 'Create Account', btnPlayGame: 'Play Game', btnViewCredits: 'View Credits', logoAlt: 'Endless Online', + loginTitle: 'Login to Your Account', + loginUsername: 'Username', + loginPassword: 'Password', + loginRemember: 'Remember Me', + btnLoginConnect: 'Connect', + characterSelectTitle: 'Select Your Character', + btnNewCharacter: 'New Character', + btnChangePassword: 'Change Password', + adminBadgeSpy: 'SPY', + adminBadgeLightGuide: 'GUIDE', + adminBadgeGuardian: 'GUARD', + adminBadgeGameMaster: 'GM', + adminBadgeHighGameMaster: 'HGM', + characterEmptySlot: 'Empty Slot', + btnLogin: 'Login', + btnDeleteCharacter: 'Delete', }, } as const; diff --git a/src/ui/ui.tsx b/src/ui/ui.tsx index 25277c9a..1463d1ae 100644 --- a/src/ui/ui.tsx +++ b/src/ui/ui.tsx @@ -1,25 +1,31 @@ -import { useMemo, useState } from 'preact/hooks'; import type { Client } from '@/client'; import { GameState } from '@/game-state'; -import { AlertContainer } from '@/ui/containers'; -import { ClientProvider, LocaleProvider } from '@/ui/context'; -import { MainMenu } from '@/ui/main-menu'; +import { + AlertContainer, + CharacterSelect, + Login, + MainMenu, +} from '@/ui/containers'; +import { ClientProvider, LocaleProvider, useGameState } from '@/ui/context'; -export function Ui({ client }: { client: Client }) { - const [state, setState] = useState(client.state); +function UiContent() { + const gameState = useGameState(); - useMemo(() => { - client.on('stateChanged', (newState) => { - setState(newState); - }); - }, [client]); + return ( + + {(gameState === GameState.Initial || + gameState === GameState.Connected) && } + {gameState === GameState.Login && } + {gameState === GameState.LoggedIn && } + + ); +} +export function Ui({ client }: { client: Client }) { return ( - - {state === GameState.Initial && } - + ); From d089e30b0e7ff2dd7585932762f7a65eb6cd3d6a Mon Sep 17 00:00:00 2001 From: Richard Leek Date: Fri, 3 Apr 2026 11:03:35 -0400 Subject: [PATCH 006/103] Wire up change password button --- src/controllers/authentication-controller.ts | 2 +- src/game-state.ts | 5 +++-- src/ui/containers/change-password.tsx | 3 +++ src/ui/containers/character-select/character-select.tsx | 9 ++++++++- src/ui/containers/index.ts | 1 + src/ui/ui.tsx | 4 +++- 6 files changed, 19 insertions(+), 5 deletions(-) create mode 100644 src/ui/containers/change-password.tsx diff --git a/src/controllers/authentication-controller.ts b/src/controllers/authentication-controller.ts index aec7b09e..f1fabef8 100644 --- a/src/controllers/authentication-controller.ts +++ b/src/controllers/authentication-controller.ts @@ -181,7 +181,7 @@ export class AuthenticationController { subscriber(characters); } playSfxById(SfxId.Login); - this.client.setState(GameState.LoggedIn); + this.client.setState(GameState.CharacterSelect); }); } diff --git a/src/game-state.ts b/src/game-state.ts index 441b4602..7d013786 100644 --- a/src/game-state.ts +++ b/src/game-state.ts @@ -3,8 +3,9 @@ export enum GameState { Connected = 1, CreateAccount = 2, Login = 3, - LoggedIn = 4, - InGame = 5, + CharacterSelect = 4, + ChangePassword = 5, + InGame = 6, } export enum SpellTarget { diff --git a/src/ui/containers/change-password.tsx b/src/ui/containers/change-password.tsx new file mode 100644 index 00000000..fb3da319 --- /dev/null +++ b/src/ui/containers/change-password.tsx @@ -0,0 +1,3 @@ +export function ChangePassword() { + return 'Change Password'; +} diff --git a/src/ui/containers/character-select/character-select.tsx b/src/ui/containers/character-select/character-select.tsx index 87d8a0cd..0cd47092 100644 --- a/src/ui/containers/character-select/character-select.tsx +++ b/src/ui/containers/character-select/character-select.tsx @@ -1,4 +1,5 @@ import { useCallback } from 'preact/hooks'; +import { GameState } from '@/game-state'; import { Button } from '@/ui/components'; import { useCharacters, useClient, useLocale } from '@/ui/context'; import { Character } from './character'; @@ -12,6 +13,10 @@ export function CharacterSelect() { client.disconnect(); }, [client]); + const changePassword = useCallback(() => { + client.setState(GameState.ChangePassword); + }, [client]); + return (
@@ -23,7 +28,9 @@ export function CharacterSelect() {
- + diff --git a/src/ui/containers/index.ts b/src/ui/containers/index.ts index 0138f417..3812b16f 100644 --- a/src/ui/containers/index.ts +++ b/src/ui/containers/index.ts @@ -1,4 +1,5 @@ export { AlertContainer } from './alert-container'; +export { ChangePassword } from './change-password'; export { CharacterSelect } from './character-select'; export { Login } from './login'; export { MainMenu } from './main-menu'; diff --git a/src/ui/ui.tsx b/src/ui/ui.tsx index 1463d1ae..d74e40bf 100644 --- a/src/ui/ui.tsx +++ b/src/ui/ui.tsx @@ -2,6 +2,7 @@ import type { Client } from '@/client'; import { GameState } from '@/game-state'; import { AlertContainer, + ChangePassword, CharacterSelect, Login, MainMenu, @@ -16,7 +17,8 @@ function UiContent() { {(gameState === GameState.Initial || gameState === GameState.Connected) && } {gameState === GameState.Login && } - {gameState === GameState.LoggedIn && } + {gameState === GameState.CharacterSelect && } + {gameState === GameState.ChangePassword && } ); } From b0a6785118c8cd22bf5732d60a9b0dc022226cf9 Mon Sep 17 00:00:00 2001 From: Richard Leek Date: Fri, 3 Apr 2026 12:25:02 -0400 Subject: [PATCH 007/103] Add copilot instructions --- .github/copilot-instructions.md | 167 +++ .github/daisyui-llms.txt | 1874 +++++++++++++++++++++++++++++++ 2 files changed, 2041 insertions(+) create mode 100644 .github/copilot-instructions.md create mode 100644 .github/daisyui-llms.txt diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..bb49be2d --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,167 @@ +# EOWeb – Copilot Instructions + +EOWeb is a browser-based client for Endless Online (classic MMORPG, 0.0.28 protocol). It connects to game servers over WebSocket and renders the world using Pixi.js, with a Preact UI for menus/dialogs. + +## Commands + +```sh +pnpm dev # Vite dev server (port 3000) +pnpm build # tsc && vite build → dist/ +pnpm lint # biome check . +pnpm format # biome check --write . +``` + +There is no test suite. + +## Architecture + +The app has two rendering layers that serve different purposes: + +- **Preact UI** (`src/ui/`) — shown only for non-game screens (main menu, login, character select, change password). Conditionally rendered based on `GameState` enum. +- **Pixi.js** (`src/map.ts`, `src/atlas.ts`, `src/render/`) — handles all in-game rendering: map tiles, characters, NPCs, animations, effects. + +The central object is `Client` (`src/client/client.ts`). It owns all game state, every controller, the packet bus, the Pixi.js `Application`, and the `MapRenderer`. Nearly everything receives a `Client` reference. + +### Data flow + +``` +WebSocket → PacketBus → handlers/ → client state / emit events → UI / Pixi render +``` + +- **`src/bus.ts` (`PacketBus`)** — manages the WebSocket connection and dispatches incoming packets by `PacketFamily + PacketAction` to registered handler functions. +- **`src/handlers/`** — one file per packet family. Each exports a `registerXxxHandlers(client: Client)` function that calls `client.bus.registerPacketHandler(family, action, fn)`. All are registered at startup via `registerAllHandlers(client)`. +- **`src/controllers/`** — encapsulate client-side logic for sending packets and managing related state (e.g., `MovementController`, `InventoryController`). Each takes `Client` in its constructor. +- **`src/client/events.ts`** — typed `ClientEvents` map used by the mitt emitter on `Client`. Handlers call `client.emit(...)`, UI subscribes via `client.on(...)`. + +### Persistence + +`src/db.ts` wraps IndexedDB (via `idb`) with three stores: +- `pubs` — serialized EIF/ENF/ECF/ESF pub files (keyed by string: `'eif'`, `'enf'`, etc.) +- `maps` — serialized EMF map files (keyed by map ID number) +- `edfs` — EDF dialog/string files (keyed by file ID); fetched from `/data/datNNN.edf` on cache miss + +### Game loop + +`src/main.tsx` sets up a Pixi.js ticker at 120 ms per tick: +```ts +client.tick(); // game logic +client.render(interp); // interpolated Pixi render +``` + +### Graphics + +`src/atlas.ts` (`Atlas`) builds a dynamic texture atlas for character/NPC sprites on demand. Graphics files are loaded from `public/gfx/gfxNNN/` (Custom PE Files extracted via gfx-loader). `src/gfx/gfx-loader.worker.ts` loads graphics in a Web Worker. + +## Key Conventions + +### Imports + +- **Always use `@/` path alias** — never `../` relative parent imports. This is enforced by Biome as an error. +- **Import from barrel files**, not deep paths. The following modules have barrels and must be imported from the top level: + - `@/utils`, `@/render`, `@/controllers`, `@/handlers`, `@/gfx`, `@/fonts` + - For UI sub-modules: `@/ui/chat` not `@/ui/chat/chat` + +### Style + +- Single quotes for JS/TS strings and JSX attributes (enforced by Biome). +- Biome (not ESLint/Prettier) is the sole linter/formatter. Run `pnpm lint` to check, `pnpm format` to auto-fix. + +### Packet handlers + +Register handlers in `src/handlers/.ts`: +```ts +export function registerWalkHandlers(client: Client) { + client.bus!.registerPacketHandler(PacketFamily.Walk, PacketAction.Player, + (reader) => handleWalkPlayer(client, reader)); +} +``` +Each handler function deserializes using the corresponding `eolib` packet class: `XxxServerPacket.deserialize(reader)`. + +### Controllers + +Controller classes store a private `client: Client` reference. They send packets by constructing `eolib` client packet objects and calling `this.client.bus!.send(packet)`. + +### Hair style + +`hairStyle` is 1-based. A value of `0` means no hair. Rendering uses `(hairStyle - 1) * 40 + hairColor * 4`. + +### Nullable returns + +- `getEmf(id)` returns `Promise` — always handle the null case. +- `client.getDialogStrings(id)` always returns a 2-element `string[]` (fallback `['', '']`). + +### UI Look and Feel + +The UI uses DaisyUI components with Tailwind CSS. Reference @.github/daisyui-llms.txt for component usage and styling conventions. + +Shared components (e.g., `Button`) are in `src/ui/components/`. Container components for each screen are in `src/ui/containers/`. + +### In game UI elements + +All in-game UI elements should be movable by the player (Toggle in settings, then drag to reposition) with default +positions based on screen size. + +A global UI Scale setting should allow the player to adjust the size of all UI elements (except the main game canvas) for better readability on different screen sizes. + +The following elements are planned: +- Hotbar (assignable item/skill slots) (Not implemented) +- HUD (character name/level/HP/TP/TNL display (numeric & bars)) (Not implemented) +- Side menu (inventory, map, stats, skills, quests, settings, etc.) (Not implemented) + - Collapsed to hamburger menu for mobile screens +- Touch controls (virtual joystick + buttons, shown only on mobile) (Not implemented) +- Chat (Not implemented) + - All messages have a name (optional), icon (optional), and text. + - The client tracks timestamp for each message and displays as local time (e.g., `[12:34:56]`). + - Chat messages are saved in IndexedDB for chat history to view/search in the Chat Log window. + - WoW style (Color coded, optionally open in separate tabs) + - Local channel (nearby characters, and npcs) + - Global channel (all players, messages prefixed with `~` to send to global) + - Group channel (party messages, prefixed with `'` to send to group) + - Guild channel (guild messages, prefixed with `&` to send to guild) + - Admin channel (GMs only, prefixed with `+` to send to admin) + - Whisper channel(s) (private messages, prefixed with `!recipientName` to send a whisper) + - System messages (Combat log, item pickups, etc. system generated) +- Dialog windows + - Flex positions by default (centered with gap), but allow dragging to reposition (and save position per dialog type). + - Support multiple open dialogs at once (e.g., Inventory + Paperdoll + Chest) with proper z-index stacking when clicked. + - Base dialog types: + - Scrolling List + - Layout: + - Title + - Scrollable content area (e.g., list of items, quests, skills, etc.) + - Action buttons (e.g., Cancel, OK, etc.) + - Push/Pop state for nested dialogs (e.g items for sale -> list of items) + - Dialogs using this layout: Shop, Skill master, Inn keeper, Lawyer (for marriage/divorce), Guild master + - Scrolling grid: + - Layout: + - Title + - Scrollable grid content area (e.g., inventory grid, skill grid, etc.) + - Action buttons (e.g., Cancel, OK, etc.) + - Push/Pop state for nested dialogs (e.g., inventory -> item details) + - Dialogs using this layout: Chest, Locker, Skill list + - Custom Dialog Types: + - Paperdoll + - Displays player information and equipment + - Layout: + - Character info (name, title, home, class, partner, guild name, guild rank name, admin badge) + - Equipment slots (hat, necklace, weapon, armor, shield, gloves, belt, boots, ring 1, ring 2, armlet 1, armlet 2, bracer 1, bracer 2, accessory) + - Displayed in a grid with empty slots for unequipped items + - Image is loaded from gfx-loader + - Hovering over an equipped item shows a tooltip with item details + - OK button to close the dialog + - More to come + +## Public assets + +Game data files must be placed in `public/` before running: +- `public/gfx/gfxNNN.egf` — Custom PE Files for graphics (extracted via gfx-loader) +- `public/sfx/sfxNNN.wav` — sound effects +- `public/maps/NNNNN.emf` — map files (_only_ used for the title screen, in-game maps are loaded via protocol over WebSocket and cached in IndexedDB) +- `public/data/datNNN.edf` — dialog/string data files + +# Refactoring notes + +- Currently a lot of game state is stored in `Client`. Some things have been extracted into controllers but there's still +a lot that need to be moved out. +- The `ClientEvent` type is being phased out in favor of controller specific events (e.g., `CharacterSelectEvent`) that are emitted by controllers instead of the client. This allows for better separation of concerns and more modular code. +- The `GameState` enum is being expanded to include more specific states (e.g., `CharacterSelect`, `ChangePassword`) instead of the generic `LoggedIn`. This allows for more precise control over the UI and game flow. diff --git a/.github/daisyui-llms.txt b/.github/daisyui-llms.txt new file mode 100644 index 00000000..19e9a812 --- /dev/null +++ b/.github/daisyui-llms.txt @@ -0,0 +1,1874 @@ +--- +description: daisyUI 5 +alwaysApply: true +applyTo: "**" +downloadedFrom: https://daisyui.com/llms.txt +version: 5.5.x +--- + +# daisyUI 5 +daisyUI 5 is a CSS library for Tailwind CSS 4 +daisyUI 5 provides class names for common UI components + +- [daisyUI 5 docs](http://daisyui.com) +- [Guide: How to use this file in LLMs and code editors](https://daisyui.com/docs/editor/) +- [daisyUI 5 release notes](https://daisyui.com/docs/v5/) +- [daisyUI 4 to 5 upgrade guide](https://daisyui.com/docs/upgrade/) + +## daisyUI 5 install notes +[install guide](https://daisyui.com/docs/install/) +1. daisyUI 5 requires Tailwind CSS 4 +2. `tailwind.config.js` file is deprecated in Tailwind CSS v4. do not use `tailwind.config.js`. Tailwind CSS v4 only needs `@import "tailwindcss";` in the CSS file if it's a node dependency. +3. daisyUI 5 can be installed using `npm i -D daisyui@latest` and then adding `@plugin "daisyui";` to the CSS file +4. daisyUI is suggested to be installed as a dependency but if you really want to use it from CDN, you can use Tailwind CSS and daisyUI CDN files: +```html + + +``` +5. A CSS file with Tailwind CSS and daisyUI looks like this (if it's a node dependency) +```css +@import "tailwindcss"; +@plugin "daisyui"; +``` + +## daisyUI 5 usage rules +1. We can give styles to a HTML element by adding daisyUI class names to it. By adding a component class name, part class names (if there's any available for that component), and modifier class names (if there's any available for that component) +2. Components can be customized using Tailwind CSS utility classes if the customization is not possible using the existing daisyUI classes. For example `btn px-10` sets a custom horizontal padding to a `btn` +3. If customization of daisyUI styles using Tailwind CSS utility classes didn't work because of CSS specificity issues, you can use the `!` at the end of the Tailwind CSS utility class to override the existing styles. For example `btn bg-red-500!` sets a custom background color to a `btn` forcefully. This is a last resort solution and should be used sparingly +4. If a specific component or something similar to it doesn't exist in daisyUI, you can create your own component using Tailwind CSS utility +5. when using Tailwind CSS `flex` and `grid` for layout, it should be responsive using Tailwind CSS responsive utility prefixes. +6. Only allowed class names are existing daisyUI class names or Tailwind CSS utility classes. +7. Ideally, you won't need to write any custom CSS. Using daisyUI class names or Tailwind CSS utility classes is preferred. +8. suggested - if you need placeholder images, use https://picsum.photos/200/300 with the size you want +9. suggested - when designing , don't add a custom font unless it's necessary +10. don't add `bg-base-100 text-base-content` to body unless it's necessary +11. For design decisions, use Refactoring UI book best practices + +daisyUI 5 class names are one of the following categories. These type names are only for reference and are not used in the actual code +- `component`: the required component class +- `part`: a child part of a component +- `style`: sets a specific style to component or part +- `behavior`: changes the behavior of component or part +- `color`: sets a specific color to component or part +- `size`: sets a specific size to component or part +- `placement`: sets a specific placement to component or part +- `direction`: sets a specific direction to component or part +- `modifier`: modifies the component or part in a specific way +- `variant`: prefixes for utility classes that conditionally apply styles. syntax is `variant:utility-class` + +## Config +daisyUI 5 config docs: https://daisyui.com/docs/config/ +daisyUI without config: +```css +@plugin "daisyui"; +``` +daisyUI config with `light` theme only: +```css +@plugin "daisyui" { + themes: light --default; +} +``` +daisyUI with all the default configs: +```css +@plugin "daisyui" { + themes: light --default, dark --prefersdark; + root: ":root"; + include: ; + exclude: ; + prefix: ; + logs: true; +} +``` +An example config: +In below config, all the built-in themes are enabled while bumblebee is the default theme and synthwave is the prefersdark theme (default dark mode) +All the other themes are enabled and can be used by adding `data-theme="THEME_NAME"` to the `` element +root scrollbar gutter is excluded. `daisy-` prefix is used for all daisyUI classes and console.log is disabled +```css +@plugin "daisyui" { + themes: light, dark, cupcake, bumblebee --default, emerald, corporate, synthwave --prefersdark, retro, cyberpunk, valentine, halloween, garden, forest, aqua, lofi, pastel, fantasy, wireframe, black, luxury, dracula, cmyk, autumn, business, acid, lemonade, night, coffee, winter, dim, nord, sunset, caramellatte, abyss, silk; + root: ":root"; + include: ; + exclude: rootscrollgutter, checkbox; + prefix: daisy-; + logs: false; +} +``` +## daisyUI 5 colors + +### daisyUI color names +- `primary`: Primary brand color, The main color of your brand +- `primary-content`: Foreground content color to use on primary color +- `secondary`: Secondary brand color, The optional, secondary color of your brand +- `secondary-content`: Foreground content color to use on secondary color +- `accent`: Accent brand color, The optional, accent color of your brand +- `accent-content`: Foreground content color to use on accent color +- `neutral`: Neutral dark color, For not-saturated parts of UI +- `neutral-content`: Foreground content color to use on neutral color +- `base-100`:-100 Base surface color of page, used for blank backgrounds +- `base-200`:-200 Base color, darker shade, to create elevations +- `base-300`:-300 Base color, even more darker shade, to create elevations +- `base-content`: Foreground content color to use on base color +- `info`: Info color, For informative/helpful messages +- `info-content`: Foreground content color to use on info color +- `success`: Success color, For success/safe messages +- `success-content`: Foreground content color to use on success color +- `warning`: Warning color, For warning/caution messages +- `warning-content`: Foreground content color to use on warning color +- `error`: Error color, For error/danger/destructive messages +- `error-content`: Foreground content color to use on error color + +### daisyUI color rules +1. daisyUI adds semantic color names to Tailwind CSS colors +2. daisyUI color names can be used in utility classes, like other Tailwind CSS color names. for example, `bg-primary` will use the primary color for the background +3. daisyUI color names include variables as value so they can change based the theme +4. There's no need to use `dark:` for daisyUI color names +5. Ideally only daisyUI color names should be used for colors so the colors can change automatically based on the theme +6. If a Tailwind CSS color name (like `red-500`) is used, it will be same red color on all themes +7. If a daisyUI color name (like `primary`) is used, it will change color based on the theme +8. Using Tailwind CSS color names for text colors should be avoided because Tailwind CSS color `text-gray-800` on `bg-base-100` would be unreadable on a dark theme - because on dark theme, `bg-base-100` is a dark color +9. `*-content` colors should have a good contrast compared to their associated colors +10. suggestion - when designing a page use `base-*` colors for majority of the page. use `primary` color for important elements + +### daisyUI custom theme with custom colors +A CSS file with Tailwind CSS, daisyUI and a custom daisyUI theme looks like this: +```css +@import "tailwindcss"; +@plugin "daisyui"; +@plugin "daisyui/theme" { + name: "mytheme"; + default: true; /* set as default */ + prefersdark: false; /* set as default dark mode (prefers-color-scheme:dark) */ + color-scheme: light; /* color of browser-provided UI */ + + --color-base-100: oklch(98% 0.02 240); + --color-base-200: oklch(95% 0.03 240); + --color-base-300: oklch(92% 0.04 240); + --color-base-content: oklch(20% 0.05 240); + --color-primary: oklch(55% 0.3 240); + --color-primary-content: oklch(98% 0.01 240); + --color-secondary: oklch(70% 0.25 200); + --color-secondary-content: oklch(98% 0.01 200); + --color-accent: oklch(65% 0.25 160); + --color-accent-content: oklch(98% 0.01 160); + --color-neutral: oklch(50% 0.05 240); + --color-neutral-content: oklch(98% 0.01 240); + --color-info: oklch(70% 0.2 220); + --color-info-content: oklch(98% 0.01 220); + --color-success: oklch(65% 0.25 140); + --color-success-content: oklch(98% 0.01 140); + --color-warning: oklch(80% 0.25 80); + --color-warning-content: oklch(20% 0.05 80); + --color-error: oklch(65% 0.3 30); + --color-error-content: oklch(98% 0.01 30); + + --radius-selector: 1rem; /* border radius of selectors (checkbox, toggle, badge) */ + --radius-field: 0.25rem; /* border radius of fields (button, input, select, tab) */ + --radius-box: 0.5rem; /* border radius of boxes (card, modal, alert) */ + /* preferred values for --radius-* : 0rem, 0.25rem, 0.5rem, 1rem, 2rem */ + + --size-selector: 0.25rem; /* base size of selectors (checkbox, toggle, badge). Value must be 0.25rem unless we intentionally want bigger selectors. In so it can be 0.28125 or 0.3125. If we intentionally want smaller selectors, it can be 0.21875 or 0.1875 */ + --size-field: 0.25rem; /* base size of fields (button, input, select, tab). Value must be 0.25rem unless we intentionally want bigger fields. In so it can be 0.28125 or 0.3125. If we intentionally want smaller fields, it can be 0.21875 or 0.1875 */ + + --border: 1px; /* border size. Value must be 1px unless we intentionally want thicker borders. In so it can be 1.5px or 2px. If we intentionally want thinner borders, it can be 0.5px */ + + --depth: 1; /* only 0 or 1 – Adds a shadow and subtle 3D depth effect to components */ + --noise: 0; /* only 0 or 1 - Adds a subtle noise (grain) effect to components */ +} +``` +#### Rules +- All CSS variables above are required +- Colors can be OKLCH or hex or other formats +- If you're generating a custom theme, do not include the comments from the example above. Just provide the code. + +People can use https://daisyui.com/theme-generator/ visual tool to create their own theme. + +## daisyUI 5 components + +### accordion +Accordion is used for showing and hiding content but only one item can stay open at a time + +[accordion docs](https://daisyui.com/components/accordion/) + +#### Class names +- component: `collapse` +- part: `collapse-title`, `collapse-content` +- modifier: `collapse-arrow`, `collapse-plus`, `collapse-open`, `collapse-close` + +#### Syntax +```html +
{CONTENT}
+``` +where content is: +```html + +
{title}
+
{CONTENT}
+``` + +#### Rules +- {MODIFIER} is optional and can have one of the modifier class names +- Accordion uses radio inputs. All radio inputs with the same name work together and only one of them can be open at a time +- If you have more than one set of accordion items on a page, use different names for the radio inputs on each set +- Replace {name} with a unique name for the accordion group +- replace `{checked}` with `checked="checked"` if you want the accordion to be open by default + +### alert +Alert informs users about important events + +[alert docs](https://daisyui.com/components/alert/) + +#### Class names +- component: `alert` +- style: `alert-outline`, `alert-dash`, `alert-soft` +- color: `alert-info`, `alert-success`, `alert-warning`, `alert-error` +- direction: `alert-vertical`, `alert-horizontal` + +#### Syntax +```html + +``` + +#### Rules +- {MODIFIER} is optional and can have one of each style/color/direction class names +- Add `sm:alert-horizontal` for responsive layouts + +### avatar +Avatars are used to show a thumbnail + +[avatar docs](https://daisyui.com/components/avatar/) + +#### Class names +- component: `avatar`, `avatar-group` +- modifier: `avatar-online`, `avatar-offline`, `avatar-placeholder` + +#### Syntax +```html +
+
+ +
+
+``` + +#### Rules +- {MODIFIER} is optional and can have one of the modifier class names +- Use `avatar-group` for containing multiple avatars +- You can set custom sizes using `w-*` and `h-*` +- You can use mask classes such as `mask-squircle`, `mask-hexagon`, `mask-triangle` + +### badge +Badges are used to inform the user of the status of specific data + +[badge docs](https://daisyui.com/components/badge/) + +#### Class names +- component: `badge` +- style: `badge-outline`, `badge-dash`, `badge-soft`, `badge-ghost` +- color: `badge-neutral`, `badge-primary`, `badge-secondary`, `badge-accent`, `badge-info`, `badge-success`, `badge-warning`, `badge-error` +- size: `badge-xs`, `badge-sm`, `badge-md`, `badge-lg`, `badge-xl` + +#### Syntax +```html +Badge +``` + +#### Rules +- {MODIFIER} is optional and can have one of each style/color/size class names +- Can be used inside text or buttons +- To create an empty badge, just remove the text between the span tags + +### breadcrumbs +Breadcrumbs helps users to navigate + +[breadcrumbs docs](https://daisyui.com/components/breadcrumbs/) + +#### Class names +- component: `breadcrumbs` + +#### Syntax +```html + +``` + +#### Rules +- breadcrumbs only has one main class name +- Can contain icons inside the links +- If you set `max-width` or the list gets larger than the container it will scroll + +### button +Buttons allow the user to take actions + +[button docs](https://daisyui.com/components/button/) + +#### Class names +- component: `btn` +- color: `btn-neutral`, `btn-primary`, `btn-secondary`, `btn-accent`, `btn-info`, `btn-success`, `btn-warning`, `btn-error` +- style: `btn-outline`, `btn-dash`, `btn-soft`, `btn-ghost`, `btn-link` +- behavior: `btn-active`, `btn-disabled` +- size: `btn-xs`, `btn-sm`, `btn-md`, `btn-lg`, `btn-xl` +- modifier: `btn-wide`, `btn-block`, `btn-square`, `btn-circle` + +#### Syntax +```html + +``` +#### Rules +- {MODIFIER} is optional and can have one of each color/style/behavior/size/modifier class names +- btn can be used on any html tags such as ` +``` + +#### Rules +- {MODIFIER} is optional and can have one of the size class names +- To make a button active, add `dock-active` class to the button +- add `` is required for responsivness of the dock in iOS + +### drawer +Drawer is a grid layout that can show/hide a sidebar on the left or right side of the page + +[drawer docs](https://daisyui.com/components/drawer/) + +#### Class names +- component: `drawer` +- part: `drawer-toggle`, `drawer-content`, `drawer-side`, `drawer-overlay` +- placement: `drawer-end` +- modifier: `drawer-open` +- variant: `is-drawer-open:`, `is-drawer-close:` + +#### Syntax +```html +
+ +
{CONTENT}
+
{SIDEBAR}
+
+``` +where {CONTENT} can be navbar, site content, footer, etc +and {SIDEBAR} can be a menu like: +```html +
+``` +To open/close the drawer, use a label that points to the `drawer-toggle` input: +```html + +``` +Example: This sidebar is always visible on large screen, can be toggled on small screen: +```html +
+ +
+ + +
+
+ + +
+
+``` + +Example: This sidebar is always visible. When it's close we only see iocns, when it's open we see icons and text +```html +
+ +
+ +
+
+ +
+ + + +
+ +
+
+
+
+``` + +#### Rules +- {MODIFIER} is optional and can have one of the modifier/placement class names +- `id` is required for the `drawer-toggle` input. change `my-drawer` to a unique id according to your needs +- `lg:drawer-open` can be used to make sidebar visible on larger screens +- `drawer-toggle` is a hidden checkbox. Use label with "for" attribute to toggle state +- if you want to open the drawer when a button is clicked, use `` where `my-drawer` is the id of the `drawer-toggle` input +- when using drawer, every page content must be inside `drawer-content` element. for example navbar, footer, etc should not be outside of `drawer` + +### dropdown +Dropdown can open a menu or any other element when the button is clicked + +[dropdown docs](https://daisyui.com/components/dropdown/) + +#### Class names +- component: `dropdown` +- part: `dropdown-content` +- placement: `dropdown-start`, `dropdown-center`, `dropdown-end`, `dropdown-top`, `dropdown-bottom`, `dropdown-left`, `dropdown-right` +- modifier: `dropdown-hover`, `dropdown-open`, `dropdown-close` + +#### Syntax +Using details and summary +```html + +``` + +Using popover API +```html + + +``` + +Using CSS focus +```html + +``` + +#### Rules +- {MODIFIER} is optional and can have one of the modifier/placement class names +- replace `{id}` and `{anchor}` with a unique name +- For CSS focus dropdowns, use `tabindex="0"` and `role="button"` on the button +- The content can be any HTML element (not just `
    `) + +### fab +FAB (Floating Action Button) stays in the bottom corner of screen. It includes a focusable and accessible element with button role. Clicking or focusing it shows additional buttons (known as Speed Dial buttons) in a vertical arrangement or a flower shape (quarter circle) + +[fab docs](https://daisyui.com/components/fab/) + +#### Class names +- component: `fab` +- part: `fab-close`, `fab-main-action` +- modifier: `fab-flower` + +#### Syntax +A single FAB in the corder of screen +```html +
    + +
    +``` +A FAB that opens a 3 other buttons in the corner of page vertically +```html +
    +
    {IconOriginal}
    + + + +
    +``` +A FAB that opens a 3 other buttons in the corner of page vertically and they have label text +```html +
    +
    {IconOriginal}
    +
    {Label1}
    +
    {Label2}
    +
    {Label3}
    +
    +``` +FAB with rectangle buttons. These are not circular buttons so they can have more content. +```html +
    +
    {IconOriginal}
    + + + +
    +``` +FAB with close button. When FAB is open, the original button is replaced with a close button +```html +
    +
    {IconOriginal}
    +
    Close
    +
    {Label1}
    +
    {Label2}
    +
    {Label3}
    +
    +``` +FAB with Main Action button. When FAB is open, the original button is replaced with a main action button +```html +
    +
    {IconOriginal}
    +
    + {LabelMainAction} +
    +
    {Label1}
    +
    {Label2}
    +
    {Label3}
    +
    +``` +FAB Flower. It opens the buttons in a flower shape (quarter circle) arrangement instead of vertical +```html +
    +
    {IconOriginal}
    + + + + +
    +``` +FAB Flower with tooltips. There's no space for a text label in a quarter circle, so tooltips are used to indicate the button's function +```html +
    +
    {IconOriginal}
    + +
    + +
    +
    + +
    +
    + +
    +
    +``` +#### Rules +- {Icon*} should be replaced with the appropriate icon for each button. SVG icons are recommended +- {IconOriginal} is the icon that we see before opening the FAB +- {IconMainAction} is the icon we see after opening the FAB +- {Icon1}, {Icon2}, {Icon3} are the icons for the additional buttons +- {Label*} is the label text for each button + +### fieldset +Fieldset is a container for grouping related form elements. It includes fieldset-legend as a title and label as a description + +[fieldset docs](https://daisyui.com/components/fieldset/) + +#### Class names +- Component: `fieldset`, `label` +- Parts: `fieldset-legend` + +#### Syntax +```html +
    + {title} + {CONTENT} +

    {description}

    +
    +``` + +#### Rules +- You can use any element as a direct child of fieldset to add form elements + +### file-input +File Input is a an input field for uploading files + +[file-input docs](https://daisyui.com/components/file-input/) + +#### Class Names: +- Component: `file-input` +- Style: `file-input-ghost` +- Color: `file-input-neutral`, `file-input-primary`, `file-input-secondary`, `file-input-accent`, `file-input-info`, `file-input-success`, `file-input-warning`, `file-input-error` +- Size: `file-input-xs`, `file-input-sm`, `file-input-md`, `file-input-lg`, `file-input-xl` + +#### Syntax +```html + +``` + +#### Rules +- {MODIFIER} is optional and can have one of each style/color/size class names + +### filter +Filter is a group of radio buttons. Choosing one of the options will hide the others and shows a reset button next to the chosen option + +[filter docs](https://daisyui.com/components/filter/) + +#### Class names +- component: `filter` +- part: `filter-reset` + +#### Syntax +Using HTML form +```html +
    + + + +
    +``` +Without HTML form +```html +
    + + + +
    +``` + +#### Rules +- replace `{NAME}` with proper value, according to the context of the filter +- Each set of radio inputs must have unique `name` attributes to avoid conflicts +- Use `
    ` tag when possible and only use `
    ` if you can't use a HTML form for some reason +- Use `filter-reset` class for the reset button + +### footer +Footer can contain logo, copyright notice, and links to other pages + +[footer docs](https://daisyui.com/components/footer/) + +#### Class names +- component: `footer` +- part: `footer-title` +- placement: `footer-center` +- direction: `footer-horizontal`, `footer-vertical` + +#### Syntax +```html +
    {CONTENT}
    +``` +where content can contain several `
)}
); } -// --------------------------------------------------------------------------- -// ChatDialog // --------------------------------------------------------------------------- type Props = { @@ -329,13 +320,11 @@ export function ChatDialog({ id }: Props) { maxHeight: '45vh', }} > - {/* Row 1: Input + Send */} } /> - {/* Row 2: Tabs */} {tabs.length > 1 && (
)} - {/* Row 3: Chat log */} {tab.name} {hasUnread && ( - + )} {isCloseable && ( diff --git a/src/ui/in-game/dialogs/dialog-base.tsx b/src/ui/in-game/dialogs/dialog-base.tsx index 03792a46..ec3f4e7e 100644 --- a/src/ui/in-game/dialogs/dialog-base.tsx +++ b/src/ui/in-game/dialogs/dialog-base.tsx @@ -155,12 +155,10 @@ export function DialogBase({ onKeyDown={stopProp} onContextMenu={stopProp} > - {/* Title bar */}
- {/* Title or custom content (e.g. tabs) */}
{titleContent ?? ( @@ -169,10 +167,8 @@ export function DialogBase({ )}
- {/* Controls — hidden for dialogs like main chat */} {!hideControls && (
- {/* Layout mode dropdown */}
{menuOpen && ( -
e.stopPropagation()} > {AUTO_LAYOUT_OPTIONS.map((l) => ( - +
  • + +
  • ))} -
    + )}
    - {/* Minimize / restore button */} - {/* Close button */}
    ); diff --git a/src/ui/in-game/dialogs/settings-dialog.tsx b/src/ui/in-game/dialogs/settings-dialog.tsx index 8f89d95b..18e9ad02 100644 --- a/src/ui/in-game/dialogs/settings-dialog.tsx +++ b/src/ui/in-game/dialogs/settings-dialog.tsx @@ -1,3 +1,4 @@ +import { Button } from '@/ui/components'; import { clearAllDialogLayouts, clearAllPositions, @@ -17,21 +18,19 @@ export function SettingsDialog() { return (
    -
    -

    More settings coming soon.

    -
    +

    More settings coming soon.

    Reset all UI positions and layout settings back to defaults.

    - +
    diff --git a/src/ui/in-game/hud/nav-sidebar.tsx b/src/ui/in-game/hud/nav-sidebar.tsx index a944f9b7..7c721220 100644 --- a/src/ui/in-game/hud/nav-sidebar.tsx +++ b/src/ui/in-game/hud/nav-sidebar.tsx @@ -9,6 +9,7 @@ import { } from 'react-icons/gi'; import { LuMenu } from 'react-icons/lu'; import { DialogResourceID } from '@/edf'; +import { Button } from '@/ui/components'; import { useClient } from '@/ui/context'; import { type DialogId, useWindowManager } from '@/ui/in-game'; import { HUD_Z } from './consts'; @@ -57,26 +58,26 @@ export function NavSidebar() { onContextMenu={(e) => e.stopPropagation()} > {NAV_BUTTONS.map(({ id, label, Icon }) => ( - + ))} - +
    ); @@ -97,55 +98,53 @@ export function MobileNav() { onKeyDown={(e) => e.stopPropagation()} onContextMenu={(e) => e.stopPropagation()} > - + {open && ( -
    e.stopPropagation()} + onClick={(e) => e.stopPropagation()} > {NAV_BUTTONS.map(({ id, label, Icon }) => ( +
  • + +
  • + ))} +
  • +
    +
  • +
  • - ))} -
    - -
    +
  • + )}
    diff --git a/src/ui/in-game/hud/player-hud.tsx b/src/ui/in-game/hud/player-hud.tsx index ef91634c..12b8ce47 100644 --- a/src/ui/in-game/hud/player-hud.tsx +++ b/src/ui/in-game/hud/player-hud.tsx @@ -1,58 +1,16 @@ +import { ProgressBar } from '@/ui/components'; import { usePlayerStats } from '@/ui/in-game'; import { getExpForLevel } from '@/utils'; import { HUD_Z } from './consts'; const stopPropagation = (e: { stopPropagation(): void }) => e.stopPropagation(); -function fmt(n: number): string { - if (n >= 1_000_000) - return `${(n / 1_000_000).toFixed(1).replace(/\.0$/, '')}m`; - if (n >= 1_000) return `${(n / 1_000).toFixed(1).replace(/\.0$/, '')}k`; - return String(n); -} - function hpBarClass(pct: number): string { if (pct >= 50) return 'bg-gradient-to-r from-green-700 to-green-500'; if (pct >= 25) return 'bg-gradient-to-r from-yellow-600 to-yellow-400'; return 'bg-gradient-to-r from-red-700 to-red-500'; } -function ProgressBar({ - value, - max, - label, - barClass, -}: { - value: number; - max: number; - label: string; - barClass: string; -}) { - const pct = max > 0 ? Math.min(100, Math.round((value / max) * 100)) : 0; - return ( -
    - - {label} - -
    -
    - - {fmt(value)}/{fmt(max)} - -
    -
    - ); -} - export function PlayerHud() { const stats = usePlayerStats(); @@ -84,7 +42,6 @@ export function PlayerHud() { onKeyDown={stopPropagation} onContextMenu={stopPropagation} > - {/* Name + level */}
    {stats.name} @@ -93,7 +50,6 @@ export function PlayerHud() { Lv {stats.level}
    - {/* Bars */}
    Date: Sun, 5 Apr 2026 09:43:43 -0400 Subject: [PATCH 016/103] Standardize theme colors across all in-game UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace all hardcoded white/rgba colors with consistent DaisyUI tokens: Panel background: bg-base-300/90 backdrop-blur-sm (was bg-base-200/40 on dialogs vs bg-base-300/85 on mobile chat — now identical) Border: border-base-content/10 (was border-white/10 everywhere — now adapts to light/dark themes) Section bars: bg-base-content/5 (was bg-white/5 in dialog title bars and bg-base-300/60 in mobile chat tab bar) Active/hover states: bg-base-content/20 and bg-base-content/10 (was bg-white/20 and bg-white/10 in chat tabs and nav menu items) Player HUD: bg-base-300/95 backdrop-blur-sm (was hardcoded linear-gradient with rgba values) Progress bar track: bg-black/45 ring-1 ring-inset ring-white/8 (was inline style rgba — now uses Tailwind classes) Menus: border border-base-content/10 (was border-base-200) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/ui/components/progress-bar.tsx | 8 +------- src/ui/in-game/chat/chat-dialog.tsx | 8 ++++---- src/ui/in-game/chat/chat-input.tsx | 4 ++-- src/ui/in-game/chat/chat-tab-bar.tsx | 2 +- src/ui/in-game/dialogs/dialog-base.tsx | 6 +++--- src/ui/in-game/hud/nav-sidebar.tsx | 4 ++-- src/ui/in-game/hud/player-hud.tsx | 7 ++----- 7 files changed, 15 insertions(+), 24 deletions(-) diff --git a/src/ui/components/progress-bar.tsx b/src/ui/components/progress-bar.tsx index 69f55e27..9427b723 100644 --- a/src/ui/components/progress-bar.tsx +++ b/src/ui/components/progress-bar.tsx @@ -19,13 +19,7 @@ export function ProgressBar({ value, max, label, barClass }: ProgressBarProps) { {label} -
    +
    @@ -189,7 +189,7 @@ function AddTabButton({ dialogId }: { dialogId: ChatDialogId }) { {open && (