Compare commits

...

53 Commits

Author SHA1 Message Date
e53e154d16 npm run format 2025-02-01 16:18:33 +01:00
d65ceba66a Temp. depth sorting fix 2025-02-01 16:14:15 +01:00
db426bb03e Fix for chat bubble 2025-02-01 15:48:42 +01:00
af26ca5e89 Added container for character for easier X and Y coord handling 2025-02-01 15:27:14 +01:00
e4b9bb4d61 Refactored Character.vue as preparation for attack anims. 2025-02-01 15:10:52 +01:00
d7f60d7bfc Don't include sprite dimensions in payload sent to server 2025-02-01 14:49:33 +01:00
cfdfa98379 Revert 2025-02-01 14:22:42 +01:00
63889a537a npm update 2025-02-01 14:22:37 +01:00
99bb1555a0 Listen for attack events. TODO: finish anim. handling 2025-02-01 04:30:07 +01:00
ac1396304f Added web worker to improve tile analysis performance 2025-02-01 03:11:13 +01:00
09ee9bf01d Show tile & mapObject list on top of toolbar buttons, re-enabled camera follow 2025-02-01 02:26:54 +01:00
09b458eeef Merge remote-tracking branch 'origin/main' into feature/#321 2025-02-01 01:43:12 +01:00
9d95562679 Implemented logic to walk with arrow keys 2025-02-01 01:43:01 +01:00
a9de031673 poc 2025-02-01 01:31:28 +01:00
8e81ce716b Minor tweaks and fixes 2025-01-31 23:11:15 +01:00
2c1db56cc4 Merge remote-tracking branch 'origin/feature/#321' 2025-01-31 23:02:01 +01:00
4fba3678d6 Added eraser to tool check 2025-01-31 23:01:19 +01:00
d29ca10ba9 Merge remote-tracking branch 'origin/feature/#321' 2025-01-31 23:00:16 +01:00
67f83c3447 Forgor styling on the other tiles, fix tab closing 2025-01-31 22:57:14 +01:00
8f82bad3fa Merge remote-tracking branch 'origin/feature/#321' 2025-01-31 22:39:34 +01:00
d665ac989c #321 - Made mapObjectList & tileList side panels 2025-01-31 22:35:13 +01:00
e389534e30 npm run format 2025-01-31 22:33:45 +01:00
7d3946e274 #96 - Renamed and refactored pointer handler composables in favor of arrow key movement. 2025-01-31 22:26:23 +01:00
0f46e3b6d2 #323 - Added alt to all images, also removed unused templates
Templates for old user panel hadnt been removed yet
2025-01-31 19:35:29 +01:00
6ca82733eb #324 - Fix Character profile image 2025-01-31 19:04:42 +01:00
eb61f45535 npm update 2025-01-31 18:49:03 +01:00
a181fc7fe3 npm update 2025-01-31 17:06:16 +01:00
507d4226ac Bug fix for character profile, greatly improved javascript 2025-01-31 03:23:23 +01:00
5dd9d1e7af Added temp. offset logic for easier sprite management 2025-01-31 02:16:19 +01:00
15f9e9861e Zoom 2025-01-31 01:55:52 +01:00
7fd334d414 Applied styling fix 2025-01-31 01:19:49 +01:00
c7d4b5f2c3 Updates TS hints 2025-01-31 01:18:49 +01:00
5747166822 Don't toggle accordion on button press 2025-01-31 01:17:44 +01:00
c010373e5b Updated loading indicator to match the other one we have 2025-01-30 18:35:51 +01:00
57ad9d4889 Don't update after closing sprite action img offset modal 2025-01-30 18:32:33 +01:00
f268ac9e5b Removed comment, updated types for sprite actions, minor modal component improvement, added components for better sprite management 2025-01-30 18:29:55 +01:00
8befce7ffb Update packages 2025-01-29 23:28:04 +01:00
014c08b17a Set depth to 9999 to always show above character sprite 2025-01-28 17:54:12 +01:00
bdbda6456c CharHair work 2025-01-28 16:32:16 +01:00
85537840ab Improved code 2025-01-28 05:14:37 +01:00
2b7082ac92 Bug fix caused by formatting 2025-01-28 05:01:52 +01:00
abc58bfa38 npm run format, removed container that character was in 2025-01-28 04:57:21 +01:00
027325f2bf Merge remote-tracking branch 'origin/main' into feature/map-refactor 2025-01-28 01:59:29 +01:00
517e92b07b Fully restored tile picker function 2025-01-27 14:49:27 -06:00
6bede8c44e Map editor ui fixes, switching back to game from map editor, draw/tap checkbox, and partial restoration of tile picker function 2025-01-27 14:33:29 -06:00
9e652868ca Prepping to work on Placed map objects 2025-01-27 00:18:46 -06:00
35f0dcca64 Hierarchical pointer handling logic 2025-01-26 21:21:24 -06:00
9618e07bc6 refactoring pointer events and input handling improvements 2025-01-26 20:50:34 -06:00
791830fd6f Refactoring of modalShown booleans 2025-01-26 18:56:13 -06:00
37acf1782b Removed isAnimated and isLooping fields 2025-01-27 01:51:53 +01:00
14aa696197 Changes to mapeditorcomposable, fix pencil and fill tool for tiles
Locked in, made mapeditor my bi-
2025-01-26 23:28:15 +01:00
cfac1d508b Minor change 2025-01-25 16:49:45 +01:00
82cfe5902f Bye 2025-01-25 16:49:25 +01:00
66 changed files with 1835 additions and 2002 deletions

310
package-lock.json generated
View File

@ -840,9 +840,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@parcel/watcher": { "node_modules/@parcel/watcher": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
"integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==", "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
@ -861,25 +861,25 @@
"url": "https://opencollective.com/parcel" "url": "https://opencollective.com/parcel"
}, },
"optionalDependencies": { "optionalDependencies": {
"@parcel/watcher-android-arm64": "2.5.0", "@parcel/watcher-android-arm64": "2.5.1",
"@parcel/watcher-darwin-arm64": "2.5.0", "@parcel/watcher-darwin-arm64": "2.5.1",
"@parcel/watcher-darwin-x64": "2.5.0", "@parcel/watcher-darwin-x64": "2.5.1",
"@parcel/watcher-freebsd-x64": "2.5.0", "@parcel/watcher-freebsd-x64": "2.5.1",
"@parcel/watcher-linux-arm-glibc": "2.5.0", "@parcel/watcher-linux-arm-glibc": "2.5.1",
"@parcel/watcher-linux-arm-musl": "2.5.0", "@parcel/watcher-linux-arm-musl": "2.5.1",
"@parcel/watcher-linux-arm64-glibc": "2.5.0", "@parcel/watcher-linux-arm64-glibc": "2.5.1",
"@parcel/watcher-linux-arm64-musl": "2.5.0", "@parcel/watcher-linux-arm64-musl": "2.5.1",
"@parcel/watcher-linux-x64-glibc": "2.5.0", "@parcel/watcher-linux-x64-glibc": "2.5.1",
"@parcel/watcher-linux-x64-musl": "2.5.0", "@parcel/watcher-linux-x64-musl": "2.5.1",
"@parcel/watcher-win32-arm64": "2.5.0", "@parcel/watcher-win32-arm64": "2.5.1",
"@parcel/watcher-win32-ia32": "2.5.0", "@parcel/watcher-win32-ia32": "2.5.1",
"@parcel/watcher-win32-x64": "2.5.0" "@parcel/watcher-win32-x64": "2.5.1"
} }
}, },
"node_modules/@parcel/watcher-android-arm64": { "node_modules/@parcel/watcher-android-arm64": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
"integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==", "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -898,9 +898,9 @@
} }
}, },
"node_modules/@parcel/watcher-darwin-arm64": { "node_modules/@parcel/watcher-darwin-arm64": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
"integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==", "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -919,9 +919,9 @@
} }
}, },
"node_modules/@parcel/watcher-darwin-x64": { "node_modules/@parcel/watcher-darwin-x64": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
"integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==", "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -940,9 +940,9 @@
} }
}, },
"node_modules/@parcel/watcher-freebsd-x64": { "node_modules/@parcel/watcher-freebsd-x64": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
"integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==", "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -961,9 +961,9 @@
} }
}, },
"node_modules/@parcel/watcher-linux-arm-glibc": { "node_modules/@parcel/watcher-linux-arm-glibc": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
"integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==", "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -982,9 +982,9 @@
} }
}, },
"node_modules/@parcel/watcher-linux-arm-musl": { "node_modules/@parcel/watcher-linux-arm-musl": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
"integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==", "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -1003,9 +1003,9 @@
} }
}, },
"node_modules/@parcel/watcher-linux-arm64-glibc": { "node_modules/@parcel/watcher-linux-arm64-glibc": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
"integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==", "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1024,9 +1024,9 @@
} }
}, },
"node_modules/@parcel/watcher-linux-arm64-musl": { "node_modules/@parcel/watcher-linux-arm64-musl": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
"integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==", "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1045,9 +1045,9 @@
} }
}, },
"node_modules/@parcel/watcher-linux-x64-glibc": { "node_modules/@parcel/watcher-linux-x64-glibc": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
"integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==", "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1066,9 +1066,9 @@
} }
}, },
"node_modules/@parcel/watcher-linux-x64-musl": { "node_modules/@parcel/watcher-linux-x64-musl": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
"integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==", "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1087,9 +1087,9 @@
} }
}, },
"node_modules/@parcel/watcher-win32-arm64": { "node_modules/@parcel/watcher-win32-arm64": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
"integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==", "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1108,9 +1108,9 @@
} }
}, },
"node_modules/@parcel/watcher-win32-ia32": { "node_modules/@parcel/watcher-win32-ia32": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
"integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==", "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -1129,9 +1129,9 @@
} }
}, },
"node_modules/@parcel/watcher-win32-x64": { "node_modules/@parcel/watcher-win32-x64": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
"integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==", "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1161,9 +1161,9 @@
} }
}, },
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.32.0", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.0.tgz",
"integrity": "sha512-G2fUQQANtBPsNwiVFg4zKiPQyjVKZCUdQUol53R8E71J7AsheRMV/Yv/nB8giOcOVqP7//eB5xPqieBYZe9bGg==", "integrity": "sha512-Eeao7ewDq79jVEsrtWIj5RNqB8p2knlm9fhR6uJ2gqP7UfbLrTrxevudVrEPDM7Wkpn/HpRC2QfazH7MXLz3vQ==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -1175,9 +1175,9 @@
] ]
}, },
"node_modules/@rollup/rollup-android-arm64": { "node_modules/@rollup/rollup-android-arm64": {
"version": "4.32.0", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.0.tgz",
"integrity": "sha512-qhFwQ+ljoymC+j5lXRv8DlaJYY/+8vyvYmVx074zrLsu5ZGWYsJNLjPPVJJjhZQpyAKUGPydOq9hRLLNvh1s3A==", "integrity": "sha512-yVh0Kf1f0Fq4tWNf6mWcbQBCLDpDrDEl88lzPgKhrgTcDrTtlmun92ywEF9dCjmYO3EFiSuJeeo9cYRxl2FswA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1189,9 +1189,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-arm64": { "node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.32.0", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.0.tgz",
"integrity": "sha512-44n/X3lAlWsEY6vF8CzgCx+LQaoqWGN7TzUfbJDiTIOjJm4+L2Yq+r5a8ytQRGyPqgJDs3Rgyo8eVL7n9iW6AQ==", "integrity": "sha512-gCs0ErAZ9s0Osejpc3qahTsqIPUDjSKIyxK/0BGKvL+Tn0n3Kwvj8BrCv7Y5sR1Ypz1K2qz9Ny0VvkVyoXBVUQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1203,9 +1203,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-x64": { "node_modules/@rollup/rollup-darwin-x64": {
"version": "4.32.0", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.0.tgz",
"integrity": "sha512-F9ct0+ZX5Np6+ZDztxiGCIvlCaW87HBdHcozUfsHnj1WCUTBUubAoanhHUfnUHZABlElyRikI0mgcw/qdEm2VQ==", "integrity": "sha512-aIB5Anc8hngk15t3GUkiO4pv42ykXHfmpXGS+CzM9CTyiWyT8HIS5ygRAy7KcFb/wiw4Br+vh1byqcHRTfq2tQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1217,9 +1217,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-arm64": { "node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.32.0", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.0.tgz",
"integrity": "sha512-JpsGxLBB2EFXBsTLHfkZDsXSpSmKD3VxXCgBQtlPcuAqB8TlqtLcbeMhxXQkCDv1avgwNjF8uEIbq5p+Cee0PA==", "integrity": "sha512-kpdsUdMlVJMRMaOf/tIvxk8TQdzHhY47imwmASOuMajg/GXpw8GKNd8LNwIHE5Yd1onehNpcUB9jHY6wgw9nHQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1231,9 +1231,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-x64": { "node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.32.0", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.0.tgz",
"integrity": "sha512-wegiyBT6rawdpvnD9lmbOpx5Sph+yVZKHbhnSP9MqUEDX08G4UzMU+D87jrazGE7lRSyTRs6NEYHtzfkJ3FjjQ==", "integrity": "sha512-D0RDyHygOBCQiqookcPevrvgEarN0CttBecG4chOeIYCNtlKHmf5oi5kAVpXV7qs0Xh/WO2RnxeicZPtT50V0g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1245,9 +1245,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": { "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.32.0", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.0.tgz",
"integrity": "sha512-3pA7xecItbgOs1A5H58dDvOUEboG5UfpTq3WzAdF54acBbUM+olDJAPkgj1GRJ4ZqE12DZ9/hNS2QZk166v92A==", "integrity": "sha512-mCIw8j5LPDXmCOW8mfMZwT6F/Kza03EnSr4wGYEswrEfjTfVsFOxvgYfuRMxTuUF/XmRb9WSMD5GhCWDe2iNrg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -1259,9 +1259,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-musleabihf": { "node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.32.0", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.0.tgz",
"integrity": "sha512-Y7XUZEVISGyge51QbYyYAEHwpGgmRrAxQXO3siyYo2kmaj72USSG8LtlQQgAtlGfxYiOwu+2BdbPjzEpcOpRmQ==", "integrity": "sha512-AwwldAu4aCJPob7zmjuDUMvvuatgs8B/QiVB0KwkUarAcPB3W+ToOT+18TQwY4z09Al7G0BvCcmLRop5zBLTag==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -1273,9 +1273,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-gnu": { "node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.32.0", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.0.tgz",
"integrity": "sha512-r7/OTF5MqeBrZo5omPXcTnjvv1GsrdH8a8RerARvDFiDwFpDVDnJyByYM/nX+mvks8XXsgPUxkwe/ltaX2VH7w==", "integrity": "sha512-e7kDUGVP+xw05pV65ZKb0zulRploU3gTu6qH1qL58PrULDGxULIS0OSDQJLH7WiFnpd3ZKUU4VM3u/Z7Zw+e7Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1287,9 +1287,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-musl": { "node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.32.0", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.0.tgz",
"integrity": "sha512-HJbifC9vex9NqnlodV2BHVFNuzKL5OnsV2dvTw6e1dpZKkNjPG6WUq+nhEYV6Hv2Bv++BXkwcyoGlXnPrjAKXw==", "integrity": "sha512-SXYJw3zpwHgaBqTXeAZ31qfW/v50wq4HhNVvKFhRr5MnptRX2Af4KebLWR1wpxGJtLgfS2hEPuALRIY3LPAAcA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1301,9 +1301,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-loongarch64-gnu": { "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.32.0", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.0.tgz",
"integrity": "sha512-VAEzZTD63YglFlWwRj3taofmkV1V3xhebDXffon7msNz4b14xKsz7utO6F8F4cqt8K/ktTl9rm88yryvDpsfOw==", "integrity": "sha512-e5XiCinINCI4RdyU3sFyBH4zzz7LiQRvHqDtRe9Dt8o/8hTBaYpdPimayF00eY2qy5j4PaaWK0azRgUench6WQ==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@ -1315,9 +1315,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": { "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.32.0", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.0.tgz",
"integrity": "sha512-Sts5DST1jXAc9YH/iik1C9QRsLcCoOScf3dfbY5i4kH9RJpKxiTBXqm7qU5O6zTXBTEZry69bGszr3SMgYmMcQ==", "integrity": "sha512-3SWN3e0bAsm9ToprLFBSro8nJe6YN+5xmB11N4FfNf92wvLye/+Rh5JGQtKOpwLKt6e61R1RBc9g+luLJsc23A==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -1329,9 +1329,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-gnu": { "node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.32.0", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.0.tgz",
"integrity": "sha512-qhlXeV9AqxIyY9/R1h1hBD6eMvQCO34ZmdYvry/K+/MBs6d1nRFLm6BOiITLVI+nFAAB9kUB6sdJRKyVHXnqZw==", "integrity": "sha512-B1Oqt3GLh7qmhvfnc2WQla4NuHlcxAD5LyueUi5WtMc76ZWY+6qDtQYqnxARx9r+7mDGfamD+8kTJO0pKUJeJA==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -1343,9 +1343,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-s390x-gnu": { "node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.32.0", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.0.tgz",
"integrity": "sha512-8ZGN7ExnV0qjXa155Rsfi6H8M4iBBwNLBM9lcVS+4NcSzOFaNqmt7djlox8pN1lWrRPMRRQ8NeDlozIGx3Omsw==", "integrity": "sha512-UfUCo0h/uj48Jq2lnhX0AOhZPSTAq3Eostas+XZ+GGk22pI+Op1Y6cxQ1JkUuKYu2iU+mXj1QjPrZm9nNWV9rg==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@ -1357,9 +1357,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-gnu": { "node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.32.0", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.0.tgz",
"integrity": "sha512-VDzNHtLLI5s7xd/VubyS10mq6TxvZBp+4NRWoW+Hi3tgV05RtVm4qK99+dClwTN1McA6PHwob6DEJ6PlXbY83A==", "integrity": "sha512-chZLTUIPbgcpm+Z7ALmomXW8Zh+wE2icrG+K6nt/HenPLmtwCajhQC5flNSk1Xy5EDMt/QAOz2MhzfOfJOLSiA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1371,9 +1371,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-musl": { "node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.32.0", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.0.tgz",
"integrity": "sha512-qcb9qYDlkxz9DxJo7SDhWxTWV1gFuwznjbTiov289pASxlfGbaOD54mgbs9+z94VwrXtKTu+2RqwlSTbiOqxGg==", "integrity": "sha512-jo0UolK70O28BifvEsFD/8r25shFezl0aUk2t0VJzREWHkq19e+pcLu4kX5HiVXNz5qqkD+aAq04Ct8rkxgbyQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1385,9 +1385,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-arm64-msvc": { "node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.32.0", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.0.tgz",
"integrity": "sha512-pFDdotFDMXW2AXVbfdUEfidPAk/OtwE/Hd4eYMTNVVaCQ6Yl8et0meDaKNL63L44Haxv4UExpv9ydSf3aSayDg==", "integrity": "sha512-Vmg0NhAap2S54JojJchiu5An54qa6t/oKT7LmDaWggpIcaiL8WcWHEN6OQrfTdL6mQ2GFyH7j2T5/3YPEDOOGA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1399,9 +1399,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-ia32-msvc": { "node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.32.0", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.0.tgz",
"integrity": "sha512-/TG7WfrCAjeRNDvI4+0AAMoHxea/USWhAzf9PVDFHbcqrQ7hMMKp4jZIy4VEjk72AAfN5k4TiSMRXRKf/0akSw==", "integrity": "sha512-CV2aqhDDOsABKHKhNcs1SZFryffQf8vK2XrxP6lxC99ELZAdvsDgPklIBfd65R8R+qvOm1SmLaZ/Fdq961+m7A==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -1413,9 +1413,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-msvc": { "node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.32.0", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.0.tgz",
"integrity": "sha512-5hqO5S3PTEO2E5VjCePxv40gIgyS2KvO7E7/vvC/NbIW4SIRamkMr1hqj+5Y67fbBWv/bQLB6KelBQmXlyCjWA==", "integrity": "sha512-g2ASy1QwHP88y5KWvblUolJz9rN+i4ZOsYzkEwcNfaNooxNUXG+ON6F5xFo0NIItpHqxcdAyls05VXpBnludGw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -2168,9 +2168,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001695", "version": "1.0.30001696",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001695.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001696.tgz",
"integrity": "sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==", "integrity": "sha512-pDCPkvzfa39ehJtJ+OwGT/2yvT2SbjfHhiIW2LWOAcMQ7BzwxT/XuyUp4OTOd0XFWA6BKw0JalnBHgSi5DGJBQ==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -2535,9 +2535,9 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.88", "version": "1.5.90",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.88.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.90.tgz",
"integrity": "sha512-K3C2qf1o+bGzbilTDCTBhTQcMS9KW60yTAaTeeXsfvQuTDDwlokLam/AdqlqcSy9u4UainDgsHV23ksXAOgamw==", "integrity": "sha512-C3PN4aydfW91Natdyd449Kw+BzhLmof6tzy5W1pFC5SpQxVXT+oyiyOG9AgYYSN9OdA/ik3YkCrpwqI8ug5Tug==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
@ -2743,9 +2743,9 @@
} }
}, },
"node_modules/fastq": { "node_modules/fastq": {
"version": "1.18.0", "version": "1.19.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz",
"integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
@ -3393,9 +3393,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/loupe": { "node_modules/loupe": {
"version": "3.1.2", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz",
"integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==", "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -3740,9 +3740,9 @@
"license": "BlueOak-1.0.0" "license": "BlueOak-1.0.0"
}, },
"node_modules/papaparse": { "node_modules/papaparse": {
"version": "5.5.1", "version": "5.5.2",
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.1.tgz", "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.2.tgz",
"integrity": "sha512-EuEKUhyxrHVozD7g3/ztsJn6qaKse8RPfR6buNB2dMJvdtXNhcw8jccVi/LxNEY3HVrV6GO6Z4OoeCG9Iy9wpA==", "integrity": "sha512-PZXg8UuAc4PcVwLosEEDYjPyfWnTEhOrUfdv+3Bx+NuAb+5NhDmXzg5fHWmdCh1mP5p7JAZfFr3IMQfcntNAdA==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -3827,9 +3827,9 @@
} }
}, },
"node_modules/phaser3-rex-plugins": { "node_modules/phaser3-rex-plugins": {
"version": "1.80.12", "version": "1.80.13",
"resolved": "https://registry.npmjs.org/phaser3-rex-plugins/-/phaser3-rex-plugins-1.80.12.tgz", "resolved": "https://registry.npmjs.org/phaser3-rex-plugins/-/phaser3-rex-plugins-1.80.13.tgz",
"integrity": "sha512-6vzYVhiSeiQSBBDXl2xQyF1omVhElF9yQTW7DKl0JHXchQ0bmvj4B+YVT5EFcle7y+3reedwOUDlchLvoccxsQ==", "integrity": "sha512-d2P3c+0r7h/GXtOZIjB6DLjBQ2rSEeZ+AOG1D6jgGGn/Txb+PiDmFMhBF3qJ6YoPlVarSE8lw6V8Jy7K7xPnLg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -4250,9 +4250,9 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.32.0", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.32.0.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.0.tgz",
"integrity": "sha512-JmrhfQR31Q4AuNBjjAX4s+a/Pu/Q8Q9iwjWBsjRH1q52SPFE2NqRMK6fUZKKnvKO6id+h7JIRf0oYsph53eATg==", "integrity": "sha512-+4C/cgJ9w6sudisA0nZz0+O7lTP9a3CzNLsoDwaRumM8QHwghUsu6tqHXiTmNUp/rqNiM14++7dkzHDyCRs0Jg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -4266,25 +4266,25 @@
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.32.0", "@rollup/rollup-android-arm-eabi": "4.34.0",
"@rollup/rollup-android-arm64": "4.32.0", "@rollup/rollup-android-arm64": "4.34.0",
"@rollup/rollup-darwin-arm64": "4.32.0", "@rollup/rollup-darwin-arm64": "4.34.0",
"@rollup/rollup-darwin-x64": "4.32.0", "@rollup/rollup-darwin-x64": "4.34.0",
"@rollup/rollup-freebsd-arm64": "4.32.0", "@rollup/rollup-freebsd-arm64": "4.34.0",
"@rollup/rollup-freebsd-x64": "4.32.0", "@rollup/rollup-freebsd-x64": "4.34.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.32.0", "@rollup/rollup-linux-arm-gnueabihf": "4.34.0",
"@rollup/rollup-linux-arm-musleabihf": "4.32.0", "@rollup/rollup-linux-arm-musleabihf": "4.34.0",
"@rollup/rollup-linux-arm64-gnu": "4.32.0", "@rollup/rollup-linux-arm64-gnu": "4.34.0",
"@rollup/rollup-linux-arm64-musl": "4.32.0", "@rollup/rollup-linux-arm64-musl": "4.34.0",
"@rollup/rollup-linux-loongarch64-gnu": "4.32.0", "@rollup/rollup-linux-loongarch64-gnu": "4.34.0",
"@rollup/rollup-linux-powerpc64le-gnu": "4.32.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.34.0",
"@rollup/rollup-linux-riscv64-gnu": "4.32.0", "@rollup/rollup-linux-riscv64-gnu": "4.34.0",
"@rollup/rollup-linux-s390x-gnu": "4.32.0", "@rollup/rollup-linux-s390x-gnu": "4.34.0",
"@rollup/rollup-linux-x64-gnu": "4.32.0", "@rollup/rollup-linux-x64-gnu": "4.34.0",
"@rollup/rollup-linux-x64-musl": "4.32.0", "@rollup/rollup-linux-x64-musl": "4.34.0",
"@rollup/rollup-win32-arm64-msvc": "4.32.0", "@rollup/rollup-win32-arm64-msvc": "4.34.0",
"@rollup/rollup-win32-ia32-msvc": "4.32.0", "@rollup/rollup-win32-ia32-msvc": "4.34.0",
"@rollup/rollup-win32-x64-msvc": "4.32.0", "@rollup/rollup-win32-x64-msvc": "4.34.0",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
@ -4361,9 +4361,9 @@
} }
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "7.6.3", "version": "7.7.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.0.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "integrity": "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 453 KiB

After

Width:  |  Height:  |  Size: 454 KiB

View File

@ -2,7 +2,7 @@
<Debug /> <Debug />
<Notifications /> <Notifications />
<BackgroundImageLoader /> <BackgroundImageLoader />
<GmPanel v-if="gameStore.character?.role === 'gm'" /> <GmPanel v-if="gameStore.character?.role === 'gm'" @open-map-editor="mapEditor.toggleActive" />
<component :is="currentScreen" /> <component :is="currentScreen" />
</template> </template>
@ -16,25 +16,26 @@ import MapEditor from '@/components/screens/MapEditor.vue'
import BackgroundImageLoader from '@/components/utilities/BackgroundImageLoader.vue' import BackgroundImageLoader from '@/components/utilities/BackgroundImageLoader.vue'
import Debug from '@/components/utilities/Debug.vue' import Debug from '@/components/utilities/Debug.vue'
import Notifications from '@/components/utilities/Notifications.vue' import Notifications from '@/components/utilities/Notifications.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore' import { useMapEditorStore } from '@/stores/mapEditorStore'
import { computed, watch } from 'vue' import { computed, ref, useTemplateRef, watch } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const mapEditorStore = useMapEditorStore() const mapEditor = useMapEditorComposable()
const currentScreen = computed(() => { const currentScreen = computed(() => {
if (!gameStore.game.isLoaded) return Loading if (!gameStore.game.isLoaded) return Loading
if (!gameStore.connection) return Login if (!gameStore.connection) return Login
if (!gameStore.token) return Login if (!gameStore.token) return Login
if (!gameStore.character) return Characters if (!gameStore.character) return Characters
if (mapEditorStore.active) return MapEditor if (mapEditor.active.value) return MapEditor
return Game return Game
}) })
// Watch mapEditorStore.active and empty gameStore.game.loadedAssets // Watch mapEditor.active and empty gameStore.game.loadedAssets
watch( watch(
() => mapEditorStore.active, () => mapEditor.active.value,
() => { () => {
gameStore.game.loadedTextures = [] gameStore.game.loadedTextures = []
} }

5
src/application/enums.ts Normal file
View File

@ -0,0 +1,5 @@
export enum Direction {
POSITIVE,
NEGATIVE,
UNCHANGED
}

View File

@ -19,7 +19,6 @@ export type TextureData = {
updatedAt: Date updatedAt: Date
originX?: number originX?: number
originY?: number originY?: number
isAnimated?: boolean
frameRate?: number frameRate?: number
frameWidth?: number frameWidth?: number
frameHeight?: number frameHeight?: number
@ -40,7 +39,6 @@ export type MapObject = {
tags: any | null tags: any | null
originX: number originX: number
originY: number originY: number
isAnimated: boolean
frameRate: number frameRate: number
frameWidth: number frameWidth: number
frameHeight: number frameHeight: number
@ -68,7 +66,7 @@ export type Map = {
name: string name: string
width: number width: number
height: number height: number
tiles: any | null tiles: string[][]
pvp: boolean pvp: boolean
mapEffects: MapEffect[] mapEffects: MapEffect[]
mapEventTiles: MapEventTile[] mapEventTiles: MapEventTile[]
@ -105,7 +103,7 @@ export enum MapEventTileType {
export type MapEventTile = { export type MapEventTile = {
id: UUID id: UUID
map: Map mapId: UUID
type: MapEventTileType type: MapEventTileType
positionX: number positionX: number
positionY: number positionY: number
@ -185,6 +183,7 @@ export type Character = {
export type MapCharacter = { export type MapCharacter = {
character: Character character: Character
isMoving: boolean isMoving: boolean
isAttacking?: boolean
} }
export type CharacterItem = { export type CharacterItem = {
@ -218,22 +217,28 @@ export type Sprite = {
characterTypes: CharacterType[] characterTypes: CharacterType[]
} }
export interface SpriteImage {
url: string
offset: {
x: number
y: number
}
}
export type SpriteAction = { export type SpriteAction = {
id: UUID id: string
sprite: Sprite sprite: string
action: string action: string
sprites: string[] sprites: SpriteImage[]
originX: number originX: number
originY: number originY: number
isAnimated: boolean
isLooping: boolean
frameWidth: number frameWidth: number
frameHeight: number frameHeight: number
frameRate: number frameRate: number
} }
export type Chat = { export type Chat = {
id: UUID id: string
character: Character character: Character
map: Map map: Map
message: string message: string

View File

@ -1,126 +1,45 @@
<template> <template>
<ChatBubble :mapCharacter="props.mapCharacter" :currentX="currentPositionX" :currentY="currentPositionY" /> <Container ref="characterContainer" :x="currentPositionX" :y="currentPositionY" :depth="isometricDepth">
<Healthbar :mapCharacter="props.mapCharacter" :currentX="currentPositionX" :currentY="currentPositionY" /> <ChatBubble :mapCharacter="props.mapCharacter" />
<Container ref="charContainer" :depth="isometricDepth" :x="currentPositionX" :y="currentPositionY"> <HealthBar :mapCharacter="props.mapCharacter" />
<Sprite ref="charSprite" :origin-y="1" :flipX="isFlippedX" /> <CharacterHair :mapCharacter="props.mapCharacter" />
<Sprite ref="characterSprite" :origin-y="1" :flipX="isFlippedX" />
</Container> </Container>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import config from '@/application/config' import { Direction } from '@/application/enums'
import { type MapCharacter } from '@/application/types' import { type MapCharacter } from '@/application/types'
import CharacterHair from '@/components/game/character/partials/CharacterHair.vue'
import ChatBubble from '@/components/game/character/partials/ChatBubble.vue' import ChatBubble from '@/components/game/character/partials/ChatBubble.vue'
import Healthbar from '@/components/game/character/partials/Healthbar.vue' import HealthBar from '@/components/game/character/partials/HealthBar.vue'
import { loadSpriteTextures } from '@/composables/gameComposable' import { useCharacterSprite } from '@/composables/useCharacterSpriteComposable'
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/mapComposable'
import { CharacterTypeStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapStore } from '@/stores/mapStore' import { useMapStore } from '@/stores/mapStore'
import { Container, refObj, Sprite, useScene } from 'phavuer' import { Container, Sprite, useScene } from 'phavuer'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue' import { onMounted, onUnmounted, watch } from 'vue'
enum Direction {
POSITIVE,
NEGATIVE,
UNCHANGED
}
const props = defineProps<{ const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap tilemap: Phaser.Tilemaps.Tilemap
mapCharacter: MapCharacter mapCharacter: MapCharacter
}>() }>()
const charContainer = refObj<Phaser.GameObjects.Container>()
const charSprite = refObj<Phaser.GameObjects.Sprite>()
const charSpriteId = ref('')
const gameStore = useGameStore() const gameStore = useGameStore()
const mapStore = useMapStore() const mapStore = useMapStore()
const scene = useScene() const scene = useScene()
const currentPositionX = ref(0) const { characterContainer, characterSprite, currentPositionX, currentPositionY, isometricDepth, isFlippedX, updatePosition, calcDirection, updateSprite, initializeSprite, cleanup } = useCharacterSprite(scene, props.tilemap, props.mapCharacter)
const currentPositionY = ref(0)
const isometricDepth = ref(1)
const isInitialPosition = ref(true)
const tween = ref<Phaser.Tweens.Tween | null>(null)
const updateIsometricDepth = (positionX: number, positionY: number) => { const handlePositionUpdate = (newValues: any, oldValues: any) => {
isometricDepth.value = calculateIsometricDepth(positionX, positionY, 28, 94, true) if (!newValues) return
}
const updatePosition = (positionX: number, positionY: number, direction: Direction) => { if (!oldValues || newValues.positionX !== oldValues.positionX || newValues.positionY !== oldValues.positionY) {
const newPositionX = tileToWorldX(props.tilemap, positionX, positionY) const direction = !oldValues ? Direction.POSITIVE : calcDirection(oldValues.positionX, oldValues.positionY, newValues.positionX, newValues.positionY)
const newPositionY = tileToWorldY(props.tilemap, positionX, positionY) updatePosition(newValues.positionX, newValues.positionY, direction)
if (isInitialPosition.value) {
currentPositionX.value = newPositionX
currentPositionY.value = newPositionY
isInitialPosition.value = false
return
} }
if (tween.value?.isPlaying()) { if (newValues.isMoving !== oldValues?.isMoving || newValues.rotation !== oldValues?.rotation) {
tween.value.stop() updateSprite()
}
const distance = Math.sqrt(Math.pow(newPositionX - currentPositionX.value, 2) + Math.pow(newPositionY - currentPositionY.value, 2))
if (distance >= config.tile_size.width / 1.1) {
currentPositionX.value = newPositionX
currentPositionY.value = newPositionY
return
}
const duration = distance * 5.7
tween.value = props.tilemap.scene.tweens.add({
targets: { x: currentPositionX.value, y: currentPositionY.value },
x: newPositionX,
y: newPositionY,
duration,
ease: 'Linear',
onStart: () => {
if (direction === Direction.POSITIVE) {
updateIsometricDepth(positionX, positionY)
}
},
onUpdate: (tween) => {
// @ts-ignore
currentPositionX.value = tween.targets[0].x
// @ts-ignore
currentPositionY.value = tween.targets[0].y
},
onComplete: () => {
if (direction === Direction.NEGATIVE) {
updateIsometricDepth(positionX, positionY)
}
}
})
}
const calcDirection = (oldPositionX: number, oldPositionY: number, newPositionX: number, newPositionY: number): Direction => {
if (newPositionY < oldPositionY || newPositionX < oldPositionX) return Direction.NEGATIVE
if (newPositionX > oldPositionX || newPositionY > oldPositionY) return Direction.POSITIVE
return Direction.UNCHANGED
}
const isFlippedX = computed(() => [6, 4].includes(props.mapCharacter.character.rotation ?? 0))
const charTexture = computed(() => {
const spriteId = charSpriteId.value ?? 'idle_right_down'
const action = props.mapCharacter.isMoving ? 'walk' : 'idle'
const direction = [0, 6].includes(props.mapCharacter.character.rotation) ? 'left_up' : 'right_down'
return `${spriteId}-${action}_${direction}`
})
const updateSprite = () => {
if (props.mapCharacter.isMoving) {
charSprite.value!.anims.play(charTexture.value, true)
} else {
charSprite.value!.anims.stop()
charSprite.value!.setFrame(0)
charSprite.value!.setTexture(charTexture.value)
} }
} }
@ -129,49 +48,22 @@ watch(
positionX: props.mapCharacter.character.positionX, positionX: props.mapCharacter.character.positionX,
positionY: props.mapCharacter.character.positionY, positionY: props.mapCharacter.character.positionY,
isMoving: props.mapCharacter.isMoving, isMoving: props.mapCharacter.isMoving,
rotation: props.mapCharacter.character.rotation rotation: props.mapCharacter.character.rotation,
isAttacking: props.mapCharacter.isAttacking
}), }),
(newValues, oldValues) => { handlePositionUpdate
if (!newValues) return
if (!oldValues || newValues.positionX !== oldValues.positionX || newValues.positionY !== oldValues.positionY) {
const direction = !oldValues ? Direction.POSITIVE : calcDirection(oldValues.positionX, oldValues.positionY, newValues.positionX, newValues.positionY)
updatePosition(newValues.positionX, newValues.positionY, direction)
}
// Handle animation updates
if (newValues.isMoving !== oldValues?.isMoving || newValues.rotation !== oldValues?.rotation) {
updateSprite()
}
}
) )
onMounted(async () => { onMounted(async () => {
const characterTypeStorage = new CharacterTypeStorage() await initializeSprite()
const spriteId = await characterTypeStorage.getSpriteId(props.mapCharacter.character.characterType!)
if (!spriteId) return
charSpriteId.value = spriteId
await loadSpriteTextures(scene, spriteId)
charSprite.value!.setTexture(charTexture.value)
charSprite.value!.setFlipX(isFlippedX.value)
charContainer.value!.setName(props.mapCharacter.character!.name)
if (props.mapCharacter.character.id === gameStore.character!.id) { if (props.mapCharacter.character.id === gameStore.character!.id) {
mapStore.setCharacterLoaded(true) mapStore.setCharacterLoaded(true)
scene.cameras.main.startFollow(characterContainer.value as Phaser.GameObjects.Container)
// #146 : Set camera position to character, need to be improved still
scene.cameras.main.startFollow(charContainer.value as Phaser.GameObjects.Container)
} }
updatePosition(props.mapCharacter.character.positionX, props.mapCharacter.character.positionY, props.mapCharacter.character.rotation)
}) })
onUnmounted(() => { onUnmounted(() => {
tween.value?.stop() cleanup()
}) })
</script> </script>

View File

@ -1,51 +1,54 @@
<template> <template>
<Image v-bind="imageProps" v-if="gameStore.getLoadedAsset(texture)" /> <Image v-bind="imageProps" v-if="gameStore.isTextureLoaded(texture)" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { MapCharacter, Sprite as SpriteT } from '@/application/types' import type { MapCharacter, Sprite as SpriteT } from '@/application/types'
import { loadSpriteTextures } from '@/composables/gameComposable' import { loadSpriteTextures } from '@/composables/gameComposable'
import { CharacterHairStorage, CharacterTypeStorage, SpriteStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { Image, useScene } from 'phavuer' import { Image, useScene } from 'phavuer'
import { computed } from 'vue' import { computed, onMounted, ref } from 'vue'
const props = defineProps<{ const props = defineProps<{
mapCharacter: MapCharacter mapCharacter: MapCharacter
currentX: number
currentY: number
}>() }>()
const gameStore = useGameStore() const gameStore = useGameStore()
const scene = useScene() const scene = useScene()
const hairSpriteId = ref('')
const sprite = ref<SpriteT | null>(null)
const texture = computed(() => { const texture = computed(() => {
const { rotation, characterHair } = props.mapCharacter.character const { rotation } = props.mapCharacter.character
const spriteId = characterHair?.sprite?.id
const direction = [0, 6].includes(rotation) ? 'back' : 'front' const direction = [0, 6].includes(rotation) ? 'back' : 'front'
return `${spriteId}-${direction}` return `${hairSpriteId.value}-${direction}`
}) })
const isFlippedX = computed(() => [6, 4].includes(props.mapCharacter.character.rotation ?? 0)) const isFlippedX = computed(() => [6, 4].includes(props.mapCharacter.character.rotation ?? 0))
const imageProps = computed(() => { const imageProps = computed(() => {
// Get the current sprite action based on direction
const direction = [0, 6].includes(props.mapCharacter.character.rotation ?? 0) ? 'back' : 'front' const direction = [0, 6].includes(props.mapCharacter.character.rotation ?? 0) ? 'back' : 'front'
const spriteAction = props.mapCharacter.character.characterHair?.sprite?.spriteActions?.find((spriteAction) => spriteAction.action === direction) const spriteAction = sprite.value?.spriteActions?.find((spriteAction) => spriteAction.action === direction)
return { return {
depth: 1, depth: 9999,
originX: Number(spriteAction?.originX) ?? 0, originX: Number(spriteAction?.originX) ?? 0,
originY: Number(spriteAction?.originY) ?? 0, originY: Number(spriteAction?.originY) ?? 0,
flipX: isFlippedX.value, flipX: isFlippedX.value,
texture: texture.value, texture: texture.value
y: props.mapCharacter.isMoving ? Math.floor(Date.now() / 250) % 2 : 0
} }
}) })
loadSpriteTextures(scene, props.mapCharacter.character.characterHair?.sprite as SpriteT) onMounted(async () => {
.then(() => {}) const characterHairStorage = new CharacterHairStorage()
.catch((error) => { const spriteId = await characterHairStorage.getSpriteId(props.mapCharacter.character.characterHair!)
console.error('Error loading texture:', error) if (!spriteId) return
})
hairSpriteId.value = spriteId
const spriteStorage = new SpriteStorage()
sprite.value = await spriteStorage.get(spriteId)
await loadSpriteTextures(scene, spriteId)
})
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<Container ref="charChatContainer" :depth="999" :x="currentX" :y="currentY"> <Container ref="characterChatContainer" :depth="999">
<RoundRectangle @create="createChatBubble" :origin-x="0.5" :origin-y="7.5" :fillColor="0xffffff" :width="194" :height="21" :radius="20" /> <RoundRectangle @create="createChatBubble" :origin-x="0.5" :origin-y="7.5" :fillColor="0xffffff" :width="194" :height="21" :radius="20" />
<Text @create="createChatText" :style="{ fontSize: 13, fontFamily: 'Arial', color: '#000' }" /> <Text @create="createChatText" :style="{ fontSize: 13, fontFamily: 'Arial', color: '#000' }" />
</Container> </Container>
@ -12,12 +12,10 @@ import { onMounted } from 'vue'
const props = defineProps<{ const props = defineProps<{
mapCharacter: MapCharacter mapCharacter: MapCharacter
currentX: number
currentY: number
}>() }>()
const game = useGame() const game = useGame()
const charChatContainer = refObj<Phaser.GameObjects.Container>() const characterChatContainer = refObj<Phaser.GameObjects.Container>()
const createChatBubble = (container: Phaser.GameObjects.Container) => { const createChatBubble = (container: Phaser.GameObjects.Container) => {
container.setName(`${props.mapCharacter.character.name}_chatBubble`) container.setName(`${props.mapCharacter.character.name}_chatBubble`)
@ -41,7 +39,7 @@ const createChatText = (text: Phaser.GameObjects.Text) => {
} }
onMounted(() => { onMounted(() => {
charChatContainer.value!.setName(`${props.mapCharacter.character!.name}_chatContainer`) characterChatContainer.value!.setName(`${props.mapCharacter.character!.name}_chatContainer`)
charChatContainer.value!.setVisible(false) characterChatContainer.value!.setVisible(false)
}) })
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<Container :depth="999" :x="currentX" :y="currentY"> <Container :depth="999">
<Text @create="createNicknameText" :text="props.mapCharacter.character.name" /> <Text @create="createNicknameText" :text="props.mapCharacter.character.name" />
<RoundRectangle :origin-x="0.5" :origin-y="18.5" :fillColor="0xffffff" :width="74" :height="6" :radius="5" /> <RoundRectangle :origin-x="0.5" :origin-y="18.5" :fillColor="0xffffff" :width="74" :height="6" :radius="5" />
<RoundRectangle :origin-x="0.5" :origin-y="36.4" :fillColor="0x00b3b3" :width="70" :height="3" :radius="5" /> <RoundRectangle :origin-x="0.5" :origin-y="36.4" :fillColor="0x00b3b3" :width="70" :height="3" :radius="5" />
@ -12,8 +12,6 @@ import { Container, RoundRectangle, Text, useGame } from 'phavuer'
const props = defineProps<{ const props = defineProps<{
mapCharacter: MapCharacter mapCharacter: MapCharacter
currentX: number
currentY: number
}>() }>()
const game = useGame() const game = useGame()

View File

@ -1,12 +1,12 @@
<template> <template>
<div class="absolute" v-if="gameStore.uiSettings.isCharacterProfileOpen" :style="modalStyle"> <div class="absolute" v-if="gameStore.uiSettings.isCharacterProfileOpen" :style="modalStyle">
<div class="relative"> <div class="relative">
<img src="/assets/ui-elements/profile-ui-box-outer.svg" class="absolute w-full h-full" /> <img src="/assets/ui-elements/profile-ui-box-outer.svg" class="absolute w-full h-full" alt="" />
<img src="/assets/ui-elements/profile-ui-box-inner.svg" class="absolute left-2 bottom-2 w-[calc(100%_-_16px)] h-[calc(100%_-_40px)]" /> <img src="/assets/ui-elements/profile-ui-box-inner.svg" class="absolute left-2 bottom-2 w-[calc(100%_-_16px)] h-[calc(100%_-_40px)]" alt="" />
<div @mousedown="startDrag" class="cursor-move px-5 py-2.5 flex justify-between items-center relative"> <div @mousedown="startDrag" class="cursor-move px-5 py-2.5 flex justify-between items-center relative">
<span class="text-xs text-white font-thin">Character Profile [Alt+C]</span> <span class="text-xs text-white font-thin">Character Profile [Alt+C]</span>
<button @click="gameStore.uiSettings.isCharacterProfileOpen = false" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out"> <button @click="gameStore.uiSettings.isCharacterProfileOpen = false" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
<img alt="close" draggable="false" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" /> <img draggable="false" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" alt="Close button icon" />
</button> </button>
</div> </div>
<div class="py-4 px-6 flex flex-col gap-7 relative z-10"> <div class="py-4 px-6 flex flex-col gap-7 relative z-10">
@ -17,7 +17,7 @@
<span class="text-xs">{{ gameStore.character?.experience }} / 18.600XP</span> <span class="text-xs">{{ gameStore.character?.experience }} / 18.600XP</span>
</div> </div>
<a class="hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured-small.svg')] bg-no-repeat block w-8 h-8 relative mx-[3px]"> <a class="hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured-small.svg')] bg-no-repeat block w-8 h-8 relative mx-[3px]">
<img class="hover:drop-shadow-default w-3.5 h-3.5 m-[9px] object-contain" draggable="false" src="/assets/icons/plus-green-icon.svg" /> <img class="hover:drop-shadow-default w-3.5 h-3.5 m-[9px] object-contain" draggable="false" src="/assets/icons/plus-green-icon.svg" alt="Plus button icon" />
</a> </a>
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
@ -37,20 +37,20 @@
</div> </div>
</div> </div>
</div> </div>
<img src="/assets/placeholders/inventory_player.png" class="w-8 h-auto" /> <img src="/assets/placeholders/inventory_player.png" class="w-8 h-auto" alt="Player character sprite" />
<div class="flex flex-col items-end gap-0.5"> <div class="flex flex-col items-end gap-0.5">
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600"> <div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
<img class="w-6 h-6 center-element" src="/assets/icons/profile/helmet.svg" /> <img class="w-6 h-6 center-element" src="/assets/icons/profile/helmet.svg" alt="Helmet icon" />
</div> </div>
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600"> <div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
<img class="w-6 h-6 center-element" src="/assets/icons/profile/chestplate.svg" /> <img class="w-6 h-6 center-element" src="/assets/icons/profile/chestplate.svg" alt="Chestplate icon" />
</div> </div>
<div class="flex gap-0.5 items-end"> <div class="flex gap-0.5 items-end">
<div class="w-6 h-6 default-border rounded-sm bg-gray relative hover:bg-gray-600"> <div class="w-6 h-6 default-border rounded-sm bg-gray relative hover:bg-gray-600">
<img class="w-4 h-4 center-element" src="/assets/icons/profile/boots.svg" /> <img class="w-4 h-4 center-element" src="/assets/icons/profile/boots.svg" alt="Boots icon" />
</div> </div>
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600"> <div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
<img class="w-6 h-6 center-element" src="/assets/icons/profile/legs.svg" /> <img class="w-6 h-6 center-element" src="/assets/icons/profile/legs.svg" alt="Legs icon" />
</div> </div>
</div> </div>
</div> </div>
@ -119,111 +119,44 @@
<script setup lang="ts"> <script setup lang="ts">
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
let startX = 0 const width = ref(286)
let startY = 0 const height = ref(483)
let initialX = 0 const x = ref(window.innerWidth / 2 - 143)
let initialY = 0 const y = ref(window.innerHeight / 2 - 241)
let modalPositionX = 0
let modalPositionY = 0
let modalWidth = 286
let modalHeight = 483
const width = ref(modalWidth)
const height = ref(modalHeight)
const x = ref(0)
const y = ref(0)
const isDragging = ref(false) const isDragging = ref(false)
const modalStyle = computed(() => ({ const modalStyle = computed(() => ({
top: `${y.value}px`, top: `${y.value}px`,
left: `${x.value}px`, left: `${x.value}px`,
width: `${width.value}px`, width: `${width.value}px`,
height: `${height.value}px`, height: `${height.value}px`
maxWidth: '100vw',
maxHeight: '100vh'
})) }))
function startDrag(event: MouseEvent) { function startDrag(event: MouseEvent) {
isDragging.value = true isDragging.value = true
startX = event.clientX const startX = event.clientX - x.value
startY = event.clientY const startY = event.clientY - y.value
initialX = x.value
initialY = y.value function drag(event: MouseEvent) {
event.preventDefault() if (!isDragging.value) return
x.value = event.clientX - startX
y.value = event.clientY - startY
}
function stopDrag() {
isDragging.value = false
removeEventListener('mousemove', drag)
removeEventListener('mouseup', stopDrag)
}
addEventListener('mousemove', drag)
addEventListener('mouseup', stopDrag)
} }
function drag(event: MouseEvent) {
if (!isDragging.value) return
const dx = event.clientX - startX
const dy = event.clientY - startY
x.value = initialX + dx
y.value = initialY + dy
adjustPosition()
}
function stopDrag() {
isDragging.value = false
}
function adjustPosition() {
x.value = Math.min(x.value, window.innerWidth - width.value)
y.value = Math.min(y.value, window.innerHeight - height.value)
}
function initializePosition() {
width.value = Math.min(modalWidth, window.innerWidth)
height.value = Math.min(modalHeight, window.innerHeight)
if (modalPositionX !== 0 && modalPositionY !== 0) {
x.value = modalPositionX
y.value = modalPositionY
} else {
x.value = (window.innerWidth - width.value) / 2
y.value = (window.innerHeight - height.value) / 2
}
}
watch(
() => gameStore.uiSettings.isCharacterProfileOpen,
(value) => {
gameStore.uiSettings.isCharacterProfileOpen = value
if (value) {
initializePosition()
}
}
)
watch(
() => modalWidth,
(value) => {
width.value = Math.min(value, window.innerWidth)
}
)
watch(
() => modalHeight,
(value) => {
height.value = Math.min(value, window.innerHeight)
}
)
watch(
() => modalPositionX,
(value) => {
x.value = value
}
)
watch(
() => modalPositionY,
(value) => {
y.value = value
}
)
function keyPress(event: KeyboardEvent) { function keyPress(event: KeyboardEvent) {
if (event.altKey && event.key === 'c') { if (event.altKey && event.key === 'c') {
gameStore.toggleCharacterProfile() gameStore.toggleCharacterProfile()
@ -232,14 +165,9 @@ function keyPress(event: KeyboardEvent) {
onMounted(() => { onMounted(() => {
addEventListener('keydown', keyPress) addEventListener('keydown', keyPress)
addEventListener('mousemove', drag)
addEventListener('mouseup', stopDrag)
initializePosition()
}) })
onUnmounted(() => { onUnmounted(() => {
removeEventListener('keydown', keyPress) removeEventListener('keydown', keyPress)
removeEventListener('mousemove', drag)
removeEventListener('mouseup', stopDrag)
}) })
</script> </script>

View File

@ -7,7 +7,7 @@
</div> </div>
</div> </div>
<div class="w-96 mx-auto relative"> <div class="w-96 mx-auto relative">
<img src="/assets/icons/ingameUI/chat-icon.svg" class="absolute top-1/2 -translate-y-1/2 left-2.5 h-4 w-4 opacity-50" /> <img src="/assets/icons/ingameUI/chat-icon.svg" class="absolute top-1/2 -translate-y-1/2 left-2.5 h-4 w-4 opacity-50" alt="" />
<input <input
class="w-[332px] h-8 rounded-sm text-xs font-default pl-8 pr-4 py-0 bg-gray-600 border-2 border-solid border-gray-500 text-gray-300 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover focus:outline-none focus:ring-0 focus:border-cyan-800" class="w-[332px] h-8 rounded-sm text-xs font-default pl-8 pr-4 py-0 bg-gray-600 border-2 border-solid border-gray-500 text-gray-300 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover focus:outline-none focus:ring-0 focus:border-cyan-800"
placeholder="Type something..." placeholder="Type something..."
@ -85,11 +85,14 @@ gameStore.connection?.on('chat:message', (data: Chat) => {
if (!mapStore.characterLoaded) return if (!mapStore.characterLoaded) return
const charChatContainer = scene.children.getByName(data.character.name + '_chatContainer') as Phaser.GameObjects.Container const characterContainer = scene.children.getByName(data.character.name) as Phaser.GameObjects.Container
if (!charChatContainer) return if (!characterContainer) return
const chatBubble = charChatContainer.getByName(data.character.name + '_chatBubble') as Phaser.GameObjects.Container const characterChatContainer = characterContainer.getByName(data.character.name + '_chatContainer') as Phaser.GameObjects.Container
const chatText = charChatContainer.getByName(data.character.name + '_chatText') as Phaser.GameObjects.Text if (!characterChatContainer) return
const chatBubble = characterChatContainer.getByName(data.character.name + '_chatBubble') as Phaser.GameObjects.Container
const chatText = characterChatContainer.getByName(data.character.name + '_chatText') as Phaser.GameObjects.Text
if (!chatText || !chatBubble) return if (!chatText || !chatBubble) return
function calculateTextWidth(text: string, font: string, fontSize: number): number { function calculateTextWidth(text: string, font: string, fontSize: number): number {
@ -115,24 +118,24 @@ gameStore.connection?.on('chat:message', (data: Chat) => {
// setText but with max. char limit of 90 // setText but with max. char limit of 90
chatText.setText(data.message.substring(0, 90)) chatText.setText(data.message.substring(0, 90))
charChatContainer.setVisible(true) characterChatContainer.setVisible(true)
/** /**
* Hide chat bubble after a few seconds * Hide chat bubble after a few seconds
*/ */
// Clear any existing hide timer // Clear any existing hide timer
if (charChatContainer.getData('hideTimer')) { if (characterChatContainer.getData('hideTimer')) {
clearTimeout(charChatContainer.getData('hideTimer')) clearTimeout(characterChatContainer.getData('hideTimer'))
} }
// Set a new hide timer // Set a new hide timer
const hideTimer = setTimeout(() => { const hideTimer = setTimeout(() => {
charChatContainer.setVisible(false) characterChatContainer.setVisible(false)
}, 3000) }, 3000)
// Store the timer on the container itself // Store the timer on the container itself
charChatContainer.setData('hideTimer', hideTimer) characterChatContainer.setData('hideTimer', hideTimer)
}) })
scrollToBottom() scrollToBottom()

View File

@ -6,7 +6,7 @@
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div> <div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
</div> </div>
<a class="group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured.svg')] bg-no-repeat block w-[42px] h-[42px] relative"> <a class="group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured.svg')] bg-no-repeat block w-[42px] h-[42px] relative">
<img class="group-hover:drop-shadow-default w-6 h-5 mx-[9px] my-[11px] object-contain" draggable="false" src="/assets/icons/ingameUI/menu-icon.svg" /> <img class="group-hover:drop-shadow-default w-6 h-5 mx-[9px] my-[11px] object-contain" draggable="false" src="/assets/icons/ingameUI/menu-icon.svg" alt="Menu button icon" />
</a> </a>
</li> </li>
<li class="menu-item group relative" @click="gameStore.toggleCharacterProfile"> <li class="menu-item group relative" @click="gameStore.toggleCharacterProfile">
@ -15,7 +15,7 @@
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div> <div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
</div> </div>
<a class="group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured.svg')] bg-no-repeat block w-[42px] h-[42px] relative"> <a class="group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured.svg')] bg-no-repeat block w-[42px] h-[42px] relative">
<img class="group-hover:drop-shadow-default w-8 h-8 m-[5px] object-contain" draggable="false" src="/assets/placeholders/head.png" /> <img class="group-hover:drop-shadow-default w-8 h-8 m-[5px] object-contain" draggable="false" src="/assets/placeholders/head.png" alt="User profile button icon" />
<p class="absolute bottom-0 -right-1.5 m-0 max-w-4 font-ui z-10 text-white text-[12px] leading-[6px] drop-shadow-pixel"><span class="font-ui text-white text-[8px] ml-0.5">LVL</span> {{ characterLevel }}</p> <p class="absolute bottom-0 -right-1.5 m-0 max-w-4 font-ui z-10 text-white text-[12px] leading-[6px] drop-shadow-pixel"><span class="font-ui text-white text-[8px] ml-0.5">LVL</span> {{ characterLevel }}</p>
</a> </a>
</li> </li>
@ -25,7 +25,7 @@
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div> <div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
</div> </div>
<a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border-2 border-solid rounded border-gray-500 block w-[34px] h-[31px]"> <a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border-2 border-solid rounded border-gray-500 block w-[34px] h-[31px]">
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/chat-icon.svg" /> <img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/chat-icon.svg" alt="Open chat button icon" />
</a> </a>
</li> </li>
<li class="menu-item group relative"> <li class="menu-item group relative">
@ -34,7 +34,7 @@
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div> <div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
</div> </div>
<a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border-2 border-solid rounded border-gray-500 block w-[34px] h-[31px]"> <a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border-2 border-solid rounded border-gray-500 block w-[34px] h-[31px]">
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/map-icon.svg" /> <img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/map-icon.svg" alt="World map button icon" />
</a> </a>
</li> </li>
<li class="menu-item group relative"> <li class="menu-item group relative">
@ -43,7 +43,7 @@
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div> <div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
</div> </div>
<a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border-2 border-solid rounded border-gray-500 block w-[34px] h-[31px]"> <a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border-2 border-solid rounded border-gray-500 block w-[34px] h-[31px]">
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/socials-icon.svg" /> <img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/socials-icon.svg" alt="Users button icon" />
</a> </a>
</li> </li>
</ul> </ul>

View File

@ -5,12 +5,12 @@
</div> </div>
<div class="absolute -bottom-3 left-1/2 -translate-x-1/2 flex gap-1"> <div class="absolute -bottom-3 left-1/2 -translate-x-1/2 flex gap-1">
<button class="w-6 h-6 relative p-0"> <button class="w-6 h-6 relative p-0">
<img class="w-3 h-3 center-element" src="/assets/icons/plus-icon.svg" /> <img class="w-3 h-3 center-element" src="/assets/icons/plus-icon.svg" alt="Zoom-in button icon" />
<img class="w-full h-full" src="/assets/ui-elements/button-ui-box-textured.svg" /> <img class="w-full h-full" src="/assets/ui-elements/button-ui-box-textured.svg" alt="" />
</button> </button>
<button class="w-6 h-6 relative p-0"> <button class="w-6 h-6 relative p-0">
<img class="w-3 h-3 center-element" src="/assets/icons/minus-icon.svg" /> <img class="w-3 h-3 center-element" src="/assets/icons/minus-icon.svg" alt="Zoom-out button icon" />
<img class="w-full h-full" src="/assets/ui-elements/button-ui-box-textured.svg" /> <img class="w-full h-full" src="/assets/ui-elements/button-ui-box-textured.svg" alt="" />
</button> </button>
</div> </div>
</div> </div>

View File

@ -1,42 +0,0 @@
<template>
<div class="absolute z-50 w-full h-dvh top-0 left-0 bg-black/60" v-show="false">
<div class="center-element max-w-[875px] max-h-[600px] h-full w-[80%] bg-gray border-solid border-2 border-gray-500 rounded-md z-50 flex flex-col backdrop-blur-sm shadow-lg">
<div class="p-2.5 flex max-sm:flex-wrap justify-between items-center gap-5 border-solid border-0 border-b border-gray-500">
<h3 class="m-0 font-medium shrink-0">Game menu</h3>
<div class="hidden sm:flex gap-1.5 flex-wrap">
<button @click.stop="userPanelScreen = 'inventory'" :class="{ active: userPanelScreen === 'inventory' }" class="btn-cyan py-1.5 px-4 min-w-24">Inventory</button>
<button @click.stop="userPanelScreen = 'equipment'" :class="{ active: userPanelScreen === 'equipment' }" class="btn-cyan py-1.5 px-4 min-w-24">Equipment</button>
<button @click.stop="userPanelScreen = 'characterScreen'" :class="{ active: userPanelScreen === 'characterScreen' }" class="btn-cyan py-1.5 px-4 min-w-24">Character</button>
<button @click.stop="userPanelScreen = 'settings'" :class="{ active: userPanelScreen === 'settings' }" class="btn-cyan py-1.5 px-4 min-w-24">Settings</button>
</div>
<div class="flex gap-2.5">
<button class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
<img alt="close" draggable="false" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" />
</button>
</div>
<div class="flex sm:hidden gap-1.5 flex-wrap">
<button @click.stop="userPanelScreen = 'inventory'" :class="{ active: userPanelScreen === 'inventory' }" class="btn-cyan py-1.5 px-4 min-w-24">Inventory</button>
<button @click.stop="userPanelScreen = 'equipment'" :class="{ active: userPanelScreen === 'equipment' }" class="btn-cyan py-1.5 px-4 min-w-24">Equipment</button>
<button @click.stop="userPanelScreen = 'characterScreen'" :class="{ active: userPanelScreen === 'characterScreen' }" class="btn-cyan py-1.5 px-4 min-w-24">Character</button>
<button @click.stop="userPanelScreen = 'settings'" :class="{ active: userPanelScreen === 'settings' }" class="btn-cyan py-1.5 px-4 min-w-24">Settings</button>
</div>
</div>
<Inventory v-show="userPanelScreen === 'inventory'" />
<Equipment v-show="userPanelScreen === 'equipment'" />
<CharacterScreen v-show="userPanelScreen === 'characterScreen'" />
<Settings v-show="userPanelScreen === 'settings'" />
</div>
</div>
</template>
<script setup lang="ts">
import CharacterScreen from '@/components/game/gui/partials/CharacterScreen.vue'
import Equipment from '@/components/game/gui/partials/Equipment.vue'
import Inventory from '@/components/game/gui/partials/Inventory.vue'
import Settings from '@/components/game/gui/partials/Settings.vue'
import { useGameStore } from '@/stores/gameStore'
import { ref } from 'vue'
const gameStore = useGameStore()
let userPanelScreen = ref('inventory')
</script>

View File

@ -1,68 +0,0 @@
<template>
<div class="grow flex flex-col w-full h-full relative overflow-auto">
<div class="m-4 relative">
<h4 class="font-medium text-lg max-w-[375px]">Character</h4>
<div class="flex justify-center flex-wrap gap-20">
<div class="flex gap-3 mt-2 flex-wrap max-w-[375px]">
<div class="h-full flex flex-col justify-center items-center">
<img class="h-72 my-2 mx-auto" src="/assets/placeholders/inventory_player.png" />
</div>
</div>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-3 mx-5 mt-2">
<h3>{{ gameStore.character?.name }}</h3>
<div class="flex gap-4 flex-wrap max-w-[375px]">
<ul class="p-0 m-0 list-none">
<li class="leading-6 text-lg">Class:</li>
<li class="leading-6 text-lg">Race:</li>
<li class="leading-6 text-lg">Hit Points:</li>
<li class="leading-6 text-lg">Mana Points:</li>
<li class="leading-6 text-lg">Level:</li>
<li class="leading-6 text-lg">Stat Points:</li>
</ul>
<ul class="p-0 m-0 list-none">
<li class="leading-6 text-lg min-h-6">Knight</li>
<li class="leading-6 text-lg min-h-6">{{ gameStore.character?.characterType?.race }}</li>
<li class="leading-6 text-lg min-h-6">{{ gameStore.character?.hitpoints }} <span class="text-green">(+15)</span></li>
<li class="leading-6 text-lg min-h-6">{{ gameStore.character?.mana }}</li>
<li class="leading-6 text-lg min-h-6">{{ gameStore.character?.level }}</li>
<li class="leading-6 text-lg min-h-6">3</li>
</ul>
</div>
</div>
<div class="flex flex-col gap-3 mx-5 mt-2">
<h3>Alignment</h3>
<div class="h-8 w-64 rounded border border-solid border-white bg-gradient-to-r from-red to-blue relative">
<!-- TODO: Give slider left value based on alignment (0-100), new characters start with 50 -->
<div class="rounded border-2 border-solid border-white h-10 w-2 absolute top-1/2 -translate-y-1/2 -translate-x-1/2" :style="{ left: gameStore.character?.alignment + '%' }"></div>
</div>
</div>
</div>
</div>
<div class="absolute -left-5 -bottom-5 w-[calc(100%_+_40px)] my-px h-px bg-gray-500"></div>
</div>
<div class="m-4">
<h4 class="font-medium text-lg max-w-[375px]">Character stats</h4>
<div class="flex gap-3 mt-4 flex-wrap max-w-[375px]">
<ul class="p-0 m-0 list-none">
<li class="leading-6">Health:</li>
<li class="leading-6">Defense:</li>
<li class="leading-6">More stats:</li>
<li class="leading-6">Extra stats:</li>
</ul>
<ul class="p-0 m-0 list-none text-right">
<li class="leading-6">100 <span class="text-green">(+15)</span></li>
<li class="leading-6">1000 <span class="text-green">(+500)</span></li>
<li class="leading-6"></li>
<li class="leading-6"></li>
</ul>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useGameStore } from '@/stores/gameStore'
const gameStore = useGameStore()
</script>

View File

@ -1,89 +0,0 @@
<template>
<div class="grow flex flex-col w-full h-full relative overflow-auto">
<div class="m-4 relative">
<h4 class="font-medium text-lg max-w-[375px]">Equipment</h4>
<div class="flex justify-center items-center flex-wrap gap-20">
<div class="flex gap-3 mt-2 flex-wrap max-w-[375px]">
<div class="h-full flex flex-col justify-center items-center">
<h3>{{ gameStore.character?.name }}</h3>
<img class="h-60 my-2 mx-auto" src="/assets/placeholders/inventory_player.png" />
<span class="block text-sm">Level {{ gameStore.character?.level }}</span>
</div>
</div>
<div class="flex flex-col gap-3 mx-5 mt-2">
<div class="flex gap-3 justify-center">
<!-- Helmet -->
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square h-11 w-11 relative hover:bg-gray-200">
<img src="/assets/icons/inventory/helmet.svg" class="center-element w-11/12 opacity-20" />
</div>
<!-- Head charm -->
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square h-11 w-11 relative hover:bg-gray-200">
<img src="/assets/icons/inventory/head_charm.svg" class="center-element w-11/12 opacity-20" />
</div>
</div>
<div class="flex gap-3 justify-center">
<!-- Bracers -->
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md w-11 h-[104px] relative hover:bg-gray-200">
<img src="/assets/icons/inventory/bracers.svg" class="center-element w-11/12 opacity-20" />
</div>
<!-- Chestplate -->
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square w-[104px] h-[104px] relative hover:bg-gray-200">
<img src="/assets/icons/inventory/chestplate.svg" class="center-element w-10/12 opacity-20" />
</div>
<!-- Primary Weapon -->
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md w-11 h-[104px] self-stretch justify-self-stretch relative hover:bg-gray-200">
<img src="/assets/icons/inventory/primary_weapon.svg" class="center-element w-11/12 opacity-20" />
</div>
</div>
<div class="flex gap-3 justify-center">
<!-- Legs -->
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md w-11 h-[104px] self-stretch justify-self-stretch relative hover:bg-gray-200">
<img src="/assets/icons/inventory/legs.svg" class="center-element w-11/12 opacity-20" />
</div>
<div class="flex flex-col gap-3">
<!-- Belt/pouch -->
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square h-11 w-11 self-stretch justify-self-stretch relative hover:bg-gray-200">
<img src="/assets/icons/inventory/pouch.svg" class="center-element w-11/12 opacity-20" />
</div>
<!-- Boots -->
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square h-11 w-11 self-stretch justify-self-stretch relative hover:bg-gray-200">
<img src="/assets/icons/inventory/boots.svg" class="center-element w-11/12 opacity-20" />
</div>
</div>
</div>
</div>
</div>
<div class="absolute -left-5 -bottom-5 w-[calc(100%_+_40px)] my-px h-px bg-gray-500"></div>
</div>
<div class="m-4">
<h4 class="font-medium text-lg max-w-[375px]">Equipment Bonus</h4>
<div class="flex gap-3 mt-4 flex-wrap max-w-[375px]">
<ul class="p-0 m-0 list-none">
<li class="leading-6">Health:</li>
<li class="leading-6">Defense:</li>
<li class="leading-6">More stats:</li>
<li class="leading-6">Extra stats:</li>
</ul>
<ul class="p-0 m-0 list-none text-right">
<li class="leading-6">+15</li>
<li class="leading-6">500</li>
<li class="leading-6"></li>
<li class="leading-6"></li>
</ul>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useGameStore } from '@/stores/gameStore'
const gameStore = useGameStore()
</script>

View File

@ -1,17 +0,0 @@
<template>
<div class="grow flex flex-col w-full h-full relative overflow-auto">
<div class="m-4 relative">
<h4 class="m-auto font-medium text-lg max-w-[375px]">Inventory</h4>
<div class="flex gap-3 mt-4 mx-auto flex-wrap max-w-[375px]">
<div v-for="n in 24" class="bg-gray-300/80 border-solid border-2 border-gray-500 w-12 h-12 rounded-md aspect-square shrink-0 justify-self-stretch hover:bg-gray-200"></div>
</div>
<div class="absolute -left-5 -bottom-5 w-[calc(100%_+_40px)] my-px h-px bg-gray-500"></div>
</div>
<div class="m-4">
<h4 class="m-auto font-medium text-lg max-w-[375px]">Chest items</h4>
<div class="flex gap-3 mt-4 mx-auto flex-wrap max-w-[375px]">
<div v-for="n in 12" class="bg-gray-300/80 border-solid border-2 border-gray-500 w-12 h-12 rounded-md aspect-square shrink-0 justify-self-stretch hover:bg-gray-200"></div>
</div>
</div>
</div>
</template>

View File

@ -1,40 +0,0 @@
<template>
<div class="flex h-full w-full relative">
<div class="w-2/12 flex flex-col relative">
<!-- Settings Categories -->
<div class="relative p-2.5">
<h3>Settings</h3>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</div>
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'character' }" @click.stop="settingCategory = 'character'">
<span>Character</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'account' }" @click.stop="settingCategory = 'account'">
<span>Account</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'audio' }" @click.stop="settingCategory = 'audio'">
<span>Audio</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'video' }" @click.stop="settingCategory = 'video'">
<span>Video</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
</div>
<div class="absolute w-px bg-gray-500 h-full top-0 left-1/6"></div>
<!-- Assets list -->
<div class="overflow-auto h-full w-10/12 flex flex-col relative">
<CharacterSettings v-show="settingCategory == 'character'" />
</div>
</div>
</template>
<script setup lang="ts">
import CharacterSettings from '@/components/game/gui/partials/settings/CharacterSettings.vue'
import { ref } from 'vue'
let settingCategory = ref('character')
</script>

View File

@ -1,34 +0,0 @@
<template>
<div class="h-full overflow-auto">
<div class="relative p-2.5 flex flex-col gap-5 h-72">
<h3>Character details</h3>
<button @click="toggle" class="btn-cyan px-4 py-1.5 w-24">Edit</button>
<form class="flex gap-2.5 flex-wrap">
<div class="form-field-half max-w-[300px]">
<label for="name">Name</label>
<input class="input-field" :class="{ inactive: !editCharacter }" type="text" name="name" placeholder="Ethereal" :disabled="!editCharacter" />
</div>
<div class="form-field-half max-w-[300px] relative">
<label for="class">Class</label>
<select class="input-field" v-model="characterClass" :class="{ inactive: !editCharacter }" name="class" :disabled="!editCharacter">
<option value="Knight" :selected="characterClass == 'Knight'" :disabled="characterClass == 'Knight'">Knight</option>
<option value="Paladin" :selected="characterClass == 'Paladin'" :disabled="characterClass == 'Paladin'">Paladin</option>
</select>
<span v-if="!editCharacter" class="absolute bottom-[9px] left-[14px] text-sm text-gray-300/50">{{ characterClass }}</span>
</div>
<button v-if="editCharacter" @click="toggle" class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const editCharacter = ref(false)
const characterClass = ref('')
const toggle = () => {
editCharacter.value = !editCharacter.value
}
</script>

View File

@ -1,220 +0,0 @@
<template>
<Scene name="effects" @preload="preloadScene" @create="createScene" @update="updateScene" />
</template>
<script setup lang="ts">
import type { Map, WeatherState } from '@/application/types'
import { MapStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore'
import { useMapStore } from '@/stores/mapStore'
import { Scene } from 'phavuer'
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
// Types
interface LightConfig {
SUNRISE_HOUR: number
SUNSET_HOUR: number
DAY_STRENGTH: number
NIGHT_STRENGTH: number
TRANSITION_HOURS: number
}
interface EffectObjects {
light: Phaser.GameObjects.Graphics | null
rain: Phaser.GameObjects.Particles.ParticleEmitter | null
fog: Phaser.GameObjects.Sprite | null
}
interface EffectValues {
light?: number
rain?: number
fog?: number
[key: string]: number | undefined
}
// Constants
const LIGHT_CONFIG: LightConfig = {
SUNRISE_HOUR: 6,
SUNSET_HOUR: 20,
DAY_STRENGTH: 100,
NIGHT_STRENGTH: 30,
TRANSITION_HOURS: 3
}
// Composables
const useEffects = () => {
const effects = ref<EffectObjects>({
light: null,
rain: null,
fog: null
})
const initializeEffects = (scene: Phaser.Scene) => {
effects.value.light = scene.add.graphics().setDepth(1000)
effects.value.rain = scene.add
.particles(0, 0, 'raindrop', {
x: { min: 0, max: window.innerWidth },
y: -50,
quantity: 5,
lifespan: 2000,
speedY: { min: 300, max: 500 },
scale: { start: 0.005, end: 0.005 },
alpha: { start: 0.5, end: 0 },
blendMode: 'ADD'
})
.setDepth(900)
effects.value.rain.stop()
effects.value.fog = scene.add
.sprite(window.innerWidth / 2, window.innerHeight / 2, 'fog')
.setScale(2)
.setAlpha(0)
.setDepth(950)
}
const applyEffects = (effectValues: EffectValues) => {
if (effects.value.light) {
const darkness = 1 - (effectValues.light ?? 0) / 100
effects.value.light.clear().fillStyle(0x000000, darkness).fillRect(0, 0, window.innerWidth, window.innerHeight)
}
if (effects.value.rain) {
if (effectValues.rain) {
effects.value.rain.start().setQuantity(effectValues.rain / 10)
} else {
effects.value.rain.stop()
}
}
if (effects.value.fog && effectValues.fog !== undefined) {
effects.value.fog.setAlpha(effectValues.fog / 100)
}
}
const handleResize = () => {
if (effects.value.rain) {
effects.value.rain.updateConfig({ x: { min: 0, max: window.innerWidth } })
}
if (effects.value.fog) {
effects.value.fog.setPosition(window.innerWidth / 2, window.innerHeight / 2)
}
}
return { effects, initializeEffects, applyEffects, handleResize }
}
// Store instances
const gameStore = useGameStore()
const mapStore = useMapStore()
const mapStorage = new MapStorage()
// State
const sceneRef = ref<Phaser.Scene | null>(null)
const mapObject = ref<Map | null>(null)
const weatherState = ref<WeatherState>({
rainPercentage: 0,
fogDensity: 0
})
// Effects management
const { effects, initializeEffects, applyEffects, handleResize } = useEffects()
// Utility functions
const lerp = (x: number, y: number, a: number): number => x * (1 - a) + y * a
const calculateLightStrength = (time: Date): number => {
const hour = time.getHours()
const minute = time.getMinutes()
if (hour >= LIGHT_CONFIG.SUNSET_HOUR - LIGHT_CONFIG.TRANSITION_HOURS && hour < LIGHT_CONFIG.SUNSET_HOUR) {
return lerp(LIGHT_CONFIG.DAY_STRENGTH, LIGHT_CONFIG.NIGHT_STRENGTH, (hour + minute / 60 - (LIGHT_CONFIG.SUNSET_HOUR - LIGHT_CONFIG.TRANSITION_HOURS)) / LIGHT_CONFIG.TRANSITION_HOURS)
} else if (hour >= LIGHT_CONFIG.SUNRISE_HOUR && hour < LIGHT_CONFIG.SUNRISE_HOUR + LIGHT_CONFIG.TRANSITION_HOURS) {
return lerp(LIGHT_CONFIG.NIGHT_STRENGTH, LIGHT_CONFIG.DAY_STRENGTH, (hour + minute / 60 - LIGHT_CONFIG.SUNRISE_HOUR) / LIGHT_CONFIG.TRANSITION_HOURS)
} else if (hour > LIGHT_CONFIG.SUNRISE_HOUR && hour < LIGHT_CONFIG.SUNSET_HOUR) {
return LIGHT_CONFIG.DAY_STRENGTH
}
return LIGHT_CONFIG.NIGHT_STRENGTH
}
// Scene handlers
const preloadScene = (scene: Phaser.Scene): void => {
scene.load.image('raindrop', 'assets/raindrop.png')
scene.load.image('fog', 'assets/fog.png')
}
const createScene = (scene: Phaser.Scene): void => {
sceneRef.value = scene
initializeEffects(scene)
setupSocketListeners()
}
const updateScene = (): void => {
const timeBasedLight = calculateLightStrength(gameStore.world.date)
const mapEffects =
mapObject.value?.mapEffects?.reduce<Record<string, number>>(
(acc, curr) => ({
...acc,
[curr.effect]: curr.strength
}),
{}
) ?? {}
const finalEffects: EffectValues = {
...mapEffects,
light: timeBasedLight,
rain: weatherState.value.rainPercentage,
fog: weatherState.value.fogDensity
}
applyEffects(finalEffects)
}
// Map management
const loadMap = async (): Promise<void> => {
if (!mapStore.mapId) return
mapObject.value = await mapStorage.get(mapStore.mapId)
}
// Socket handlers
const setupSocketListeners = (): void => {
gameStore.connection?.emit('weather', (response: WeatherState) => {
weatherState.value = response
updateScene()
})
gameStore.connection?.on('weather', (data: WeatherState) => {
weatherState.value = data
updateScene()
})
gameStore.connection?.on('date', updateScene)
}
// Watchers
watch(
() => mapStore.mapId,
async (newMapId) => {
if (newMapId) {
await loadMap()
updateScene()
}
}
)
watch(
() => mapObject.value,
() => {
updateScene()
}
)
// Lifecycle hooks
onMounted(() => window.addEventListener('resize', handleResize))
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
if (sceneRef.value) sceneRef.value.scene.remove('effects')
gameStore.connection?.off('weather')
})
</script>

View File

@ -32,8 +32,18 @@ gameStore.connection?.on('map:character:leave', (characterId: UUID) => {
mapStore.removeCharacter(characterId) mapStore.removeCharacter(characterId)
}) })
gameStore.connection?.on('map:character:attack', (characterId: UUID) => {
mapStore.updateCharacterProperty(characterId, 'isAttacking', true)
})
gameStore.connection?.on('map:character:move', (data: { characterId: UUID; positionX: number; positionY: number; rotation: number; isMoving: boolean }) => { gameStore.connection?.on('map:character:move', (data: { characterId: UUID; positionX: number; positionY: number; rotation: number; isMoving: boolean }) => {
mapStore.updateCharacterPosition(data) mapStore.updateCharacterPosition(data)
// @TODO: Replace with universal class, composable or store
if (data.characterId === gameStore.character?.id) {
gameStore.character!.positionX = data.positionX
gameStore.character!.positionY = data.positionY
gameStore.character!.rotation = data.rotation
}
}) })
onUnmounted(() => { onUnmounted(() => {

View File

@ -4,10 +4,10 @@
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import config from '@/application/config'
import type { UUID } from '@/application/types' import type { Map as MapT, UUID } from '@/application/types'
import { unduplicateArray } from '@/application/utilities' import { unduplicateArray } from '@/application/utilities'
import Controls from '@/components/utilities/Controls.vue' import Controls from '@/components/utilities/Controls.vue'
import { FlattenMapArray, loadMapTilesIntoScene, setLayerTiles } from '@/composables/mapComposable' import { loadMapTilesIntoScene, setLayerTiles } from '@/composables/mapComposable'
import { MapStorage } from '@/storage/storages' import { MapStorage } from '@/storage/storages'
import { useMapStore } from '@/stores/mapStore' import { useMapStore } from '@/stores/mapStore'
import { useScene } from 'phavuer' import { useScene } from 'phavuer'
@ -23,10 +23,10 @@ const mapStorage = new MapStorage()
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>() const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
const tileLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>() const tileLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
function createTileMap(mapData: any) { function createTileMap(map: MapT) {
const mapConfig = new Phaser.Tilemaps.MapData({ const mapConfig = new Phaser.Tilemaps.MapData({
width: mapData?.width, width: map.width,
height: mapData?.height, height: map.height,
tileWidth: config.tile_size.width, tileWidth: config.tile_size.width,
tileHeight: config.tile_size.height, tileHeight: config.tile_size.height,
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC, orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
@ -39,7 +39,7 @@ function createTileMap(mapData: any) {
} }
function createTileLayer(currentTileMap: Phaser.Tilemaps.Tilemap, mapData: any) { function createTileLayer(currentTileMap: Phaser.Tilemaps.Tilemap, mapData: any) {
const tilesArray = unduplicateArray(FlattenMapArray(mapData?.tiles ?? [])) const tilesArray = unduplicateArray(mapData?.tiles.flat())
const tilesetImages = tilesArray.map((tile: string, index: number) => { const tilesetImages = tilesArray.map((tile: string, index: number) => {
return currentTileMap.addTilesetImage(tile, tile, config.tile_size.width, config.tile_size.height, 1, 2, index + 1, { x: 0, y: -config.tile_size.height }) return currentTileMap.addTilesetImage(tile, tile, config.tile_size.width, config.tile_size.height, 1, 2, index + 1, { x: 0, y: -config.tile_size.height })
@ -58,9 +58,10 @@ function createTileLayer(currentTileMap: Phaser.Tilemaps.Tilemap, mapData: any)
loadMapTilesIntoScene(mapStore.mapId as UUID, scene) loadMapTilesIntoScene(mapStore.mapId as UUID, scene)
.then(() => mapStorage.get(mapStore.mapId)) .then(() => mapStorage.get(mapStore.mapId))
.then((mapData) => { .then((mapData) => {
if (!mapData || !mapData?.tiles) return
tileMap.value = createTileMap(mapData) tileMap.value = createTileMap(mapData)
tileLayer.value = createTileLayer(tileMap.value, mapData) tileLayer.value = createTileLayer(tileMap.value, mapData)
setLayerTiles(tileMap.value, tileLayer.value, mapData?.tiles) setLayerTiles(tileMap.value, tileLayer.value, mapData.tiles)
}) })
.catch((error) => console.error('Failed to initialize map:', error)) .catch((error) => console.error('Failed to initialize map:', error))

View File

@ -6,7 +6,7 @@
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Users</button> <button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Users</button>
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Chats</button> <button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Chats</button>
<button @mousedown.stop class="btn-cyan active py-1.5 px-4 min-w-24">Asset manager</button> <button @mousedown.stop class="btn-cyan active py-1.5 px-4 min-w-24">Asset manager</button>
<button class="btn-cyan py-1.5 px-4 min-w-24" type="button" @click="() => mapEditorStore.toggleActive()">Map editor</button> <button class="btn-cyan py-1.5 px-4 min-w-24" type="button" @click="$emit('open-map-editor')">Map editor</button>
</div> </div>
</template> </template>
<template #modalBody> <template #modalBody>
@ -20,12 +20,13 @@
<script setup lang="ts"> <script setup lang="ts">
import AssetManager from '@/components/gameMaster/assetManager/AssetManager.vue' import AssetManager from '@/components/gameMaster/assetManager/AssetManager.vue'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { ref } from 'vue' import { ref } from 'vue'
defineEmits(['open-map-editor'])
const gameStore = useGameStore() const gameStore = useGameStore()
const mapEditorStore = useMapEditorStore() const mapEditor = useMapEditorComposable()
let toggle = ref('asset-manager') let toggle = ref('asset-manager')
</script> </script>

View File

@ -21,13 +21,6 @@
<label for="tags">Tags</label> <label for="tags">Tags</label>
<ChipsInput v-model="mapObjectTags" @update:modelValue="mapObjectTags = $event" /> <ChipsInput v-model="mapObjectTags" @update:modelValue="mapObjectTags = $event" />
</div> </div>
<div class="form-field-full">
<label for="is-animated">Is animated</label>
<select v-model="mapObjectIsAnimated" class="input-field" name="is-animated">
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>
</div>
<div class="form-field-full"> <div class="form-field-full">
<label for="frame-speed">Frame rate</label> <label for="frame-speed">Frame rate</label>
<input v-model="mapObjectFrameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" /> <input v-model="mapObjectFrameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
@ -66,7 +59,6 @@ const mapObjectName = ref('')
const mapObjectTags = ref<string[]>([]) const mapObjectTags = ref<string[]>([])
const mapObjectOriginX = ref(0) const mapObjectOriginX = ref(0)
const mapObjectOriginY = ref(0) const mapObjectOriginY = ref(0)
const mapObjectIsAnimated = ref(false)
const mapObjectFrameRate = ref(0) const mapObjectFrameRate = ref(0)
const mapObjectFrameWidth = ref(0) const mapObjectFrameWidth = ref(0)
const mapObjectFrameHeight = ref(0) const mapObjectFrameHeight = ref(0)
@ -80,7 +72,6 @@ if (selectedMapObject.value) {
mapObjectTags.value = selectedMapObject.value.tags mapObjectTags.value = selectedMapObject.value.tags
mapObjectOriginX.value = selectedMapObject.value.originX mapObjectOriginX.value = selectedMapObject.value.originX
mapObjectOriginY.value = selectedMapObject.value.originY mapObjectOriginY.value = selectedMapObject.value.originY
mapObjectIsAnimated.value = selectedMapObject.value.isAnimated
mapObjectFrameRate.value = selectedMapObject.value.frameRate mapObjectFrameRate.value = selectedMapObject.value.frameRate
mapObjectFrameWidth.value = selectedMapObject.value.frameWidth mapObjectFrameWidth.value = selectedMapObject.value.frameWidth
mapObjectFrameHeight.value = selectedMapObject.value.frameHeight mapObjectFrameHeight.value = selectedMapObject.value.frameHeight
@ -120,7 +111,6 @@ function saveObject() {
tags: mapObjectTags.value, tags: mapObjectTags.value,
originX: mapObjectOriginX.value, originX: mapObjectOriginX.value,
originY: mapObjectOriginY.value, originY: mapObjectOriginY.value,
isAnimated: mapObjectIsAnimated.value,
frameRate: mapObjectFrameRate.value, frameRate: mapObjectFrameRate.value,
frameWidth: mapObjectFrameWidth.value, frameWidth: mapObjectFrameWidth.value,
frameHeight: mapObjectFrameHeight.value frameHeight: mapObjectFrameHeight.value
@ -141,7 +131,6 @@ watch(selectedMapObject, (mapObject: MapObject | null) => {
mapObjectTags.value = mapObject.tags mapObjectTags.value = mapObject.tags
mapObjectOriginX.value = mapObject.originX mapObjectOriginX.value = mapObject.originX
mapObjectOriginY.value = mapObject.originY mapObjectOriginY.value = mapObject.originY
mapObjectIsAnimated.value = mapObject.isAnimated
mapObjectFrameRate.value = mapObject.frameRate mapObjectFrameRate.value = mapObject.frameRate
mapObjectFrameWidth.value = mapObject.frameWidth mapObjectFrameWidth.value = mapObject.frameWidth
mapObjectFrameHeight.value = mapObject.frameHeight mapObjectFrameHeight.value = mapObject.frameHeight

View File

@ -21,9 +21,12 @@
<button class="btn-cyan py-2 my-4" type="button" @click.prevent="addNewImage">New action</button> <button class="btn-cyan py-2 my-4" type="button" @click.prevent="addNewImage">New action</button>
<Accordion v-for="action in spriteActions" :key="action.id"> <Accordion v-for="action in spriteActions" :key="action.id">
<template #header> <template #header>
<div class="flex justify-between items-center"> <div class="flex items-center">
{{ action.action }} {{ action.action }}
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="() => spriteActions.splice(spriteActions.indexOf(action), 1)">Delete</button> <div class="ml-auto space-x-2">
<button class="btn-cyan px-4 py-1.5 min-w-24" type="button" @click.stop.prevent="openPreviewModal(action)">View</button>
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.stop.prevent="() => spriteActions.splice(spriteActions.indexOf(action), 1)">Delete</button>
</div>
</div> </div>
</template> </template>
<template #content> <template #content>
@ -40,38 +43,35 @@
<label for="origin-y">Origin Y</label> <label for="origin-y">Origin Y</label>
<input v-model.number="action.originY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" /> <input v-model.number="action.originY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
</div> </div>
<div class="form-field-half"> <div class="form-field-full">
<label for="is-animated">Is animated</label>
<select v-model="action.isAnimated" class="input-field" name="is-animated">
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>
</div>
<div class="form-field-half" v-if="action.isAnimated">
<label for="is-looping">Is looping</label>
<select v-model="action.isLooping" class="input-field" name="is-looping">
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>
</div>
<div class="form-field-full" v-if="action.isAnimated">
<label for="frame-speed">Frame rate</label> <label for="frame-speed">Frame rate</label>
<input v-model.number="action.frameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" /> <input v-model.number="action.frameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
</div> </div>
<div class="form-field-full"> <div class="form-field-full">
<SpriteActionsInput v-model="action.sprites" /> <SpriteActionsInput v-model="action.sprites" @tempOffsetChange="(index, offset) => handleTempOffsetChange(action, index, offset)" />
</div> </div>
</form> </form>
</template> </template>
</Accordion> </Accordion>
<SpritePreview
v-if="selectedAction"
:sprites="selectedAction.sprites"
:frame-rate="selectedAction.frameRate"
:is-modal-open="isModalOpen"
:temp-offset-index="tempOffsetData.index"
:temp-offset="tempOffsetData.offset"
@update:frame-rate="updateFrameRate"
@update:is-modal-open="isModalOpen = $event"
/>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { Sprite, SpriteAction } from '@/application/types' import type { Sprite, SpriteAction, UUID } from '@/application/types'
import { uuidv4 } from '@/application/utilities' import { uuidv4 } from '@/application/utilities'
import SpriteActionsInput from '@/components/gameMaster/assetManager/partials/sprite/partials/SpriteImagesInput.vue' import SpriteActionsInput from '@/components/gameMaster/assetManager/partials/sprite/partials/SpriteImagesInput.vue'
import SpritePreview from '@/components/gameMaster/assetManager/partials/sprite/partials/SpritePreview.vue'
import Accordion from '@/components/utilities/Accordion.vue' import Accordion from '@/components/utilities/Accordion.vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
@ -84,6 +84,8 @@ const selectedSprite = computed(() => assetManagerStore.selectedSprite)
const spriteName = ref('') const spriteName = ref('')
const spriteActions = ref<SpriteAction[]>([]) const spriteActions = ref<SpriteAction[]>([])
const isModalOpen = ref(false)
const selectedAction = ref<SpriteAction | null>(null)
if (!selectedSprite.value) { if (!selectedSprite.value) {
console.error('No sprite selected') console.error('No sprite selected')
@ -140,8 +142,6 @@ function saveSprite() {
sprites: action.sprites, sprites: action.sprites,
originX: action.originX, originX: action.originX,
originY: action.originY, originY: action.originY,
isAnimated: action.isAnimated,
isLooping: action.isLooping,
frameRate: action.frameRate, frameRate: action.frameRate,
frameWidth: action.frameWidth, frameWidth: action.frameWidth,
frameHeight: action.frameHeight frameHeight: action.frameHeight
@ -163,14 +163,11 @@ function addNewImage() {
const newImage: SpriteAction = { const newImage: SpriteAction = {
id: uuidv4(), id: uuidv4(),
spriteId: selectedSprite.value.id, sprite: selectedSprite.value.id,
sprite: selectedSprite.value,
action: 'new_action', action: 'new_action',
sprites: [], sprites: [],
originX: 0, originX: 0,
originY: 0, originY: 0,
isAnimated: false,
isLooping: false,
frameRate: 0, frameRate: 0,
frameWidth: 0, frameWidth: 0,
frameHeight: 0 frameHeight: 0
@ -188,12 +185,40 @@ function sortSpriteActions(actions: SpriteAction[]): SpriteAction[] {
return [...actions].sort((a, b) => a.action.localeCompare(b.action)) return [...actions].sort((a, b) => a.action.localeCompare(b.action))
} }
function openPreviewModal(action: SpriteAction) {
selectedAction.value = action
isModalOpen.value = true
}
function updateFrameRate(value: number) {
if (selectedAction.value) {
selectedAction.value.frameRate = value
}
}
const tempOffsetData = ref<{ index: number | undefined; offset: { x: number; y: number } | undefined }>({
index: undefined,
offset: undefined
})
function handleTempOffsetChange(action: SpriteAction, index: number, offset: { x: number; y: number }) {
if (selectedAction.value === action) {
tempOffsetData.value = { index, offset }
}
}
watch(selectedSprite, (sprite: Sprite | null) => { watch(selectedSprite, (sprite: Sprite | null) => {
if (!sprite) return if (!sprite) return
spriteName.value = sprite.name spriteName.value = sprite.name
spriteActions.value = sortSpriteActions(sprite.spriteActions) spriteActions.value = sortSpriteActions(sprite.spriteActions)
}) })
watch(isModalOpen, (newValue) => {
if (!newValue) {
selectedAction.value = null
}
})
onMounted(() => { onMounted(() => {
if (!selectedSprite.value) return if (!selectedSprite.value) return
}) })

View File

@ -1,19 +1,44 @@
<template> <template>
<div class="flex flex-wrap gap-3"> <div class="flex flex-wrap gap-3">
<div v-for="(image, index) in modelValue" :key="index" class="h-20 w-20 p-4 bg-gray-300 bg-opacity-50 rounded text-center relative group cursor-move" draggable="true" @dragstart="dragStart($event, index)" @dragover.prevent @dragenter.prevent @drop="drop($event, index)"> <div v-for="(image, index) in modelValue" :key="index" class="h-20 w-20 p-4 bg-gray-300 bg-opacity-50 rounded text-center relative group cursor-move" draggable="true" @dragstart="dragStart($event, index)" @dragover.prevent @dragenter.prevent @drop="drop($event, index)">
<img :src="image" class="max-w-full max-h-full object-contain pointer-events-none" alt="Uploaded image" /> <img :src="image.url" class="max-w-full max-h-full object-contain pointer-events-none" alt="Uploaded image" @load="updateImageDimensions($event, index)" />
<div v-if="imageDimensions[index]" class="absolute bottom-1 right-1 bg-black/50 text-white text-xs px-1 py-0.5 rounded transition-opacity font-default">{{ image.dimensions.width }}x{{ image.dimensions.height }}</div>
<div class="absolute top-1 left-1 flex-row space-y-1"> <div class="absolute top-1 left-1 flex-row space-y-1">
<button @click.stop="deleteImage(index)" class="bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" aria-label="Delete image"> <button @click.stop="deleteImage(index)" class="bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" aria-label="Delete image">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
</button> </button>
<button class="bg-blue-500 text-white rounded-full w-6 h-6 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" aria-label="Scope image"> <button @click.stop="openOffsetModal(index)" class="bg-blue-500 text-white rounded-full w-6 h-6 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" aria-label="Scope image">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg width="50px" height="50px" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> <path d="M8.29289 3.70711L1 11V15H5L12.2929 7.70711L8.29289 3.70711Z" fill="white" />
<path d="M9.70711 2.29289L13.7071 6.29289L15.1716 4.82843C15.702 4.29799 16 3.57857 16 2.82843C16 1.26633 14.7337 0 13.1716 0C12.4214 0 11.702 0.297995 11.1716 0.828428L9.70711 2.29289Z" fill="white" />
</svg> </svg>
</button> </button>
</div> </div>
<Modal :is-modal-open="selectedImageIndex === index" :modal-width="300" :modal-height="210" :is-resizable="false" :bg-style="'none'" @modal:close="closeOffsetModal">
<template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">Action offset ({{ selectedImageIndex }})</h3>
</template>
<template #modalBody>
<div class="m-4">
<form method="post" @submit.prevent="saveOffset(index)" class="inline">
<div class="gap-2.5 flex flex-wrap">
<div class="form-field-half">
<label for="offsetX">Offset X</label>
<input class="input-field max-w-64" v-model="tempOffset.x" name="offsetX" id="offsetX" type="number" />
</div>
<div class="form-field-half">
<label for="offsetY">Offset Y</label>
<input class="input-field max-w-64" v-model="tempOffset.y" name="offsetY" id="offsetY" type="number" />
</div>
</div>
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
</form>
</div>
</template>
</Modal>
</div> </div>
<div class="h-20 w-20 p-4 bg-gray-200 bg-opacity-50 rounded justify-center items-center flex hover:cursor-pointer" @click="triggerFileInput" @drop.prevent="onDrop" @dragover.prevent> <div class="h-20 w-20 p-4 bg-gray-200 bg-opacity-50 rounded justify-center items-center flex hover:cursor-pointer" @click="triggerFileInput" @drop.prevent="onDrop" @dragover.prevent>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 invert" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 invert" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@ -25,10 +50,19 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import Modal from '@/components/utilities/Modal.vue'
import { ref, watch } from 'vue'
interface SpriteImage {
url: string
offset: {
x: number
y: number
}
}
interface Props { interface Props {
modelValue: string[] modelValue: SpriteImage[]
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@ -36,11 +70,15 @@ const props = withDefaults(defineProps<Props>(), {
}) })
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:modelValue', value: string[]): void (e: 'update:modelValue', value: SpriteImage[]): void
(e: 'close'): void
(e: 'tempOffsetChange', index: number, offset: { x: number; y: number }): void
}>() }>()
const fileInput = ref<HTMLInputElement | null>(null) const fileInput = ref<HTMLInputElement | null>(null)
const draggedIndex = ref<number | null>(null) const draggedIndex = ref<number | null>(null)
const selectedImageIndex = ref<number | null>(null)
const tempOffset = ref({ x: 0, y: 0 })
const triggerFileInput = () => { const triggerFileInput = () => {
fileInput.value?.click() fileInput.value?.click()
@ -61,19 +99,25 @@ const onDrop = (event: DragEvent) => {
const handleFiles = (files: FileList) => { const handleFiles = (files: FileList) => {
Array.from(files).forEach((file) => { Array.from(files).forEach((file) => {
if (file.type.startsWith('image/')) { if (!file.type.startsWith('image/')) {
const reader = new FileReader() return
reader.onload = (e) => {
if (typeof e.target?.result === 'string') {
updateImages([...props.modelValue, e.target.result])
}
}
reader.readAsDataURL(file)
} }
const reader = new FileReader()
reader.onload = (e) => {
if (typeof e.target?.result === 'string') {
const newImage: SpriteImage = {
url: e.target.result,
offset: { x: 0, y: 0 }
}
updateImages([...props.modelValue, newImage])
}
}
reader.readAsDataURL(file)
}) })
} }
const updateImages = (newImages: string[]) => { const updateImages = (newImages: SpriteImage[]) => {
emit('update:modelValue', newImages) emit('update:modelValue', newImages)
} }
@ -101,4 +145,41 @@ const drop = (event: DragEvent, dropIndex: number) => {
} }
draggedIndex.value = null draggedIndex.value = null
} }
const openOffsetModal = (index: number) => {
selectedImageIndex.value = index
tempOffset.value = { ...props.modelValue[index].offset }
}
const closeOffsetModal = () => {
selectedImageIndex.value = null
}
const saveOffset = (index: number) => {
const newImages = [...props.modelValue]
newImages[index] = {
...newImages[index],
offset: { ...tempOffset.value }
}
updateImages(newImages)
closeOffsetModal()
}
const onOffsetChange = () => {
if (selectedImageIndex.value !== null) {
emit('tempOffsetChange', selectedImageIndex.value, tempOffset.value)
}
}
watch(tempOffset, onOffsetChange, { deep: true })
const imageDimensions = ref<{ [key: number]: { width: number; height: number } }>({})
const updateImageDimensions = (event: Event, index: number) => {
const img = event.target as HTMLImageElement
imageDimensions.value[index] = {
width: img.naturalWidth,
height: img.naturalHeight
}
}
</script> </script>

View File

@ -0,0 +1,146 @@
<template>
<Modal :is-modal-open="isModalOpen" :modal-width="700" :modal-height="330" :bg-style="'none'" @modal:close="closeModal">
<template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">View sprite</h3>
</template>
<template #modalBody>
<div class="m-4 flex gap-8">
<div class="relative">
<div
class="sprite-container bg-gray-800"
:style="{
width: `${maxWidth}px`,
height: `${maxHeight}px`,
position: 'relative',
overflow: 'hidden'
}"
>
<img
v-for="(sprite, index) in spritesWithTempOffset"
:key="index"
:src="sprite.url"
alt="Sprite"
:style="{
position: 'absolute',
left: `${sprite.offset?.x || 0}px`,
bottom: `${sprite.offset?.y || 0}px`,
display: currentFrame === index ? 'block' : 'none',
transform: `scale(${zoomLevel / 100})`,
transformOrigin: 'bottom left'
}"
@load="updateContainerSize"
/>
</div>
</div>
<div class="flex flex-col justify-center gap-8 flex-1">
<div class="flex flex-col">
<label class="block mb-2 text-white">Frame Rate: {{ frameRate }} FPS</label>
<input type="range" v-model.number="localFrameRate" min="0" max="60" step="1" class="w-full accent-cyan-500" @input="updateFrameRate" />
</div>
<div class="flex flex-col">
<label class="block mb-2 text-white">Frame: {{ currentFrame + 1 }} of {{ sprites.length }}</label>
<input type="range" v-model.number="currentFrame" :min="0" :max="sprites.length - 1" step="1" class="w-full accent-cyan-500" @input="stopAnimation" />
</div>
<div class="flex flex-col">
<label class="block mb-2 text-white">Zoom: {{ zoomLevel }}%</label>
<input type="range" v-model.number="zoomLevel" min="10" max="200" step="10" class="w-full accent-cyan-500" />
</div>
</div>
</div>
</template>
</Modal>
</template>
<script setup lang="ts">
import type { SpriteImage } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
const props = defineProps<{
sprites: SpriteImage[]
frameRate: number
isModalOpen?: boolean
tempOffsetIndex?: number
tempOffset?: { x: number; y: number }
}>()
const emit = defineEmits<{
(e: 'update:frameRate', value: number): void
(e: 'update:isModalOpen', value: boolean): void
}>()
const currentFrame = ref(0)
const maxWidth = ref(250)
const maxHeight = ref(250)
const localFrameRate = ref(props.frameRate)
const zoomLevel = ref(100)
let animationInterval: number | null = null
const spritesWithTempOffset = computed(() => {
return props.sprites.map((sprite, index) => {
if (index === props.tempOffsetIndex && props.tempOffset) {
return { ...sprite, offset: props.tempOffset }
}
return sprite
})
})
function updateContainerSize(event: Event) {
const img = event.target as HTMLImageElement
maxWidth.value = Math.max(maxWidth.value, img.naturalWidth)
maxHeight.value = Math.max(maxHeight.value, img.naturalHeight)
}
function updateAnimation() {
stopAnimation()
if (props.frameRate <= 0 || props.sprites.length === 0) {
currentFrame.value = 0
return
}
animationInterval = window.setInterval(() => {
currentFrame.value = (currentFrame.value + 1) % props.sprites.length
}, 1000 / props.frameRate)
}
function stopAnimation() {
if (animationInterval) {
clearInterval(animationInterval)
animationInterval = null
}
}
function updateFrameRate() {
emit('update:frameRate', localFrameRate.value)
}
function closeModal() {
emit('update:isModalOpen', false)
}
watch(
() => props.frameRate,
(newValue) => {
localFrameRate.value = newValue
updateAnimation()
},
{ immediate: true }
)
watch(() => props.sprites, updateAnimation, { immediate: true })
onMounted(() => {
updateAnimation()
})
onUnmounted(() => {
stopAnimation()
})
</script>
<style scoped>
.sprite-container {
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
</style>

View File

@ -29,7 +29,6 @@ import ChipsInput from '@/components/forms/ChipsInput.vue'
import { TileStorage } from '@/storage/storages' import { TileStorage } from '@/storage/storages'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { computed, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue' import { computed, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()

View File

@ -1,20 +1,46 @@
<template> <template>
<MapTiles @tileMap:create="tileMap = $event" /> <MapTiles ref="mapTiles" @tileMap:create="tileMap = $event" />
<PlacedMapObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" /> <PlacedMapObjects ref="mapObjects" v-if="tileMap" :tileMap="tileMap as Phaser.Tilemaps.Tilemap" />
<MapEventTiles v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" /> <MapEventTiles ref="eventTiles" v-if="tileMap" :tileMap="tileMap as Phaser.Tilemaps.Tilemap" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import MapEventTiles from '@/components/gameMaster/mapEditor/mapPartials/MapEventTiles.vue' import MapEventTiles from '@/components/gameMaster/mapEditor/mapPartials/MapEventTiles.vue'
import MapTiles from '@/components/gameMaster/mapEditor/mapPartials/MapTiles.vue' import MapTiles from '@/components/gameMaster/mapEditor/mapPartials/MapTiles.vue'
import PlacedMapObjects from '@/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.vue' import PlacedMapObjects from '@/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.vue'
import { useMapEditorStore } from '@/stores/mapEditorStore' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { onMounted, onUnmounted, shallowRef } from 'vue' import { useScene } from 'phavuer'
import { onMounted, onUnmounted, shallowRef, useTemplateRef } from 'vue'
const mapEditorStore = useMapEditorStore()
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>() const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
const mapEditor = useMapEditorComposable()
onUnmounted(() => { const scene = useScene()
mapEditorStore.reset()
}) const mapTiles = useTemplateRef('mapTiles')
const mapObjects = useTemplateRef('mapObjects')
const eventTiles = useTemplateRef('eventTiles')
function handlePointer(pointer: Phaser.Input.Pointer) {
if (!mapTiles.value || !mapObjects.value || !eventTiles.value) return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is pressed or if we're in move mode, this means we are moving the camera
if (pointer.event.shiftKey || mapEditor.tool.value === 'move') return
// Check if draw mode is tile
switch (mapEditor.drawMode.value) {
case 'tile':
mapTiles.value.handlePointer(pointer)
break
case 'object':
mapObjects.value.handlePointer(pointer)
break
case 'event':
eventTiles.value.handlePointer(pointer)
break
}
}
</script> </script>

View File

@ -1,88 +1,86 @@
<template> <template>
<Image v-for="tile in mapEditorStore.map?.mapEventTiles" v-bind="getImageProps(tile)" /> <Image v-for="tile in mapEditor.currentMap.value?.mapEventTiles" v-bind="getImageProps(tile)" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { MapEventTileType, type MapEventTile } from '@/application/types' import { MapEventTileType, type MapEventTile, type Map as MapT } from '@/application/types'
import { uuidv4 } from '@/application/utilities' import { uuidv4 } from '@/application/utilities'
import { getTile, tileToWorldX, tileToWorldY } from '@/composables/mapComposable' import { getTile, tileToWorldX, tileToWorldY } from '@/composables/mapComposable'
import { useMapEditorStore } from '@/stores/mapEditorStore' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { Image, useScene } from 'phavuer' import { Image, useScene } from 'phavuer'
import { onMounted, onUnmounted } from 'vue' import { shallowRef } from 'vue'
const scene = useScene() const mapEditor = useMapEditorComposable()
const mapEditorStore = useMapEditorStore()
defineExpose({ handlePointer })
const props = defineProps<{ const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap tileMap: Phaser.Tilemaps.Tilemap
}>() }>()
const tileLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
function getImageProps(tile: MapEventTile) { function getImageProps(tile: MapEventTile) {
return { return {
x: tileToWorldX(props.tilemap, tile.positionX, tile.positionY), x: tileToWorldX(props.tileMap, tile.positionX, tile.positionY),
y: tileToWorldY(props.tilemap, tile.positionX, tile.positionY), y: tileToWorldY(props.tileMap, tile.positionX, tile.positionY),
texture: tile.type, texture: tile.type,
depth: 999 depth: 999
} }
} }
function pencil(pointer: Phaser.Input.Pointer) { function pencil(pointer: Phaser.Input.Pointer, map: MapT) {
// Check if map is set if (!tileLayer.value) return
if (!mapEditorStore.map) return
// Check if tool is pencil
if (mapEditorStore.tool !== 'pencil') return
// Check if draw mode is blocking tile or teleport
if (mapEditorStore.drawMode !== 'blocking tile' && mapEditorStore.drawMode !== 'teleport') return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if there is a tile // Check if there is a tile
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY) const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
if (!tile) return if (!tile) return
// Check if event tile already exists on position // Check if event tile already exists on position
const existingEventTile = mapEditorStore.map.mapEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y) const existingEventTile = map.mapEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y)
if (existingEventTile) return if (existingEventTile) return
// If teleport, check if there is a selected map // If teleport, check if there is a selected map
if (mapEditorStore.drawMode === 'teleport' && !mapEditorStore.teleportSettings.toMap) return if (mapEditor.drawMode.value === 'teleport' && !mapEditor.teleportSettings.value.toMapId) return
const newEventTile = { const newEventTile = {
id: uuidv4(), id: uuidv4(),
mapId: mapEditorStore.map.id, mapId: map?.id,
map: mapEditorStore.map, map: map?.id,
type: mapEditorStore.drawMode === 'blocking tile' ? MapEventTileType.BLOCK : MapEventTileType.TELEPORT, type: mapEditor.drawMode.value === 'blocking tile' ? MapEventTileType.BLOCK : MapEventTileType.TELEPORT,
positionX: tile.x, positionX: tile.x,
positionY: tile.y, positionY: tile.y,
teleport: teleport:
mapEditorStore.drawMode === 'teleport' mapEditor.drawMode.value === 'teleport'
? { ? {
toMap: mapEditorStore.teleportSettings.toMap, toMap: mapEditor.teleportSettings.value.toMapId,
toPositionX: mapEditorStore.teleportSettings.toPositionX, toPositionX: mapEditor.teleportSettings.value.toPositionX,
toPositionY: mapEditorStore.teleportSettings.toPositionY, toPositionY: mapEditor.teleportSettings.value.toPositionY,
toRotation: mapEditorStore.teleportSettings.toRotation toRotation: mapEditor.teleportSettings.value.toRotation
} }
: undefined : undefined
} }
mapEditorStore.map.mapEventTiles = mapEditorStore.map.mapEventTiles.concat(newEventTile as MapEventTile) map!.mapEventTiles = map!.mapEventTiles.concat(newEventTile as MapEventTile)
} }
function eraser(pointer: Phaser.Input.Pointer) { function erase(pointer: Phaser.Input.Pointer, map: MapT) {
// Check if map is set if (!tileLayer.value) return
if (!mapEditorStore.map) return // Check if there is a tile
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if tool is pencil // Check if event tile already exists on position
if (mapEditorStore.tool !== 'eraser') return const existingEventTile = map.mapEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y)
if (!existingEventTile) return
// Check if draw mode is blocking tile or teleport // Remove existing event tile
if (mapEditorStore.eraserMode !== 'blocking tile' && mapEditorStore.eraserMode !== 'teleport') return map.mapEventTiles = map.mapEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id)
}
function handlePointer(pointer: Phaser.Input.Pointer) {
const map = mapEditor.currentMap.value
if (!map) return
// Check if left mouse button is pressed // Check if left mouse button is pressed
if (!pointer.isDown) return if (!pointer.isDown) return
@ -90,29 +88,13 @@ function eraser(pointer: Phaser.Input.Pointer) {
// Check if shift is not pressed, this means we are moving the camera // Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return if (pointer.event.shiftKey) return
// Check if there is a tile switch (mapEditor.tool.value) {
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY) case 'pencil':
if (!tile) return pencil(pointer, map)
break
// Check if event tile already exists on position case 'eraser':
const existingEventTile = mapEditorStore.map.mapEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y) erase(pointer, map)
if (!existingEventTile) return break
}
// Remove existing event tile
mapEditorStore.map.mapEventTiles = mapEditorStore.map.mapEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id)
} }
onMounted(() => {
scene.input.on(Phaser.Input.Events.POINTER_DOWN, pencil)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, eraser)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
})
onUnmounted(() => {
scene.input.off(Phaser.Input.Events.POINTER_DOWN, pencil)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, eraser)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
})
</script> </script>

View File

@ -8,7 +8,6 @@ import Controls from '@/components/utilities/Controls.vue'
import { createTileArray, getTile, placeTile, setLayerTiles } from '@/composables/mapComposable' import { createTileArray, getTile, placeTile, setLayerTiles } from '@/composables/mapComposable'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { TileStorage } from '@/storage/storages' import { TileStorage } from '@/storage/storages'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { useScene } from 'phavuer' import { useScene } from 'phavuer'
import { onMounted, onUnmounted, shallowRef, watch } from 'vue' import { onMounted, onUnmounted, shallowRef, watch } from 'vue'
@ -18,12 +17,13 @@ const emit = defineEmits(['tileMap:create'])
const scene = useScene() const scene = useScene()
const mapEditor = useMapEditorComposable() const mapEditor = useMapEditorComposable()
const mapEditorStore = useMapEditorStore()
const tileStorage = new TileStorage() const tileStorage = new TileStorage()
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>() const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
const tileLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>() const tileLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
defineExpose({ handlePointer })
function createTileMap() { function createTileMap() {
const mapData = new Phaser.Tilemaps.MapData({ const mapData = new Phaser.Tilemaps.MapData({
width: mapEditor.currentMap.value?.width, width: mapEditor.currentMap.value?.width,
@ -58,58 +58,31 @@ async function createTileLayer(currentTileMap: Phaser.Tilemaps.Tilemap) {
} }
function pencil(pointer: Phaser.Input.Pointer) { function pencil(pointer: Phaser.Input.Pointer) {
if (!tileMap.value || !tileLayer.value) return let map = mapEditor.currentMap.value
if (!map) return
// Check if map is set
if (!mapEditor.currentMap.value) return
// Check if tool is pencil
if (mapEditorStore.tool !== 'pencil') return
// Check if draw mode is tile
if (mapEditorStore.drawMode !== 'tile') return
// Check if there is a selected tile // Check if there is a selected tile
if (!mapEditorStore.selectedTile) return if (!mapEditor.selectedTile.value) return
// Check if left mouse button is pressed if (!tileMap.value || !tileLayer.value) return
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if there is a tile // Check if there is a tile
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY) const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
if (!tile) return if (!tile) return
// Place tile // Place tile
placeTile(tileMap.value, tileLayer.value, tile.x, tile.y, mapEditorStore.selectedTile) placeTile(tileMap.value, tileLayer.value, tile.x, tile.y, mapEditor.selectedTile.value)
// Adjust mapEditorStore.map.tiles // Adjust mapEditorStore.map.tiles
mapEditor.currentMap.value.tiles[tile.y][tile.x] = mapEditor.currentMap.value.tiles[tile.y][tile.x] map.tiles[tile.y][tile.x] = mapEditor.selectedTile.value
} }
function eraser(pointer: Phaser.Input.Pointer) { function eraser(pointer: Phaser.Input.Pointer) {
let map = mapEditor.currentMap.value
if (!map) return
if (!tileMap.value || !tileLayer.value) return if (!tileMap.value || !tileLayer.value) return
// Check if map is set
if (!mapEditor.currentMap.value) return
// Check if tool is pencil
if (mapEditorStore.tool !== 'eraser') return
// Check if draw mode is tile
if (mapEditorStore.eraserMode !== 'tile') return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if alt is pressed
if (pointer.event.altKey) return
// Check if there is a tile // Check if there is a tile
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY) const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
if (!tile) return if (!tile) return
@ -118,82 +91,79 @@ function eraser(pointer: Phaser.Input.Pointer) {
placeTile(tileMap.value, tileLayer.value, tile.x, tile.y, 'blank_tile') placeTile(tileMap.value, tileLayer.value, tile.x, tile.y, 'blank_tile')
// Adjust mapEditorStore.map.tiles // Adjust mapEditorStore.map.tiles
mapEditor.currentMap.value.tiles[tile.y][tile.x] = 'blank_tile' map.tiles[tile.y][tile.x] = 'blank_tile'
} }
function paint(pointer: Phaser.Input.Pointer) { function paint(pointer: Phaser.Input.Pointer) {
if (!tileMap.value || !tileLayer.value) return if (!tileMap.value || !tileLayer.value) return
// Check if map is set
if (!mapEditor.currentMap.value) return
// Check if tool is pencil
if (mapEditorStore.tool !== 'paint') return
// Check if there is a selected tile
if (!mapEditorStore.selectedTile) return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if alt is pressed
if (pointer.event.altKey) return
// Set new tileArray with selected tile // Set new tileArray with selected tile
setLayerTiles(tileMap.value, tileLayer.value, createTileArray(tileMap.value.width, tileMap.value.height, mapEditorStore.selectedTile)) const tileArray = createTileArray(tileMap.value.width, tileMap.value.height, mapEditor.selectedTile.value)
setLayerTiles(tileMap.value, tileLayer.value, tileArray)
// Adjust mapEditorStore.map.tiles // Adjust mapEditorStore.map.tiles
mapEditor.currentMap.value.tiles = createTileArray(tileMap.value.width, tileMap.value.height, mapEditor.currentMap.value.tiles) if (mapEditor.currentMap.value) {
mapEditor.currentMap.value.tiles = tileArray
}
} }
// When alt is pressed, and the pointer is down, select the tile that the pointer is over // When alt is pressed, and the pointer is down, select the tile that the pointer is over
function tilePicker(pointer: Phaser.Input.Pointer) { function tilePicker(pointer: Phaser.Input.Pointer) {
let map = mapEditor.currentMap.value
if (!map) return
if (!tileMap.value || !tileLayer.value) return if (!tileMap.value || !tileLayer.value) return
// Check if map is set
if (!mapEditor.currentMap.value) return
// Check if tool is pencil
if (mapEditorStore.tool !== 'pencil') return
// Check if draw mode is tile
if (mapEditorStore.drawMode !== 'tile') return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if alt is pressed
if (!pointer.event.altKey) return
// Check if there is a tile // Check if there is a tile
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY) const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
if (!tile) return if (!tile) return
// Select the tile // Select the tile
mapEditorStore.setSelectedMapObject(mapEditor.currentMap.value.tiles[tile.y][tile.x]) mapEditor.setSelectedTile(map.tiles[tile.y][tile.x])
} }
watch( watch(
() => mapEditorStore.shouldClearTiles, () => mapEditor.shouldClearTiles,
(shouldClear) => { (shouldClear) => {
if (shouldClear && mapEditor.currentMap.value && tileMap.value && tileLayer.value) { if (shouldClear && mapEditor.currentMap.value && tileMap.value && tileLayer.value) {
const blankTiles = createTileArray(tileMap.value.width, tileMap.value.height, 'blank_tile') const blankTiles = createTileArray(tileLayer.value.width, tileLayer.value.height, 'blank_tile')
setLayerTiles(tileMap.value, tileLayer.value, blankTiles) setLayerTiles(tileMap.value, tileLayer.value, blankTiles)
mapEditor.currentMap.value.tiles = blankTiles mapEditor.currentMap.value.tiles = blankTiles
mapEditorStore.resetClearTilesFlag() mapEditor.resetClearTilesFlag()
} }
} }
) )
function handlePointer(pointer: Phaser.Input.Pointer) {
if (!tileMap.value || !tileLayer.value) return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if alt is pressed
if (pointer.event.altKey) {
tilePicker(pointer)
return
}
// Check if draw mode is tile
switch (mapEditor.tool.value) {
case 'pencil':
pencil(pointer)
break
case 'eraser':
eraser(pointer)
break
case 'paint':
paint(pointer)
break
}
}
onMounted(async () => { onMounted(async () => {
if (!mapEditor.currentMap.value?.tiles) return if (!mapEditor.currentMap.value) return
console.log(mapEditor.currentMap.value)
tileMap.value = createTileMap() tileMap.value = createTileMap()
tileLayer.value = await createTileLayer(tileMap.value) tileLayer.value = await createTileLayer(tileMap.value)
@ -212,19 +182,9 @@ onMounted(async () => {
} }
setLayerTiles(tileMap.value, tileLayer.value, blankTiles) setLayerTiles(tileMap.value, tileLayer.value, blankTiles)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, paint)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, tilePicker)
}) })
onUnmounted(() => { onUnmounted(() => {
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, paint)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, tilePicker)
if (tileMap.value) { if (tileMap.value) {
tileMap.value.destroyLayer('tiles') tileMap.value.destroyLayer('tiles')
tileMap.value.removeAllLayers() tileMap.value.removeAllLayers()

View File

@ -11,7 +11,7 @@ import { Image, useScene } from 'phavuer'
import { computed } from 'vue' import { computed } from 'vue'
const props = defineProps<{ const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap tileMap: Phaser.Tilemaps.Tilemap
placedMapObject: PlacedMapObject placedMapObject: PlacedMapObject
selectedPlacedMapObject: PlacedMapObject | null selectedPlacedMapObject: PlacedMapObject | null
movingPlacedMapObject: PlacedMapObject | null movingPlacedMapObject: PlacedMapObject | null
@ -24,8 +24,8 @@ const imageProps = computed(() => ({
alpha: props.movingPlacedMapObject?.id === props.placedMapObject.id ? 0.5 : 1, alpha: props.movingPlacedMapObject?.id === props.placedMapObject.id ? 0.5 : 1,
tint: props.selectedPlacedMapObject?.id === props.placedMapObject.id ? 0x00ff00 : 0xffffff, tint: props.selectedPlacedMapObject?.id === props.placedMapObject.id ? 0x00ff00 : 0xffffff,
depth: calculateIsometricDepth(props.placedMapObject.positionX, props.placedMapObject.positionY, props.placedMapObject.mapObject.frameWidth, props.placedMapObject.mapObject.frameHeight), depth: calculateIsometricDepth(props.placedMapObject.positionX, props.placedMapObject.positionY, props.placedMapObject.mapObject.frameWidth, props.placedMapObject.mapObject.frameHeight),
x: tileToWorldX(props.tilemap, props.placedMapObject.positionX, props.placedMapObject.positionY), x: tileToWorldX(props.tileMap, props.placedMapObject.positionX, props.placedMapObject.positionY),
y: tileToWorldY(props.tilemap, props.placedMapObject.positionX, props.placedMapObject.positionY), y: tileToWorldY(props.tileMap, props.placedMapObject.positionX, props.placedMapObject.positionY),
flipX: props.placedMapObject.isRotated, flipX: props.placedMapObject.isRotated,
texture: props.placedMapObject.mapObject.id, texture: props.placedMapObject.mapObject.id,
originY: Number(props.placedMapObject.mapObject.originX), originY: Number(props.placedMapObject.mapObject.originX),

View File

@ -1,146 +1,77 @@
<template> <template>
<SelectedPlacedMapObjectComponent v-if="selectedPlacedMapObject" :placedMapObject="selectedPlacedMapObject" @move="moveMapObject" @rotate="rotatePlacedMapObject" @delete="deletePlacedMapObject" /> <SelectedPlacedMapObjectComponent v-if="selectedPlacedMapObject" :placedMapObject="selectedPlacedMapObject" @move="moveMapObject" @rotate="rotatePlacedMapObject" @delete="deletePlacedMapObject" />
<PlacedMapObject v-for="placedMapObject in mapEditorStore.map?.placedMapObjects" :tilemap="tilemap" :placedMapObject :selectedPlacedMapObject :movingPlacedMapObject @pointerup="clickPlacedMapObject(placedMapObject)" /> <PlacedMapObject v-for="placedMapObject in mapEditor.currentMap.value?.placedMapObjects" :tileMap="tileMap" :placedMapObject :selectedPlacedMapObject :movingPlacedMapObject @pointerup="clickPlacedMapObject(placedMapObject)" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { PlacedMapObject as PlacedMapObjectT } from '@/application/types' import type { Map as MapT, PlacedMapObject as PlacedMapObjectT } from '@/application/types'
import { uuidv4 } from '@/application/utilities' import { uuidv4 } from '@/application/utilities'
import PlacedMapObject from '@/components/gameMaster/mapEditor/mapPartials/PlacedMapObject.vue' import PlacedMapObject from '@/components/gameMaster/mapEditor/mapPartials/PlacedMapObject.vue'
import SelectedPlacedMapObjectComponent from '@/components/gameMaster/mapEditor/partials/SelectedPlacedMapObject.vue' import SelectedPlacedMapObjectComponent from '@/components/gameMaster/mapEditor/partials/SelectedPlacedMapObject.vue'
import { getTile } from '@/composables/mapComposable' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { useScene } from 'phavuer' import { useScene } from 'phavuer'
import { onMounted, onUnmounted, ref, watch } from 'vue' import { ref, watch } from 'vue'
const scene = useScene() const scene = useScene()
const mapEditorStore = useMapEditorStore() const mapEditor = useMapEditorComposable()
const selectedPlacedMapObject = ref<PlacedMapObjectT | null>(null) const selectedPlacedMapObject = ref<PlacedMapObjectT | null>(null)
const movingPlacedMapObject = ref<PlacedMapObjectT | null>(null) const movingPlacedMapObject = ref<PlacedMapObjectT | null>(null)
const props = defineProps<{ const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap tileMap: Phaser.Tilemaps.Tilemap
}>() }>()
function pencil(pointer: Phaser.Input.Pointer) { defineExpose({ handlePointer })
// Check if map is set
if (!mapEditorStore.map) return
// Check if tool is pencil
if (mapEditorStore.tool !== 'pencil') return
// Check if draw mode is map_object
if (mapEditorStore.drawMode !== 'map_object') return
// Check if there is a selected object
if (!mapEditorStore.selectedMapObject) return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if alt is pressed, this means we are selecting the object
if (pointer.event.altKey) return
// Check if there is a tile
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
function pencil(pointer: Phaser.Input.Pointer, map: MapT) {
// Check if object already exists on position // Check if object already exists on position
const existingPlacedMapObject = mapEditorStore.map.placedMapObjects.find((placedMapObject) => placedMapObject.positionX === tile.x && placedMapObject.positionY === tile.y) const existingPlacedMapObject = findInMap(pointer, map)
if (existingPlacedMapObject) return if (existingPlacedMapObject) return
const newPlacedMapObject = { const newPlacedMapObject: PlacedMapObjectT = {
id: uuidv4(), id: uuidv4(),
map: mapEditorStore.map, depth: 0,
mapObject: mapEditorStore.selectedMapObject, map: map,
mapObject: mapEditor.selectedMapObject.value!,
isRotated: false, isRotated: false,
positionX: tile.x, positionX: pointer.x,
positionY: tile.y positionY: pointer.y
} }
// Add new object to mapObjects // Add new object to mapObjects
mapEditorStore.map.placedMapObjects = mapEditorStore.map.placedMapObjects.concat(newPlacedMapObject as PlacedMapObjectT) map.placedMapObjects.concat(newPlacedMapObject)
} }
function eraser(pointer: Phaser.Input.Pointer) { function eraser(pointer: Phaser.Input.Pointer, map: MapT) {
// Check if map is set
if (!mapEditorStore.map) return
// Check if tool is eraser
if (mapEditorStore.tool !== 'eraser') return
// Check if draw mode is map_object
if (mapEditorStore.eraserMode !== 'map_object') return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if alt is pressed, this means we are selecting the object
if (pointer.event.altKey) return
// Check if there is a tile
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if object already exists on position // Check if object already exists on position
const existingPlacedMapObject = mapEditorStore.map.placedMapObjects.find((placedMapObject) => placedMapObject.positionX === tile.x && placedMapObject.positionY === tile.y) const existingPlacedMapObject = findInMap(pointer, map)
if (!existingPlacedMapObject) return if (!existingPlacedMapObject) return
// Remove existing object // Remove existing object
mapEditorStore.map.placedMapObjects = mapEditorStore.map.placedMapObjects.filter((placedMapObject) => placedMapObject.id !== existingPlacedMapObject.id) map.placedMapObjects = map.placedMapObjects.filter((placedMapObject) => placedMapObject.id !== existingPlacedMapObject.id)
} }
function objectPicker(pointer: Phaser.Input.Pointer) { function findInMap(pointer: Phaser.Input.Pointer, map: MapT) {
// Check if map is set return map.placedMapObjects.find((placedMapObject) => placedMapObject.positionX === pointer.worldX && placedMapObject.positionY === pointer.worldY)
if (!mapEditorStore.map) return }
// Check if tool is pencil
if (mapEditorStore.tool !== 'pencil') return
// Check if draw mode is map_object
if (mapEditorStore.drawMode !== 'map_object') return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// If alt is not pressed, return
if (!pointer.event.altKey) return
// Check if there is a tile
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
function objectPicker(pointer: Phaser.Input.Pointer, map: MapT) {
// Check if object already exists on position // Check if object already exists on position
const existingPlacedMapObject = mapEditorStore.map.placedMapObjects.find((placedMapObject) => placedMapObject.positionX === tile.x && placedMapObject.positionY === tile.y) const existingPlacedMapObject = findInMap(pointer, map)
if (!existingPlacedMapObject) return if (!existingPlacedMapObject) return
// Select the object // Select the object
mapEditorStore.setSelectedMapObject(existingPlacedMapObject.mapObject) mapEditor.setSelectedMapObject(existingPlacedMapObject.mapObject)
} }
function moveMapObject(id: string) { function moveMapObject(id: string, map: MapT) {
// Check if map is set movingPlacedMapObject.value = map.placedMapObjects.find((object) => object.id === id) as PlacedMapObjectT
if (!mapEditorStore.map) return
movingPlacedMapObject.value = mapEditorStore.map.placedMapObjects.find((object) => object.id === id) as PlacedMapObjectT
function handlePointerMove(pointer: Phaser.Input.Pointer) { function handlePointerMove(pointer: Phaser.Input.Pointer) {
if (!movingPlacedMapObject.value) return if (!movingPlacedMapObject.value) return
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY) movingPlacedMapObject.value.positionX = pointer.worldX
if (!tile) return movingPlacedMapObject.value.positionY = pointer.worldY
movingPlacedMapObject.value.positionX = tile.x
movingPlacedMapObject.value.positionY = tile.y
} }
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove) scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
@ -153,11 +84,8 @@ function moveMapObject(id: string) {
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp) scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
} }
function rotatePlacedMapObject(id: string) { function rotatePlacedMapObject(id: string, map: MapT) {
// Check if map is set map.placedMapObjects = map.placedMapObjects.map((placedMapObject) => {
if (!mapEditorStore.map) return
mapEditorStore.map.placedMapObjects = mapEditorStore.map.placedMapObjects.map((placedMapObject) => {
if (placedMapObject.id === id) { if (placedMapObject.id === id) {
return { return {
...placedMapObject, ...placedMapObject,
@ -168,11 +96,8 @@ function rotatePlacedMapObject(id: string) {
}) })
} }
function deletePlacedMapObject(id: string) { function deletePlacedMapObject(id: string, map: MapT) {
// Check if map is set map.placedMapObjects = map.placedMapObjects.filter((object) => object.id !== id)
if (!mapEditorStore.map) return
mapEditorStore.map.placedMapObjects = mapEditorStore.map.placedMapObjects.filter((object) => object.id !== id)
selectedPlacedMapObject.value = null selectedPlacedMapObject.value = null
} }
@ -181,41 +106,55 @@ function clickPlacedMapObject(placedMapObject: PlacedMapObjectT) {
// If alt is pressed, select the object // If alt is pressed, select the object
if (scene.input.activePointer.event.altKey) { if (scene.input.activePointer.event.altKey) {
mapEditorStore.setSelectedMapObject(placedMapObject.mapObject) mapEditor.setSelectedMapObject(placedMapObject.mapObject)
} }
} }
onMounted(() => { function handlePointer(pointer: Phaser.Input.Pointer) {
scene.input.on(Phaser.Input.Events.POINTER_DOWN, pencil) const map = mapEditor.currentMap.value
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil) if (!map) return
scene.input.on(Phaser.Input.Events.POINTER_DOWN, eraser)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, objectPicker)
})
onUnmounted(() => { if (mapEditor.drawMode.value !== 'map_object') return
scene.input.off(Phaser.Input.Events.POINTER_DOWN, pencil)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil) // Check if left mouse button is pressed
scene.input.off(Phaser.Input.Events.POINTER_DOWN, eraser) if (!pointer.isDown) return
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, objectPicker) // Check if shift is not pressed, this means we are moving the camera
}) if (pointer.event.shiftKey) return
// Check if alt is pressed, this means we are selecting the object
if (pointer.event.altKey) return
// Check if tool is pencil
switch (mapEditor.tool.value) {
case 'pencil':
if (mapEditor.selectedMapObject.value) pencil(pointer, map)
break
case 'eraser':
eraser(pointer, map)
break
case 'object picker':
objectPicker(pointer, map)
break
}
}
// watch mapEditorStore.mapObjectList and update originX and originY of objects in mapObjects // watch mapEditorStore.mapObjectList and update originX and originY of objects in mapObjects
watch( watch(
() => mapEditorStore.mapObjectList, () => mapEditor.currentMap.value,
(newMapObjects) => { () => {
if (!mapEditorStore.map) return const map = mapEditor.currentMap.value
if (!map) return
const updatedMapObjects = mapEditorStore.map.placedMapObjects.map((mapObject) => { const updatedMapObjects = map.placedMapObjects.map((mapObject) => {
const updatedMapObject = newMapObjects.find((obj) => obj.id === mapObject.mapObject.id) const updatedMapObject = map.placedMapObjects.find((obj) => obj.id === mapObject.mapObject.id)
if (updatedMapObject) { if (updatedMapObject) {
return { return {
...mapObject, ...mapObject,
mapObject: { mapObject: {
...mapObject.mapObject, ...mapObject.mapObject,
originX: updatedMapObject.originX, originX: updatedMapObject.positionX,
originY: updatedMapObject.originY originY: updatedMapObject.positionY
} }
} }
} }
@ -223,19 +162,16 @@ watch(
}) })
// Update the map with the new mapObjects // Update the map with the new mapObjects
mapEditorStore.setMap({ map.placedMapObjects = [...map.placedMapObjects, ...updatedMapObjects]
...mapEditorStore.map,
placedMapObjects: updatedMapObjects
})
// Update selectedMapObject if it's set // Update mapObject if it's set
if (mapEditorStore.selectedMapObject) { if (mapEditor.selectedMapObject.value) {
const updatedMapObject = newMapObjects.find((obj) => obj.id === mapEditorStore.selectedMapObject?.id) const updatedMapObject = map.placedMapObjects.find((obj) => obj.id === mapEditor.selectedMapObject.value?.id)
if (updatedMapObject) { if (updatedMapObject) {
mapEditorStore.setSelectedMapObject({ mapEditor.setSelectedMapObject({
...mapEditorStore.selectedMapObject, ...mapEditor.selectedMapObject.value,
originX: updatedMapObject.originX, originX: updatedMapObject.positionX,
originY: updatedMapObject.originY originY: updatedMapObject.positionY
}) })
} }
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<Modal :isModalOpen="mapEditorStore.isCreateMapModalShown" @modal:close="() => mapEditorStore.toggleCreateMapModal()" :modal-width="300" :modal-height="420" :is-resizable="false" :bg-style="'none'"> <Modal ref="modalRef" :modal-width="300" :modal-height="420" :is-resizable="false" :bg-style="'none'">
<template #modalHeader> <template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">Create new map</h3> <h3 class="m-0 font-medium shrink-0 text-white">Create new map</h3>
</template> </template>
@ -13,11 +13,11 @@
</div> </div>
<div class="form-field-half"> <div class="form-field-half">
<label for="name">Width</label> <label for="name">Width</label>
<input class="input-field max-w-64" v-model="width" name="name" id="name" type="number" /> <input class="input-field max-w-64" v-model="width" name="width" id="width" type="number" />
</div> </div>
<div class="form-field-half"> <div class="form-field-half">
<label for="name">Height</label> <label for="name">Height</label>
<input class="input-field max-w-64" v-model="height" name="name" id="name" type="number" /> <input class="input-field max-w-64" v-model="height" name="height" id="height" type="number" />
</div> </div>
<div class="form-field-full"> <div class="form-field-full">
<label for="name">PVP enabled</label> <label for="name">PVP enabled</label>
@ -37,22 +37,24 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Map } from '@/application/types' import type { Map } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { MapStorage } from '@/storage/storages' import { MapStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore' import { ref, useTemplateRef } from 'vue'
import { ref } from 'vue'
const emit = defineEmits(['create']) const emit = defineEmits(['create'])
const gameStore = useGameStore() const gameStore = useGameStore()
const mapEditorStore = useMapEditorStore()
const mapStorage = new MapStorage() const mapStorage = new MapStorage()
const modalRef = useTemplateRef('modalRef')
const name = ref('') const name = ref('')
const width = ref(0) const width = ref(0)
const height = ref(0) const height = ref(0)
const pvp = ref(false) const pvp = ref(false)
defineExpose({ open: () => modalRef.value?.open() })
async function submit() { async function submit() {
gameStore.connection?.emit('gm:map:create', { name: name.value, width: width.value, height: height.value }, async (response: Map | false) => { gameStore.connection?.emit('gm:map:create', { name: name.value, width: width.value, height: height.value }, async (response: Map | false) => {
if (!response) { if (!response) {
@ -79,6 +81,6 @@ async function submit() {
}) })
// Close modal // Close modal
mapEditorStore.toggleCreateMapModal() modalRef.value?.close()
} }
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<Modal :is-modal-open="mapEditorStore.isMapListModalShown" @modal:close="() => mapEditorStore.toggleMapListModal()" :is-resizable="false" :modal-width="300" :modal-height="360" :bg-style="'none'"> <Modal ref="modalRef" :is-resizable="false" :modal-width="300" :modal-height="360" :bg-style="'none'">
<template #modalHeader> <template #modalHeader>
<h3 class="text-lg text-white">Maps</h3> <h3 class="text-lg text-white">Maps</h3>
</template> </template>
@ -7,7 +7,7 @@
<div class="my-4 mx-auto h-full"> <div class="my-4 mx-auto h-full">
<div class="text-center mb-4 px-2 flex gap-2.5"> <div class="text-center mb-4 px-2 flex gap-2.5">
<button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="fetchMaps">Refresh</button> <button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="fetchMaps">Refresh</button>
<button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="() => mapEditorStore.toggleCreateMapModal()">New</button> <button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="createMapModal?.open">New</button>
</div> </div>
<div class="overflow-y-auto h-[calc(100%-20px)]"> <div class="overflow-y-auto h-[calc(100%-20px)]">
<div class="relative p-2.5 cursor-pointer flex gap-y-2.5 gap-x-5 flex-wrap" v-for="(map, index) in mapList" :key="map.id"> <div class="relative p-2.5 cursor-pointer flex gap-y-2.5 gap-x-5 flex-wrap" v-for="(map, index) in mapList" :key="map.id">
@ -25,7 +25,7 @@
</template> </template>
</Modal> </Modal>
<CreateMap @create="fetchMaps" v-if="mapEditorStore.isMapListModalShown" /> <CreateMap ref="createMapModal" @create="fetchMaps" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -36,14 +36,21 @@ import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { MapStorage } from '@/storage/storages' import { MapStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore' import { useMapEditorStore } from '@/stores/mapEditorStore'
import { onMounted, ref } from 'vue' import { onMounted, ref, useTemplateRef } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const mapEditorStore = useMapEditorStore()
const mapEditor = useMapEditorComposable() const mapEditor = useMapEditorComposable()
const mapStorage = new MapStorage() const mapStorage = new MapStorage()
const mapList = ref<Map[]>([]) const mapList = ref<Map[]>([])
const modalRef = useTemplateRef('modalRef')
const createMapModal = useTemplateRef('createMapModal')
defineEmits(['open-create-map'])
defineExpose({
open: () => modalRef.value?.open()
})
onMounted(async () => { onMounted(async () => {
await fetchMaps() await fetchMaps()
@ -57,7 +64,7 @@ function loadMap(id: UUID) {
gameStore.connection?.emit('gm:map:request', { mapId: id }, (response: Map) => { gameStore.connection?.emit('gm:map:request', { mapId: id }, (response: Map) => {
mapEditor.loadMap(response) mapEditor.loadMap(response)
}) })
mapEditorStore.toggleMapListModal() modalRef.value?.close()
} }
async function deleteMap(id: UUID) { async function deleteMap(id: UUID) {

View File

@ -1,58 +1,66 @@
<template> <template>
<Modal :isModalOpen="mapEditorStore.isMapObjectListModalShown" :modal-width="645" :modal-height="260" @modal:close="() => (mapEditorStore.isMapObjectListModalShown = false)" :bg-style="'none'"> <div class="absolute border-0 border-l-2 border-solid border-gray-500 w-1/4 min-w-80 flex flex-col top-0 right-0 z-10 h-dvh bg-gray-800 z-20" v-if="isOpen">
<template #modalHeader> <div class="relative z-10 p-2.5 border-solid border-0 border-b border-gray-500">
<h3 class="text-lg text-white">Map objects</h3> <h3 class="text-lg text-white">Map objects</h3>
</template> </div>
<template #modalBody> <div class="overflow-hidden grow relative">
<div class="flex pt-4 pl-4"> <div class="absolute w-full h-full top-0 left-0">
<div class="w-full flex gap-1.5 flex-row"> <div class="relative z-10 h-full">
<div> <div class="flex pt-4 pl-4">
<label class="mb-1.5 font-titles hidden" for="search">Search...</label> <div class="w-full flex gap-1.5 flex-row">
<input @mousedown.stop class="input-field" type="text" name="search" placeholder="Search" v-model="searchQuery" /> <div>
<label class="mb-1.5 font-titles hidden" for="search">Search...</label>
<input @mousedown.stop class="input-field" type="text" name="search" placeholder="Search" v-model="searchQuery" />
</div>
</div>
</div> </div>
</div> <div class="flex flex-col h-[calc(100%_-_170px)] p-4 pb-24">
</div> <div class="mb-4 flex flex-wrap gap-2">
<div class="flex flex-col h-full p-4"> <button v-for="tag in uniqueTags" :key="tag" @click="toggleTag(tag)" class="btn-cyan" :class="{ 'opacity-50': !selectedTags.includes(tag) }">
<div class="mb-4 flex flex-wrap gap-2"> {{ tag }}
<button v-for="tag in uniqueTags" :key="tag" @click="toggleTag(tag)" class="btn-cyan" :class="{ 'opacity-50': !selectedTags.includes(tag) }"> </button>
{{ tag }} </div>
</button> <div class="h-full overflow-auto">
</div> <div class="flex justify-between flex-wrap gap-2.5 items-center">
<div class="h-full overflow-auto"> <div v-for="(mapObject, index) in filteredMapObjects" :key="index" class="max-w-1/4 inline-block">
<div class="flex justify-between flex-wrap gap-2.5 items-center"> <img
<div v-for="(mapObject, index) in filteredMapObjects" :key="index" class="max-w-1/4 inline-block"> class="border-2 border-solid rounded max-w-full"
<img :src="`${config.server_endpoint}/textures/map_objects/${mapObject.id}.png`"
class="border-2 border-solid max-w-full" alt="Object"
:src="`${config.server_endpoint}/textures/map_objects/${mapObject.id}.png`" @click="mapEditor.setSelectedMapObject(mapObject)"
alt="Object" :class="{
@click="mapEditorStore.setSelectedMapObject(mapObject)" 'cursor-pointer transition-all duration-300': true,
:class="{ 'border-cyan shadow-lg': mapEditor.selectedMapObject.value?.id === mapObject.id,
'cursor-pointer transition-all duration-300': true, 'border-transparent hover:border-gray-300': mapEditor.selectedMapObject.value?.id !== mapObject.id
'border-cyan shadow-lg scale-105': mapEditorStore.selectedMapObject?.id === mapObject.id, }"
'border-transparent hover:border-gray-300': mapEditorStore.selectedMapObject?.id !== mapObject.id />
}" </div>
/> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</template> </div>
</Modal> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import config from '@/application/config'
import type { MapObject } from '@/application/types' import type { MapObject } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { MapObjectStorage } from '@/storage/storages' import { MapObjectStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { liveQuery } from 'dexie' import { liveQuery } from 'dexie'
import { computed, onMounted, onUnmounted, ref } from 'vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
defineExpose({
open: () => (isOpen.value = true),
close: () => (isOpen.value = false),
toggle: () => (isOpen.value = !isOpen.value)
})
const isOpen = ref(false)
const mapObjectStorage = new MapObjectStorage() const mapObjectStorage = new MapObjectStorage()
const isModalOpen = ref(false) const mapEditor = useMapEditorComposable()
const mapEditorStore = useMapEditorStore()
const searchQuery = ref('') const searchQuery = ref('')
const selectedTags = ref<string[]>([]) const selectedTags = ref<string[]>([])
const mapObjectList = ref<MapObject[]>([]) const mapObjectList = ref<MapObject[]>([])
@ -81,13 +89,12 @@ const toggleTag = (tag: string) => {
let subscription: any = null let subscription: any = null
onMounted(() => { onMounted(() => {
isModalOpen.value = true
subscription = liveQuery(() => mapObjectStorage.liveQuery()).subscribe({ subscription = liveQuery(() => mapObjectStorage.liveQuery()).subscribe({
next: (result) => { next: (result) => {
mapObjectList.value = result mapObjectList.value = result
}, },
error: (error) => { error: (error) => {
console.error('Failed to fetch tiles:', error) console.error('Failed to fetch objects:', error)
} }
}) })
}) })

View File

@ -1,5 +1,5 @@
<template> <template>
<Modal :is-modal-open="mapEditorStore.isSettingsModalShown" @modal:close="() => mapEditorStore.toggleSettingsModal()" :modal-width="600" :modal-height="430" :bg-style="'none'"> <Modal ref="modalRef" :modal-width="600" :modal-height="430" :bg-style="'none'">
<template #modalHeader> <template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">Map settings</h3> <h3 class="m-0 font-medium shrink-0 text-white">Map settings</h3>
</template> </template>
@ -51,11 +51,9 @@ import type { UUID } from '@/application/types'
import { uuidv4 } from '@/application/utilities' import { uuidv4 } from '@/application/utilities'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { useMapEditorStore } from '@/stores/mapEditorStore' import { ref, useTemplateRef, watch } from 'vue'
import { ref, watch } from 'vue'
const mapEditor = useMapEditorComposable() const mapEditor = useMapEditorComposable()
const mapEditorStore = useMapEditorStore()
const screen = ref('settings') const screen = ref('settings')
const name = ref(mapEditor.currentMap.value?.name) const name = ref(mapEditor.currentMap.value?.name)
@ -63,6 +61,11 @@ const width = ref(mapEditor.currentMap.value?.width)
const height = ref(mapEditor.currentMap.value?.height) const height = ref(mapEditor.currentMap.value?.height)
const pvp = ref(mapEditor.currentMap.value?.pvp) const pvp = ref(mapEditor.currentMap.value?.pvp)
const mapEffects = ref(mapEditor.currentMap.value?.mapEffects || []) const mapEffects = ref(mapEditor.currentMap.value?.mapEffects || [])
const modalRef = useTemplateRef('modalRef')
defineExpose({
open: () => modalRef.value?.open()
})
watch(name, (value) => { watch(name, (value) => {
mapEditor.updateProperty('name', value!) mapEditor.updateProperty('name', value!)

View File

@ -1,5 +1,5 @@
<template> <template>
<Modal :is-modal-open="showTeleportModal" @modal:close="() => mapEditorStore.setTool('move')" :modal-width="300" :modal-height="350" :is-resizable="false" :bg-style="'none'"> <Modal ref="modalRef" @modal:close="() => mapEditorStore.setTool('move')" :modal-width="300" :modal-height="350" :is-resizable="false" :bg-style="'none'">
<template #modalHeader> <template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">Teleport settings</h3> <h3 class="m-0 font-medium shrink-0 text-white">Teleport settings</h3>
</template> </template>
@ -28,7 +28,7 @@
<label for="toMap">Map to teleport to</label> <label for="toMap">Map to teleport to</label>
<select v-model="toMap" class="input-field" name="toMap" id="toMap"> <select v-model="toMap" class="input-field" name="toMap" id="toMap">
<option :value="null">Select map</option> <option :value="null">Select map</option>
<option v-for="map in mapEditorStore.mapList" :key="map.id" :value="map">{{ map.name }}</option> <option v-for="map in mapList" :key="map.id" :value="map">{{ map.name }}</option>
</select> </select>
</div> </div>
</div> </div>
@ -43,17 +43,23 @@ import type { Map } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore' import { useMapEditorStore } from '@/stores/mapEditorStore'
import { computed, onMounted, ref, watch } from 'vue' import { computed, onMounted, ref, useTemplateRef, watch } from 'vue'
const showTeleportModal = computed(() => mapEditorStore.tool === 'pencil' && mapEditorStore.drawMode === 'teleport') const showTeleportModal = computed(() => mapEditorStore.tool === 'pencil' && mapEditorStore.drawMode === 'teleport')
const mapEditorStore = useMapEditorStore() const mapEditorStore = useMapEditorStore()
const gameStore = useGameStore() const gameStore = useGameStore()
const mapList = ref<Map[]>([])
const modalRef = useTemplateRef('modalRef')
defineExpose({
open: () => modalRef.value?.open()
})
onMounted(fetchMaps) onMounted(fetchMaps)
function fetchMaps() { function fetchMaps() {
gameStore.connection?.emit('gm:map:list', {}, (response: Map[]) => { gameStore.connection?.emit('gm:map:list', {}, (response: Map[]) => {
mapEditorStore.setMapList(response) mapList.value = response
}) })
} }
@ -65,7 +71,7 @@ function useRefTeleportSettings() {
toPositionX: ref(settings.toPositionX), toPositionX: ref(settings.toPositionX),
toPositionY: ref(settings.toPositionY), toPositionY: ref(settings.toPositionY),
toRotation: ref(settings.toRotation), toRotation: ref(settings.toRotation),
toMap: ref(settings.toMap) toMap: ref(settings.toMapId)
} }
} }
@ -76,7 +82,7 @@ function updateTeleportSettings() {
toPositionX: toPositionX.value, toPositionX: toPositionX.value,
toPositionY: toPositionY.value, toPositionY: toPositionY.value,
toRotation: toRotation.value, toRotation: toRotation.value,
toMap: toMap.value toMapId: toMap.value
}) })
} }
</script> </script>

View File

@ -1,102 +1,113 @@
<template> <template>
<Modal :isModalOpen="mapEditorStore.isTileListModalShown" :modal-width="645" :modal-height="600" @modal:close="() => (mapEditorStore.isTileListModalShown = false)" :bg-style="'none'"> <div class="absolute border-0 border-l-2 border-solid border-gray-500 w-1/4 min-w-80 flex flex-col top-0 right-0 z-10 h-dvh bg-gray-800 z-20" v-if="isOpen">
<template #modalHeader> <div class="relative z-10 p-2.5 border-solid border-0 border-b border-gray-500">
<h3 class="text-lg text-white">Tiles</h3> <h3 class="text-lg text-white">Tiles</h3>
</template> </div>
<template #modalBody> <div class="overflow-hidden grow relative">
<div class="h-full overflow-auto" v-if="!selectedGroup"> <div class="absolute top-0 left-0 h-full w-full">
<div class="flex pt-4 pl-4"> <div class="relative z-10 h-full">
<div class="w-full flex gap-1.5 flex-row"> <div class="h-full" v-if="!selectedGroup">
<div> <div class="flex pt-4 pl-4">
<label class="mb-1.5 font-titles hidden" for="search">Search...</label> <div class="w-full flex gap-1.5 flex-row">
<input @mousedown.stop class="input-field" type="text" name="search" placeholder="Search" v-model="searchQuery" /> <div>
<label class="mb-1.5 font-titles hidden" for="search">Search...</label>
<input @mousedown.stop class="input-field" type="text" name="search" placeholder="Search" v-model="searchQuery" />
</div>
</div>
</div>
<div class="flex flex-col h-[calc(100%_-_170px)] p-4 pb-24">
<div class="mb-4 flex flex-wrap gap-2">
<button v-for="tag in uniqueTags" :key="tag" @click="toggleTag(tag)" class="btn-cyan" :class="{ 'opacity-50': !selectedTags.includes(tag) }">
{{ tag }}
</button>
</div>
<div class="h-full flex-grow overflow-y-auto">
<div class="grid grid-cols-4 gap-2 justify-items-center">
<div v-for="group in groupedTiles" :key="group.parent.id" class="flex flex-col items-center justify-center relative">
<img
class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300"
:src="`${config.server_endpoint}/textures/tiles/${group.parent.id}.png`"
:alt="group.parent.name"
@click="openGroup(group)"
@load="() => tileProcessor.processTile(group.parent)"
:class="{
'border-cyan shadow-lg': isActiveTile(group.parent),
'border-transparent hover:border-gray-300': !isActiveTile(group.parent)
}"
/>
<span class="text-xs mt-1">{{ getTileCategory(group.parent) }}</span>
<span v-if="group.children.length > 0" class="absolute top-0 right-0 bg-cyan text-white rounded-full w-5 h-5 flex items-center justify-center text-xs">
{{ group.children.length + 1 }}
</span>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> <div v-else class="h-full overflow-auto">
<div class="flex flex-col h-full p-4"> <div class="p-4">
<div class="mb-4 flex flex-wrap gap-2"> <button @click="closeGroup" class="btn-cyan mb-4">Back to All Tiles</button>
<button v-for="tag in uniqueTags" :key="tag" @click="toggleTag(tag)" class="btn-cyan" :class="{ 'opacity-50': !selectedTags.includes(tag) }"> <h4 class="text-lg mb-4">{{ selectedGroup.parent.name }} Group</h4>
{{ tag }} <div class="grid grid-cols-4 gap-2 justify-items-center">
</button> <div class="flex flex-col items-center justify-center">
</div> <img
<div class="h-[calc(100%_-_60px)] flex-grow overflow-y-auto"> class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300"
<div class="grid grid-cols-8 gap-2 justify-items-center"> :src="`${config.server_endpoint}/textures/tiles/${selectedGroup.parent.id}.png`"
<div v-for="group in groupedTiles" :key="group.parent.id" class="flex flex-col items-center justify-center relative"> :alt="selectedGroup.parent.name"
<img @click="selectTile(selectedGroup.parent.id)"
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300" :class="{
:src="`${config.server_endpoint}/textures/tiles/${group.parent.id}.png`" 'border-cyan shadow-lg': isActiveTile(selectedGroup.parent),
:alt="group.parent.name" 'border-transparent hover:border-gray-300': !isActiveTile(selectedGroup.parent)
@click="openGroup(group)" }"
@load="() => processTile(group.parent)" />
:class="{ <span class="text-xs mt-1">{{ getTileCategory(selectedGroup.parent) }}</span>
'border-cyan shadow-lg scale-105': isActiveTile(group.parent), </div>
'border-transparent hover:border-gray-300': !isActiveTile(group.parent) <div v-for="childTile in selectedGroup.children" :key="childTile.id" class="flex flex-col items-center justify-center">
}" <img
/> class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300"
<span class="text-xs mt-1">{{ getTileCategory(group.parent) }}</span> :src="`${config.server_endpoint}/textures/tiles/${childTile.id}.png`"
<span v-if="group.children.length > 0" class="absolute top-0 right-0 bg-cyan text-white rounded-full w-5 h-5 flex items-center justify-center text-xs"> :alt="childTile.name"
{{ group.children.length + 1 }} @click="selectTile(childTile.id)"
</span> :class="{
'border-cyan shadow-lg': isActiveTile(childTile),
'border-transparent hover:border-gray-300': !isActiveTile(childTile)
}"
/>
<span class="text-xs mt-1">{{ getTileCategory(childTile) }}</span>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div v-else class="h-full overflow-auto"> </div>
<div class="p-4"> </div>
<button @click="closeGroup" class="btn-cyan mb-4">Back to All Tiles</button>
<h4 class="text-lg mb-4">{{ selectedGroup.parent.name }} Group</h4>
<div class="grid grid-cols-8 gap-2 justify-items-center">
<div class="flex flex-col items-center justify-center">
<img
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
:src="`${config.server_endpoint}/textures/tiles/${selectedGroup.parent.id}.png`"
:alt="selectedGroup.parent.name"
@click="selectTile(selectedGroup.parent.id)"
:class="{
'border-cyan shadow-lg scale-105': isActiveTile(selectedGroup.parent),
'border-transparent hover:border-gray-300': !isActiveTile(selectedGroup.parent)
}"
/>
<span class="text-xs mt-1">{{ getTileCategory(selectedGroup.parent) }}</span>
</div>
<div v-for="childTile in selectedGroup.children" :key="childTile.id" class="flex flex-col items-center justify-center">
<img
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
:src="`${config.server_endpoint}/textures/tiles/${childTile.id}.png`"
:alt="childTile.name"
@click="selectTile(childTile.id)"
:class="{
'border-cyan shadow-lg scale-105': isActiveTile(childTile),
'border-transparent hover:border-gray-300': !isActiveTile(childTile)
}"
/>
<span class="text-xs mt-1">{{ getTileCategory(childTile) }}</span>
</div>
</div>
</div>
</div>
</template>
</Modal>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import config from '@/application/config'
import type { Tile } from '@/application/types' import type { Tile } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { useTileProcessingComposable } from '@/composables/useTileProcessingComposable'
import { TileStorage } from '@/storage/storages' import { TileStorage } from '@/storage/storages'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { liveQuery } from 'dexie'
import { computed, onMounted, onUnmounted, ref } from 'vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
const isOpen = ref(false)
const tileStorage = new TileStorage() const tileStorage = new TileStorage()
const mapEditorStore = useMapEditorStore() const mapEditor = useMapEditorComposable()
const tileProcessor = useTileProcessingComposable()
const searchQuery = ref('') const searchQuery = ref('')
const selectedTags = ref<string[]>([]) const selectedTags = ref<string[]>([])
const tileCategories = ref<Map<string, string>>(new Map()) const tileCategories = ref<Map<string, string>>(new Map())
const selectedGroup = ref<{ parent: Tile; children: Tile[] } | null>(null) const selectedGroup = ref<{ parent: Tile; children: Tile[] } | null>(null)
const tiles = ref<Tile[]>([]) const tiles = ref<Tile[]>([])
defineExpose({
open: () => (isOpen.value = true),
close: () => (isOpen.value = false),
toggle: () => (isOpen.value = !isOpen.value)
})
const uniqueTags = computed(() => { const uniqueTags = computed(() => {
const allTags = tiles.value.flatMap((tile) => tile.tags || []) const allTags = tiles.value.flatMap((tile) => tile.tags || [])
return Array.from(new Set(allTags)) return Array.from(new Set(allTags))
@ -111,7 +122,7 @@ const groupedTiles = computed(() => {
}) })
filteredTiles.forEach((tile) => { filteredTiles.forEach((tile) => {
const parentGroup = groups.find((group) => areTilesRelated(group.parent, tile)) const parentGroup = groups.find((group) => tileProcessor.areTilesRelated(group.parent, tile))
if (parentGroup && parentGroup.parent.id !== tile.id) { if (parentGroup && parentGroup.parent.id !== tile.id) {
parentGroup.children.push(tile) parentGroup.children.push(tile)
} else { } else {
@ -122,32 +133,6 @@ const groupedTiles = computed(() => {
return groups return groups
}) })
const tileColorData = ref<Map<string, { r: number; g: number; b: number }>>(new Map())
const tileEdgeData = ref<Map<string, number>>(new Map())
function areTilesRelated(tile1: Tile, tile2: Tile): boolean {
const colorSimilarityThreshold = 30 // Adjust this value as needed
const edgeComplexitySimilarityThreshold = 20 // Adjust this value as needed
const color1 = tileColorData.value.get(tile1.id)
const color2 = tileColorData.value.get(tile2.id)
const edge1 = tileEdgeData.value.get(tile1.id)
const edge2 = tileEdgeData.value.get(tile2.id)
if (!color1 || !color2 || edge1 === undefined || edge2 === undefined) {
return false
}
const colorDifference = Math.sqrt(Math.pow(color1.r - color2.r, 2) + Math.pow(color1.g - color2.g, 2) + Math.pow(color1.b - color2.b, 2))
const edgeComplexityDifference = Math.abs(edge1 - edge2)
const namePrefix1 = tile1.name.split('_')[0]
const namePrefix2 = tile2.name.split('_')[0]
return colorDifference <= colorSimilarityThreshold && edgeComplexityDifference <= edgeComplexitySimilarityThreshold && namePrefix1 === namePrefix2
}
const toggleTag = (tag: string) => { const toggleTag = (tag: string) => {
if (selectedTags.value.includes(tag)) { if (selectedTags.value.includes(tag)) {
selectedTags.value = selectedTags.value.filter((t) => t !== tag) selectedTags.value = selectedTags.value.filter((t) => t !== tag)
@ -156,59 +141,6 @@ const toggleTag = (tag: string) => {
} }
} }
function processTile(tile: Tile) {
const img = new Image()
img.crossOrigin = 'Anonymous'
img.onload = () => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = img.width
canvas.height = img.height
ctx!.drawImage(img, 0, 0, img.width, img.height)
const imageData = ctx!.getImageData(0, 0, canvas.width, canvas.height)
tileColorData.value.set(tile.id, getDominantColor(imageData))
tileEdgeData.value.set(tile.id, getEdgeComplexity(imageData))
}
img.src = `${config.server_endpoint}/textures/tiles/${tile.id}.png`
}
function getDominantColor(imageData: ImageData) {
let r = 0,
g = 0,
b = 0,
total = 0
for (let i = 0; i < imageData.data.length; i += 4) {
if (imageData.data[i + 3] > 0) {
// Only consider non-transparent pixels
r += imageData.data[i]
g += imageData.data[i + 1]
b += imageData.data[i + 2]
total++
}
}
return {
r: Math.round(r / total),
g: Math.round(g / total),
b: Math.round(b / total)
}
}
function getEdgeComplexity(imageData: ImageData) {
let edgePixels = 0
for (let y = 0; y < imageData.height; y++) {
for (let x = 0; x < imageData.width; x++) {
const i = (y * imageData.width + x) * 4
if (imageData.data[i + 3] > 0 && (x === 0 || y === 0 || x === imageData.width - 1 || y === imageData.height - 1 || imageData.data[i - 1] === 0 || imageData.data[i + 7] === 0)) {
edgePixels++
}
}
}
return edgePixels
}
function getTileCategory(tile: Tile): string { function getTileCategory(tile: Tile): string {
return tileCategories.value.get(tile.id) || '' return tileCategories.value.get(tile.id) || ''
} }
@ -222,29 +154,26 @@ function closeGroup() {
} }
function selectTile(tile: string) { function selectTile(tile: string) {
mapEditorStore.setSelectedTile(tile) mapEditor.setSelectedTile(tile)
} }
function isActiveTile(tile: Tile): boolean { function isActiveTile(tile: Tile): boolean {
return mapEditorStore.selectedTile === tile.id return mapEditor.selectedTile.value === tile.id
} }
let subscription: any = null onMounted(async () => {
tiles.value = await tileStorage.getAll()
const initialBatchSize = 20
const initialTiles = tiles.value.slice(0, initialBatchSize)
initialTiles.forEach((tile) => tileProcessor.processTile(tile))
onMounted(() => { // Process remaining tiles in background
subscription = liveQuery(() => tileStorage.liveQuery()).subscribe({ setTimeout(() => {
next: (result) => { tiles.value.slice(initialBatchSize).forEach((tile) => tileProcessor.processTile(tile))
tiles.value = result }, 1000)
},
error: (error) => {
console.error('Failed to fetch tiles:', error)
}
})
}) })
onUnmounted(() => { onUnmounted(() => {
if (subscription) { tileProcessor.cleanup()
subscription.unsubscribe()
}
}) })
</script> </script>

View File

@ -1,81 +1,86 @@
<template> <template>
<div class="flex justify-center p-5"> <div class="flex justify-center p-5">
<div class="toolbar fixed bottom-0 left-0 m-3 rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10"> <div class="toolbar fixed bottom-0 left-0 m-3 rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10 z-20">
<div ref="toolbar" class="tools flex gap-2.5" v-if="mapEditor.currentMap.value"> <div ref="toolbar" class="tools flex gap-2.5" v-if="mapEditor.currentMap.value">
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditorStore.tool === 'move' }" @click="handleClick('move')"> <button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditor.tool.value === 'move' }" @click="handleClick('move')">
<img class="invert w-5 h-5" src="/assets/icons/mapEditor/move.svg" alt="Move camera" /> <span class="h-5" :class="{ 'ml-2.5': mapEditorStore.tool !== 'move' }">(M)</span> <img class="invert w-5 h-5" src="/assets/icons/mapEditor/move.svg" alt="Move camera" /> <span class="h-5" :class="{ 'ml-2.5': mapEditor.tool.value !== 'move' }">(M)</span>
</button> </button>
<div class="w-px bg-cyan"></div> <div class="w-px bg-cyan"></div>
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditorStore.tool === 'pencil' }" @click="handleClick('pencil')"> <button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditor.tool.value === 'pencil' }" @click="handleClick('pencil')">
<img class="invert w-5 h-5" src="/assets/icons/mapEditor/pencil.svg" alt="Pencil" /> <span class="h-5" :class="{ 'ml-2.5': mapEditorStore.tool !== 'pencil' }">(P)</span> <img class="invert w-5 h-5" src="/assets/icons/mapEditor/pencil.svg" alt="Pencil" /> <span class="h-5" :class="{ 'ml-2.5': mapEditor.tool.value !== 'pencil' }">(P)</span>
<div class="select" v-if="mapEditorStore.tool === 'pencil'"> <div class="select" v-if="mapEditor.tool.value === 'pencil'">
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectPencilOpen }"> <div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectPencilOpen }">
{{ mapEditorStore.drawMode.replace('_', ' ') }} {{ mapEditor.drawMode.value.replace('_', ' ') }}
<img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/mapEditor/chevron.svg" alt="" /> <img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/mapEditor/chevron.svg" alt="" />
</div> </div>
<div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectPencilOpen && mapEditorStore.tool === 'pencil'"> <div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectPencilOpen && mapEditor.tool.value === 'pencil'">
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('tile')"> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('tile', 'pencil')">
Tile Tile
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div> <div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span> </span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('map_object')"> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('map_object', 'pencil')">
Map object Map object
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div> <div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span> </span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('teleport')"> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('teleport', 'pencil')">
Teleport Teleport
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div> <div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span> </span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('blocking tile')">Blocking tile</span> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('blocking tile', 'pencil')">Blocking tile</span>
</div> </div>
</div> </div>
</button> </button>
<div class="w-px bg-cyan"></div> <div class="w-px bg-cyan"></div>
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditorStore.tool === 'eraser' }" @click="handleClick('eraser')"> <button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditor.tool.value === 'eraser' }" @click="handleClick('eraser')">
<img class="invert w-5 h-5" src="/assets/icons/mapEditor/eraser.svg" alt="Eraser" /> <span class="h-5" :class="{ 'ml-2.5': mapEditorStore.tool !== 'eraser' }">(E)</span> <img class="invert w-5 h-5" src="/assets/icons/mapEditor/eraser.svg" alt="Eraser" /> <span class="h-5" :class="{ 'ml-2.5': mapEditor.tool.value !== 'eraser' }">(E)</span>
<div class="select" v-if="mapEditorStore.tool === 'eraser'"> <div class="select" v-if="mapEditor.tool.value === 'eraser'">
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectEraserOpen }"> <div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectEraserOpen }">
{{ mapEditorStore.eraserMode.replace('_', ' ') }} {{ mapEditor.drawMode.value.replace('_', ' ') }}
<img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/mapEditor/chevron.svg" /> <img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/mapEditor/chevron.svg" alt="" />
</div> </div>
<div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectEraserOpen"> <div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectEraserOpen">
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('tile')"> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('tile', 'eraser')">
Tile Tile
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div> <div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span> </span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('map_object')"> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('map_object', 'eraser')">
Map object Map object
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div> <div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span> </span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('teleport')"> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('teleport', 'eraser')">
Teleport Teleport
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div> <div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span> </span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('blocking tile')">Blocking tile</span> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('blocking tile', 'eraser')">Blocking tile</span>
</div> </div>
</div> </div>
</button> </button>
<div class="w-px bg-cyan"></div> <div class="w-px bg-cyan"></div>
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditorStore.tool === 'paint' }" @click="handleClick('paint')"> <button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditor.tool.value === 'paint' }" @click="handleClick('paint')">
<img class="invert w-5 h-5" src="/assets/icons/mapEditor/paint.svg" alt="Paint bucket" /> <span class="h-5" :class="{ 'ml-2.5': mapEditorStore.tool !== 'paint' }">(B)</span> <img class="invert w-5 h-5" src="/assets/icons/mapEditor/paint.svg" alt="Paint bucket" /> <span class="h-5" :class="{ 'ml-2.5': mapEditor.tool.value !== 'paint' }">(B)</span>
</button> </button>
<div class="w-px bg-cyan"></div> <div class="w-px bg-cyan"></div>
<button class="flex justify-center items-center min-w-10 p-0 relative" @click="handleClick('settings')"><img class="invert w-5 h-5" src="/assets/icons/mapEditor/gear.svg" alt="Map settings" /> <span class="h-5 ml-2.5">(Z)</span></button> <button class="flex justify-center items-center min-w-10 p-0 relative" @click="handleClick('settings')"><img class="invert w-5 h-5" src="/assets/icons/mapEditor/gear.svg" alt="Map settings" /> <span class="h-5 ml-2.5">(Z)</span></button>
<div class="w-px bg-cyan"></div>
<label class="my-auto gap-0" for="checkbox">Continuous Drawing</label>
<input type="checkbox" />
</div> </div>
<div class="toolbar fixed bottom-0 right-0 m-3 rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10 space-x-2"> <div class="toolbar fixed bottom-0 right-0 m-3 rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10 space-x-2">
<button class="btn-cyan px-3.5" @click="() => mapEditorStore.toggleMapListModal()">Load</button> <button class="btn-cyan px-3.5" @click="() => emit('open-maps')">Load</button>
<button class="btn-cyan px-3.5" @click="() => emit('save')" v-if="mapEditor.currentMap.value">Save</button> <button class="btn-cyan px-3.5" @click="() => emit('save')" v-if="mapEditor.currentMap.value">Save</button>
<button class="btn-cyan px-3.5" @click="() => emit('clear')" v-if="mapEditor.currentMap.value">Clear</button> <button class="btn-cyan px-3.5" @click="() => emit('clear')" v-if="mapEditor.currentMap.value">Clear</button>
<button class="btn-cyan px-3.5" @click="() => mapEditorStore.toggleActive()">Exit</button> <button class="btn-cyan px-3.5" @click="() => emit('close-editor')">Exit</button>
</div> </div>
</div> </div>
</div> </div>
@ -83,14 +88,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { useMapEditorComposable } from '@/composables/useMapEditorComposable' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { onClickOutside } from '@vueuse/core' import { onClickOutside } from '@vueuse/core'
import { onBeforeUnmount, onMounted, ref } from 'vue' import { onBeforeUnmount, onMounted, ref } from 'vue'
const mapEditor = useMapEditorComposable() const mapEditor = useMapEditorComposable()
const mapEditorStore = useMapEditorStore()
const emit = defineEmits(['save', 'clear']) const emit = defineEmits(['save', 'clear', 'open-maps', 'open-settings', 'close-editor', 'open-tile-list', 'open-map-object-list', 'close-lists'])
// track when clicked outside of toolbar items // track when clicked outside of toolbar items
const toolbar = ref(null) const toolbar = ref(null)
@ -99,26 +102,49 @@ const toolbar = ref(null)
let selectPencilOpen = ref(false) let selectPencilOpen = ref(false)
let selectEraserOpen = ref(false) let selectEraserOpen = ref(false)
let tileListShown = ref(false)
let mapObjectListShown = ref(false)
defineExpose({ tileListShown, mapObjectListShown })
// drawMode // drawMode
function setDrawMode(value: string) { function setDrawMode(value: string) {
mapEditorStore.isTileListModalShown = value === 'tile' if (mapEditor.tool.value === 'paint' || mapEditor.tool.value === 'pencil' || mapEditor.tool.value === 'eraser') {
mapEditorStore.isMapObjectListModalShown = value === 'map_object' emit('close-lists')
if (value === 'tile') emit('open-tile-list')
if (value === 'map_object') emit('open-map-object-list')
}
mapEditorStore.setDrawMode(value) mapEditor.setDrawMode(value)
selectPencilOpen.value = false
selectEraserOpen.value = false
}
function setPencilMode() {
mapEditor.setTool('pencil')
selectPencilOpen.value = false selectPencilOpen.value = false
} }
// drawMode // drawMode
function setEraserMode(value: string) { function setEraserMode() {
mapEditorStore.setEraserMode(value) mapEditor.setTool('eraser')
selectEraserOpen.value = false selectEraserOpen.value = false
} }
function handleModeClick(mode: string, type: 'pencil' | 'eraser') {
setDrawMode(mode)
type === 'pencil' ? setPencilMode() : setEraserMode()
}
function handleClick(tool: string) { function handleClick(tool: string) {
if (tool === 'settings') { if (tool === 'settings') {
mapEditorStore.toggleSettingsModal() emit('open-settings')
emit('close-lists')
} else if (tool === 'move') {
emit('close-lists')
mapEditor.setTool(tool)
} else { } else {
mapEditorStore.setTool(tool) mapEditor.setTool(tool)
} }
selectPencilOpen.value = tool === 'pencil' ? !selectPencilOpen.value : false selectPencilOpen.value = tool === 'pencil' ? !selectPencilOpen.value : false
@ -126,22 +152,17 @@ function handleClick(tool: string) {
} }
function cycleToolMode(tool: 'pencil' | 'eraser') { function cycleToolMode(tool: 'pencil' | 'eraser') {
const modes = ['tile', 'object', 'teleport', 'blocking tile'] const modes = ['tile', 'map_object', 'teleport', 'blocking tile']
const currentMode = tool === 'pencil' ? mapEditorStore.drawMode : mapEditorStore.eraserMode const currentIndex = modes.indexOf(mapEditor.drawMode.value)
const currentIndex = modes.indexOf(currentMode)
const nextIndex = (currentIndex + 1) % modes.length const nextIndex = (currentIndex + 1) % modes.length
const nextMode = modes[nextIndex] const nextMode = modes[nextIndex]
if (tool === 'pencil') { setDrawMode(nextMode)
setDrawMode(nextMode)
} else {
setEraserMode(nextMode)
}
} }
function initKeyShortcuts(event: KeyboardEvent) { function initKeyShortcuts(event: KeyboardEvent) {
// Check if map is set // Check if map is set
if (!mapEditorStore.map) return if (!mapEditor.currentMap.value) return
// prevent if focused on composables // prevent if focused on composables
if (document.activeElement?.tagName === 'INPUT') return if (document.activeElement?.tagName === 'INPUT') return
@ -156,7 +177,7 @@ function initKeyShortcuts(event: KeyboardEvent) {
if (keyActions.hasOwnProperty(event.key)) { if (keyActions.hasOwnProperty(event.key)) {
const tool = keyActions[event.key] const tool = keyActions[event.key]
if ((tool === 'pencil' || tool === 'eraser') && mapEditorStore.tool === tool) { if ((tool === 'pencil' || tool === 'eraser') && mapEditor.tool.value === tool) {
cycleToolMode(tool) cycleToolMode(tool)
} else { } else {
handleClick(tool) handleClick(tool)

View File

@ -34,7 +34,7 @@
<button class="ml-6 w-4 h-8 p-0"> <button class="ml-6 w-4 h-8 p-0">
<img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 m-auto" alt="Arrow left" /> <img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 m-auto" alt="Arrow left" />
</button> </button>
<img class="w-24 object-contain mb-3.5" alt="Player avatar" :src="config.server_endpoint + '/avatar/s/' + characters.find((c) => c.id === selectedCharacterId)?.characterType + '/' + (selectedHairId ?? 'default')" /> <img class="w-24 object-contain mb-3.5 max-h-[70%]" alt="Player avatar" :src="config.server_endpoint + '/avatar/s/' + characters.find((c) => c.id === selectedCharacterId)?.characterType + '/' + (selectedHairId ?? 'default')" />
<button class="mr-6 w-4 h-8 p-0"> <button class="mr-6 w-4 h-8 p-0">
<img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 -scale-x-100" alt="Arrow right" /> <img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 -scale-x-100" alt="Arrow right" />
</button> </button>
@ -87,7 +87,7 @@
</div> </div>
</div> </div>
<div v-else> <div v-else>
<img class="w-20 invert-80" src="/assets/icons/loading-icon1.svg" /> <img class="w-20 invert-80" src="/assets/icons/loading-icon1.svg" alt="Loading" />
</div> </div>
<div class="w-2/3 button-wrapper flex self-center justify-center lg:justify-end gap-4 max-w-[860px]" v-if="!isLoading"> <div class="w-2/3 button-wrapper flex self-center justify-center lg:justify-end gap-4 max-w-[860px]" v-if="!isLoading">
@ -131,11 +131,11 @@ import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const isLoading = ref<boolean>(true) const isLoading = ref<boolean>(true)
const characters = ref<CharacterT[]>([]) const characters = ref<CharacterT[]>([])
const selectedCharacterId = ref<number | null>(null) const selectedCharacterId = ref<string | null>(null)
const isCreateNewCharacterModalOpen = ref<boolean>(false) const isCreateNewCharacterModalOpen = ref<boolean>(false)
const newCharacterName = ref<string>('') const newCharacterName = ref<string>('')
const characterHairs = ref<CharacterHair[]>([]) const characterHairs = ref<CharacterHair[]>([])
const selectedHairId = ref<number | null>(null) const selectedHairId = ref<string | null>(null)
// Fetch characters // Fetch characters
setTimeout(() => { setTimeout(() => {

View File

@ -11,7 +11,6 @@
<ExpBar /> <ExpBar />
<CharacterProfile /> <CharacterProfile />
<Effects />
</Scene> </Scene>
</Game> </Game>
</div> </div>
@ -27,7 +26,6 @@ import ExpBar from '@/components/game/gui/ExpBar.vue'
import Hotkeys from '@/components/game/gui/Hotkeys.vue' import Hotkeys from '@/components/game/gui/Hotkeys.vue'
import Hud from '@/components/game/gui/Hud.vue' import Hud from '@/components/game/gui/Hud.vue'
import Menu from '@/components/game/gui/Menu.vue' import Menu from '@/components/game/gui/Menu.vue'
import Effects from '@/components/game/map/Effects.vue'
import Map from '@/components/game/map/Map.vue' import Map from '@/components/game/map/Map.vue'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { Game, Scene } from 'phavuer' import { Game, Scene } from 'phavuer'

View File

@ -1,16 +1,6 @@
<template> <template>
<div class="flex flex-col justify-center items-center h-dvh relative col"> <div class="flex flex-col justify-center items-center h-dvh relative col">
<svg width="40" height="40" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <img class="w-20 invert-80" src="/assets/icons/loading-icon1.svg" alt="Loading" />
<circle cx="4" cy="12" r="3" fill="white">
<animate id="spinner_qFRN" begin="0;spinner_OcgL.end+0.25s" attributeName="cy" calcMode="spline" dur="0.6s" values="12;6;12" keySplines=".33,.66,.66,1;.33,0,.66,.33" />
</circle>
<circle cx="12" cy="12" r="3" fill="white">
<animate begin="spinner_qFRN.begin+0.1s" attributeName="cy" calcMode="spline" dur="0.6s" values="12;6;12" keySplines=".33,.66,.66,1;.33,0,.66,.33" />
</circle>
<circle cx="20" cy="12" r="3" fill="white">
<animate id="spinner_OcgL" begin="spinner_qFRN.begin+0.2s" attributeName="cy" calcMode="spline" dur="0.6s" values="12;6;12" keySplines=".33,.66,.66,1;.33,0,.66,.33" />
</circle>
</svg>
</div> </div>
</template> </template>

View File

@ -8,8 +8,8 @@
<div class="h-dvh flex items-center lg:justify-center flex-col px-8 max-lg:pt-20"> <div class="h-dvh flex items-center lg:justify-center flex-col px-8 max-lg:pt-20">
<!-- <img src="/assets/tlogo.png" class="mb-10 w-52" alt="Noxious logo" />--> <!-- <img src="/assets/tlogo.png" class="mb-10 w-52" alt="Noxious logo" />-->
<div class="relative"> <div class="relative">
<img src="/assets/ui-elements/login-ui-box-outer.svg" class="absolute w-full h-full" alt="UI box outer" /> <img src="/assets/ui-elements/login-ui-box-outer.svg" class="absolute w-full h-full" alt="" />
<img src="/assets/ui-elements/login-ui-box-inner.svg" class="absolute left-2 top-2 w-[calc(100%_-_16px)] h-[calc(100%_-_16px)]" alt="UI box inner" /> <img src="/assets/ui-elements/login-ui-box-inner.svg" class="absolute left-2 top-2 w-[calc(100%_-_16px)] h-[calc(100%_-_16px)]" alt="" />
<!-- Login Form --> <!-- Login Form -->
<LoginForm v-if="currentForm === 'login' && !doesUrlHaveToken" @openResetPasswordModal="() => (isPasswordResetFormShown = true)" @switchToRegister="currentForm = 'register'" /> <LoginForm v-if="currentForm === 'login' && !doesUrlHaveToken" @openResetPasswordModal="() => (isPasswordResetFormShown = true)" @switchToRegister="currentForm = 'register'" />

View File

@ -5,12 +5,23 @@
<div v-if="!isLoaded" class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white text-3xl font-ui">Loading...</div> <div v-if="!isLoaded" class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white text-3xl font-ui">Loading...</div>
<div v-else> <div v-else>
<Map :key="mapEditor.currentMap.value?.id" /> <Map :key="mapEditor.currentMap.value?.id" />
<Toolbar @save="save" @clear="clear" /> <Toolbar
<MapList /> ref="toolbar"
<TileList /> @save="save"
<ObjectList /> @clear="clear"
<MapSettings /> @open-maps="mapModal?.open"
<TeleportModal /> @open-settings="mapSettingsModal?.open"
@close-editor="mapEditor.toggleActive"
@close-lists="tileList?.close"
@closeLists="objectList?.close"
@open-tile-list="tileList?.open"
@open-map-object-list="objectList?.open"
/>
<MapList ref="mapModal" @open-create-map="mapSettingsModal?.open" />
<TileList ref="tileList" />
<ObjectList ref="objectList" />
<MapSettings ref="mapSettingsModal" />
<TeleportModal ref="teleportModal" />
</div> </div>
</Scene> </Scene>
</Game> </Game>
@ -32,14 +43,19 @@ import { loadAllTilesIntoScene } from '@/composables/mapComposable'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { MapStorage } from '@/storage/storages' import { MapStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { Game, Scene } from 'phavuer' import { Game, Scene } from 'phavuer'
import { ref, watch } from 'vue' import { ref, useTemplateRef, watch } from 'vue'
const mapStorage = new MapStorage() const mapStorage = new MapStorage()
const mapEditor = useMapEditorComposable() const mapEditor = useMapEditorComposable()
const gameStore = useGameStore() const gameStore = useGameStore()
const mapEditorStore = useMapEditorStore()
const toolbar = useTemplateRef('toolbar')
const mapModal = useTemplateRef('mapModal')
const tileList = useTemplateRef('tileList')
const objectList = useTemplateRef('objectList')
const mapSettingsModal = useTemplateRef('mapSettingsModal')
const teleportModal = useTemplateRef('teleportModal')
const isLoaded = ref(false) const isLoaded = ref(false)
@ -53,7 +69,7 @@ const gameConfig = {
const createGame = (game: Phaser.Game) => { const createGame = (game: Phaser.Game) => {
// Resize the game when the window is resized // Resize the game when the window is resized
addEventListener('resize', () => { window.addEventListener('resize', () => {
game.scale.resize(window.innerWidth, window.innerHeight) game.scale.resize(window.innerWidth, window.innerHeight)
}) })
} }
@ -78,22 +94,19 @@ const preloadScene = async (scene: Phaser.Scene) => {
} }
function save() { function save() {
if (!mapEditor.currentMap.value) return const currentMap = mapEditor.currentMap.value
if (!currentMap) return
const data = { const data = {
mapId: mapEditor.currentMap.value.id, mapId: currentMap.id,
name: mapEditor.currentMap.value.name, name: currentMap.name,
width: mapEditor.currentMap.value.width, width: currentMap.width,
height: mapEditor.currentMap.value.height, height: currentMap.height,
tiles: mapEditor.currentMap.value.tiles, tiles: currentMap.tiles,
pvp: mapEditor.currentMap.value.pvp, pvp: currentMap.pvp,
mapEffects: mapEditor.currentMap.value.mapEffects?.map(({ id, effect, strength }) => ({ id, effect, strength })) ?? [], mapEffects: currentMap.mapEffects?.map(({ id, effect, strength }) => ({ id, effect, strength })) ?? [],
mapEventTiles: mapEditor.currentMap.value.mapEventTiles?.map(({ id, type, positionX, positionY, teleport }) => ({ id, type, positionX, positionY, teleport })) ?? [], mapEventTiles: currentMap.mapEventTiles?.map(({ id, type, positionX, positionY, teleport }) => ({ id, type, positionX, positionY, teleport })) ?? [],
placedMapObjects: mapEditor.currentMap.value.placedMapObjects?.map(({ id, mapObject, depth, isRotated, positionX, positionY }) => ({ id, mapObject, depth, isRotated, positionX, positionY })) ?? [] placedMapObjects: currentMap.placedMapObjects?.map(({ id, mapObject, depth, isRotated, positionX, positionY }) => ({ id, mapObject, depth, isRotated, positionX, positionY })) ?? []
}
if (mapEditorStore.isSettingsModalShown) {
mapEditorStore.toggleSettingsModal()
} }
gameStore.connection?.emit('gm:map:update', data, (response: MapT) => { gameStore.connection?.emit('gm:map:update', data, (response: MapT) => {
@ -106,6 +119,5 @@ function clear() {
// Clear placed objects, event tiles and tiles // Clear placed objects, event tiles and tiles
mapEditor.clearMap() mapEditor.clearMap()
mapEditorStore.triggerClearTiles()
} }
</script> </script>

View File

@ -3,8 +3,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useCameraControls } from '@/composables/useCameraControls' import { useControlsComposable } from '@/composables/useControlsComposable'
import { usePointerHandlers } from '@/composables/usePointerHandlers'
import { Image, useScene } from 'phavuer' import { Image, useScene } from 'phavuer'
import { onBeforeUnmount, ref } from 'vue' import { onBeforeUnmount, ref } from 'vue'
@ -14,45 +13,16 @@ type WayPoint = { visible: boolean; x: number; y: number }
// Props // Props
const props = defineProps<{ layer: Phaser.Tilemaps.TilemapLayer }>() const props = defineProps<{ layer: Phaser.Tilemaps.TilemapLayer }>()
// Constants
const ZOOM_SETTINGS = {
WHEEL_FACTOR: 0.005,
KEY_FACTOR: 0.3,
MIN: 1,
MAX: 3
} as const
// Setup // Setup
const scene = useScene() const scene = useScene()
const waypoint = ref<WayPoint>({ visible: false, x: 0, y: 0 }) const waypoint = ref<WayPoint>({ visible: false, x: 0, y: 0 })
const { camera } = useCameraControls(scene) const { setupControls, cleanupControls } = useControlsComposable(scene, props.layer, waypoint)
const { setupPointerHandlers, cleanupPointerHandlers } = usePointerHandlers(scene, props.layer, waypoint, camera)
// Handlers
function handleScrollZoom(pointer: Phaser.Input.Pointer) {
if (!(pointer.event instanceof WheelEvent && pointer.event.shiftKey)) return
const zoomLevel = Phaser.Math.Clamp(camera.zoom - pointer.event.deltaY * ZOOM_SETTINGS.WHEEL_FACTOR, ZOOM_SETTINGS.MIN, ZOOM_SETTINGS.MAX)
camera.setZoom(zoomLevel)
}
function handleKeyComboZoom(event: { keyCodes: number[] }) {
const deltaY = event.keyCodes[1] === 38 ? 1 : -1 // 38 is Up, 40 is Down
const zoomLevel = Phaser.Math.Clamp(camera.zoom + deltaY * ZOOM_SETTINGS.KEY_FACTOR, ZOOM_SETTINGS.MIN, ZOOM_SETTINGS.MAX)
camera.setZoom(zoomLevel)
}
// Event setup // Event setup
setupPointerHandlers() setupControls()
scene.input.keyboard?.createCombo([16, 38], { resetOnMatch: true }) // Shift + Up
scene.input.keyboard?.createCombo([16, 40], { resetOnMatch: true }) // Shift + Down
scene.input.keyboard?.on('keycombomatch', handleKeyComboZoom)
scene.input.on(Phaser.Input.Events.POINTER_WHEEL, handleScrollZoom)
// Cleanup // Cleanup
onBeforeUnmount(() => { onBeforeUnmount(() => {
cleanupPointerHandlers() cleanupControls()
scene.input.keyboard?.off('keycombomatch')
scene.input.off(Phaser.Input.Events.POINTER_WHEEL, handleScrollZoom)
}) })
</script> </script>

View File

@ -17,7 +17,7 @@
<button v-if="canFullScreen" @click="toggleFullScreen" class="w-5 h-5 m-0 p-0 relative hover:scale-110 transition-transform duration-300 ease-in-out"> <button v-if="canFullScreen" @click="toggleFullScreen" class="w-5 h-5 m-0 p-0 relative hover:scale-110 transition-transform duration-300 ease-in-out">
<img :alt="isFullScreen ? 'exit full-screen' : 'full-screen'" :src="isFullScreen ? '/assets/icons/modal/minimize.svg' : '/assets/icons/modal/increase-size-option.svg'" class="w-3.5 h-3.5 invert" draggable="false" /> <img :alt="isFullScreen ? 'exit full-screen' : 'full-screen'" :src="isFullScreen ? '/assets/icons/modal/minimize.svg' : '/assets/icons/modal/increase-size-option.svg'" class="w-3.5 h-3.5 invert" draggable="false" />
</button> </button>
<button v-if="closable" @click="emit('modal:close')" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out"> <button v-if="closable" @click="closeModal" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
<img alt="close" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" draggable="false" /> <img alt="close" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" draggable="false" />
</button> </button>
</div> </div>
@ -46,7 +46,7 @@
import { computed, onMounted, onUnmounted, ref, watch } from 'vue' import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
interface ModalProps { interface ModalProps {
isModalOpen: boolean isModalOpen?: boolean
closable?: boolean closable?: boolean
isResizable?: boolean isResizable?: boolean
isFullScreen?: boolean isFullScreen?: boolean
@ -79,10 +79,16 @@ const props = withDefaults(defineProps<ModalProps>(), {
}) })
const emit = defineEmits<{ const emit = defineEmits<{
'modal:open': []
'modal:close': [] 'modal:close': []
'character:create': []
}>() }>()
defineExpose({
open: () => (isModalOpenRef.value = true),
close: () => (isModalOpenRef.value = false),
toggle: () => (isModalOpenRef.value = !isModalOpenRef.value)
})
const isModalOpenRef = ref(props.isModalOpen) const isModalOpenRef = ref(props.isModalOpen)
const width = ref(props.modalWidth) const width = ref(props.modalWidth)
const height = ref(props.modalHeight) const height = ref(props.modalHeight)
@ -150,6 +156,11 @@ function drag(event: MouseEvent) {
y.value = dragState.initialY + (event.clientY - dragState.startY) y.value = dragState.initialY + (event.clientY - dragState.startY)
} }
function closeModal() {
isModalOpenRef.value = false
emit('modal:close')
}
function toggleFullScreen() { function toggleFullScreen() {
if (isFullScreen.value) { if (isFullScreen.value) {
Object.assign({ x, y, width, height }, preFullScreenState) Object.assign({ x, y, width, height }, preFullScreenState)

View File

@ -0,0 +1,69 @@
import config from '@/application/config'
import { getTile, tileToWorldXY } from '@/composables/mapComposable'
import { useGameStore } from '@/stores/gameStore'
import { ref, type Ref } from 'vue'
export function useBaseControlsComposable(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
const gameStore = useGameStore()
const pointerStartPosition = ref({ x: 0, y: 0 })
const dragThreshold = 5 // pixels
function updateWaypoint(worldX: number, worldY: number) {
const pointerTile = getTile(layer, worldX, worldY)
if (!pointerTile) {
waypoint.value.visible = false
return
}
const worldPoint = tileToWorldXY(layer, pointerTile.x, pointerTile.y)
if (!worldPoint.worldPositionX || !worldPoint.worldPositionX) return
waypoint.value = {
visible: true,
x: worldPoint.worldPositionX,
y: worldPoint.worldPositionY + config.tile_size.height + 15
}
}
function handleDragMap(pointer: Phaser.Input.Pointer) {
if (!gameStore.game.isPlayerDraggingCamera) return
const deltaX = pointer.x - pointerStartPosition.value.x
const deltaY = pointer.y - pointerStartPosition.value.y
if (Math.abs(deltaX) <= dragThreshold && Math.abs(deltaY) <= dragThreshold) return
const scrollX = camera.scrollX - deltaX / camera.zoom
const scrollY = camera.scrollY - deltaY / camera.zoom
camera.setScroll(scrollX, scrollY)
pointerStartPosition.value = { x: pointer.x, y: pointer.y }
}
function startDragging(pointer: Phaser.Input.Pointer) {
pointerStartPosition.value = { x: pointer.x, y: pointer.y }
gameStore.setPlayerDraggingCamera(true)
}
function stopDragging() {
gameStore.setPlayerDraggingCamera(false)
}
function handleZoom(pointer: Phaser.Input.Pointer) {
if (pointer.event instanceof WheelEvent && pointer.event.shiftKey) {
const deltaY = pointer.event.deltaY
const zoomLevel = camera.zoom - deltaY * 0.005
if (zoomLevel > 0 && zoomLevel < 3) {
camera.setZoom(zoomLevel)
}
}
}
return {
updateWaypoint,
handleDragMap,
startDragging,
stopDragging,
handleZoom,
pointerStartPosition
}
}

View File

@ -0,0 +1,114 @@
import { getTile } from '@/composables/mapComposable'
import { useGameStore } from '@/stores/gameStore'
import type { Ref } from 'vue'
import { useBaseControlsComposable } from './useBaseControlsComposable'
export function useGameControlsComposable(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
const gameStore = useGameStore()
const baseHandlers = useBaseControlsComposable(scene, layer, waypoint, camera)
function handlePointerDown(pointer: Phaser.Input.Pointer) {
baseHandlers.startDragging(pointer)
}
function handlePointerMove(pointer: Phaser.Input.Pointer) {
baseHandlers.updateWaypoint(pointer.worldX, pointer.worldY)
baseHandlers.handleDragMap(pointer)
}
function handlePointerUp(pointer: Phaser.Input.Pointer) {
baseHandlers.stopDragging()
const pointerTile = getTile(layer, pointer.worldX, pointer.worldY)
if (!pointerTile) return
gameStore.connection?.emit('map:character:move', {
positionX: pointerTile.x,
positionY: pointerTile.y
})
}
const pressedKeys = new Set<string>()
let moveInterval: number | null = null
function handleKeyDown(event: KeyboardEvent) {
if (!gameStore.character) return
// console.log(event.key)
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) {
pressedKeys.add(event.key)
// Start movement loop if not already running
if (!moveInterval) {
moveInterval = window.setInterval(moveCharacter, 250) // Adjust timing as needed
moveCharacter() // Move immediately on first press
}
}
// Attack on CTRL
if (event.key === 'Control') {
gameStore.connection?.emit('map:character:attack')
}
}
function handleKeyUp(event: KeyboardEvent) {
pressedKeys.delete(event.key)
// If no movement keys are pressed, clear the interval
if (pressedKeys.size === 0 && moveInterval) {
clearInterval(moveInterval)
moveInterval = null
}
}
function moveCharacter() {
if (!gameStore.character) return
const { positionX, positionY } = gameStore.character
if (pressedKeys.has('ArrowLeft')) {
gameStore.connection?.emit('map:character:move', {
positionX: positionX - 1,
positionY: positionY
})
}
if (pressedKeys.has('ArrowRight')) {
gameStore.connection?.emit('map:character:move', {
positionX: positionX + 1,
positionY: positionY
})
}
if (pressedKeys.has('ArrowUp')) {
gameStore.connection?.emit('map:character:move', {
positionX: positionX,
positionY: positionY - 1
})
}
if (pressedKeys.has('ArrowDown')) {
gameStore.connection?.emit('map:character:move', {
positionX: positionX,
positionY: positionY + 1
})
}
}
const setupControls = () => {
scene.input.on(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
scene.input.on(Phaser.Input.Events.POINTER_WHEEL, baseHandlers.handleZoom)
scene.input.keyboard!.on('keydown', handleKeyDown)
scene.input.keyboard!.on('keyup', handleKeyUp)
}
const cleanupControls = () => {
scene.input.off(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
scene.input.off(Phaser.Input.Events.POINTER_UP, handlePointerUp)
scene.input.off(Phaser.Input.Events.POINTER_WHEEL, baseHandlers.handleZoom)
scene.input.keyboard!.off('keydown', handleKeyDown)
scene.input.keyboard!.off('keyup', handleKeyUp)
}
return { setupControls, cleanupControls }
}

View File

@ -0,0 +1,42 @@
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { computed, type Ref } from 'vue'
import { useBaseControlsComposable } from './useBaseControlsComposable'
export function useMapEditorControlsComposable(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
const mapEditor = useMapEditorComposable()
const baseHandlers = useBaseControlsComposable(scene, layer, waypoint, camera)
const isMoveTool = computed(() => mapEditor.tool.value === 'move')
function handlePointerDown(pointer: Phaser.Input.Pointer) {
if (isMoveTool.value || pointer.event.shiftKey) {
baseHandlers.startDragging(pointer)
}
}
function handlePointerMove(pointer: Phaser.Input.Pointer) {
if (isMoveTool.value || pointer.event.shiftKey) {
baseHandlers.handleDragMap(pointer)
}
baseHandlers.updateWaypoint(pointer.worldX, pointer.worldY)
}
function handlePointerUp(pointer: Phaser.Input.Pointer) {
baseHandlers.stopDragging()
}
const setupControls = () => {
scene.input.on(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
scene.input.on(Phaser.Input.Events.POINTER_WHEEL, baseHandlers.handleZoom)
}
const cleanupControls = () => {
scene.input.off(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
scene.input.off(Phaser.Input.Events.POINTER_UP, handlePointerUp)
scene.input.off(Phaser.Input.Events.POINTER_WHEEL, baseHandlers.handleZoom)
}
return { setupControls, cleanupControls }
}

View File

@ -59,7 +59,6 @@ export async function loadTexture(scene: Phaser.Scene, textureData: TextureData)
export async function loadSpriteTextures(scene: Phaser.Scene, sprite_id: string) { export async function loadSpriteTextures(scene: Phaser.Scene, sprite_id: string) {
if (!sprite_id) return false if (!sprite_id) return false
// @TODO: Fix this
const spriteStorage = new SpriteStorage() const spriteStorage = new SpriteStorage()
const sprite = await spriteStorage.get(sprite_id) const sprite = await spriteStorage.get(sprite_id)
@ -73,18 +72,17 @@ export async function loadSpriteTextures(scene: Phaser.Scene, sprite_id: string)
await loadTexture(scene, { await loadTexture(scene, {
key, key,
data: '/textures/sprites/' + sprite.id + '/' + sprite_action.action + '.png', data: '/textures/sprites/' + sprite.id + '/' + sprite_action.action + '.png',
group: sprite_action.isAnimated ? 'sprite_animations' : 'sprites', group: sprite_action.frameCount > 1 ? 'sprite_animations' : 'sprites',
updatedAt: sprite_action.updatedAt, updatedAt: sprite_action.updatedAt,
originX: sprite_action.originX, originX: sprite_action.originX,
originY: sprite_action.originY, originY: sprite_action.originY,
isAnimated: sprite_action.isAnimated,
frameWidth: sprite_action.frameWidth, frameWidth: sprite_action.frameWidth,
frameHeight: sprite_action.frameHeight, frameHeight: sprite_action.frameHeight,
frameRate: sprite_action.frameRate frameRate: sprite_action.frameRate
} as TextureData) } as TextureData)
// If the sprite is not animated, skip // If the sprite has no more than one frame, skip
if (!sprite_action.isAnimated) continue if (sprite_action.frameCount <= 1) continue
// Check if animation already exists // Check if animation already exists
if (scene.anims.get(key)) continue if (scene.anims.get(key)) continue

View File

@ -1,5 +1,5 @@
import config from '@/application/config' import config from '@/application/config'
import type { HttpResponse, TextureData, UUID } from '@/application/types' import type { HttpResponse, TextureData, Tile as TileT, UUID } from '@/application/types'
import { unduplicateArray } from '@/application/utilities' import { unduplicateArray } from '@/application/utilities'
import { loadTexture } from '@/composables/gameComposable' import { loadTexture } from '@/composables/gameComposable'
import { MapStorage, TileStorage } from '@/storage/storages' import { MapStorage, TileStorage } from '@/storage/storages'
@ -10,9 +10,7 @@ import Tileset = Phaser.Tilemaps.Tileset
import Tile = Phaser.Tilemaps.Tile import Tile = Phaser.Tilemaps.Tile
export function getTile(layer: TilemapLayer | Tilemap, positionX: number, positionY: number): Tile | null { export function getTile(layer: TilemapLayer | Tilemap, positionX: number, positionY: number): Tile | null {
const tile = layer?.getTileAtWorldXY(positionX, positionY) return layer.getTileAtWorldXY(positionX, positionY)
if (!tile) return null
return tile
} }
export function tileToWorldXY(layer: TilemapLayer | Tilemap, positionX: number, positionY: number) { export function tileToWorldXY(layer: TilemapLayer | Tilemap, positionX: number, positionY: number) {
@ -77,14 +75,18 @@ export const calculateIsometricDepth = (positionX: number, positionY: number, wi
return baseDepth + (width + height) / (2 * config.tile_size.width) return baseDepth + (width + height) / (2 * config.tile_size.width)
} }
export function FlattenMapArray(tiles: string[][]) { async function getTiles(tiles: TileT[], scene: Phaser.Scene) {
const normalArray = [] // Load each tile into the scene
for (const tile of tiles) {
for (const row of tiles) { if (!tile) continue
normalArray.push(...row) const textureData = {
key: tile.id,
data: '/textures/tiles/' + tile.id + '.png',
group: 'tiles',
updatedAt: tile.updatedAt
} as TextureData
await loadTexture(scene, textureData)
} }
return normalArray
} }
export async function loadMapTilesIntoScene(map_id: UUID, scene: Phaser.Scene) { export async function loadMapTilesIntoScene(map_id: UUID, scene: Phaser.Scene) {
@ -93,50 +95,22 @@ export async function loadMapTilesIntoScene(map_id: UUID, scene: Phaser.Scene) {
const map = await mapStorage.get(map_id) const map = await mapStorage.get(map_id)
if (!map) return if (!map) return
const tileArray = unduplicateArray(FlattenMapArray(map.tiles)) const tileArray = unduplicateArray(map.tiles)
const tiles = await tileStorage.getByIds(tileArray) const tiles = await tileStorage.getByIds(tileArray)
// Load each tile into the scene await getTiles(tiles, scene)
for (const tile of tiles) {
const textureData = {
key: tile.id,
data: '/textures/tiles/' + tile.id + '.png',
group: 'tiles',
updatedAt: tile.updatedAt
} as TextureData
await loadTexture(scene, textureData)
}
} }
export async function loadTilesIntoScene(tileIds: string[], scene: Phaser.Scene) { export async function loadTilesIntoScene(tileIds: string[], scene: Phaser.Scene) {
const tileStorage = new TileStorage() const tileStorage = new TileStorage()
const tiles = await tileStorage.getByIds(tileIds) const tiles = await tileStorage.getByIds(tileIds)
// Load each tile into the scene await getTiles(tiles, scene)
for (const tile of tiles) {
const textureData = {
key: tile.id,
data: '/textures/tiles/' + tile.id + '.png',
group: 'tiles',
updatedAt: tile.updatedAt
} as TextureData
await loadTexture(scene, textureData)
}
} }
export async function loadAllTilesIntoScene(scene: Phaser.Scene) { export async function loadAllTilesIntoScene(scene: Phaser.Scene) {
const tileStorage = new TileStorage() const tileStorage = new TileStorage()
const tiles = await tileStorage.getAll() const tiles = await tileStorage.getAll()
// Load each tile into the scene await getTiles(tiles, scene)
for (const tile of tiles) {
const textureData = {
key: tile.id,
data: '/textures/tiles/' + tile.id + '.png',
group: 'tiles',
updatedAt: tile.updatedAt
} as TextureData
await loadTexture(scene, textureData)
}
} }

View File

@ -1,77 +0,0 @@
import config from '@/application/config'
import { getTile, tileToWorldXY } from '@/composables/mapComposable'
import { useGameStore } from '@/stores/gameStore'
import { ref, type Ref } from 'vue'
export function useGamePointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
const gameStore = useGameStore()
const pointerStartPosition = ref({ x: 0, y: 0 })
const dragThreshold = 5 // pixels
function updateWaypoint(worldX: number, worldY: number) {
const pointerTile = getTile(layer, worldX, worldY)
if (!pointerTile) {
waypoint.value.visible = false
return
}
const worldPoint = tileToWorldXY(layer, pointerTile.x, pointerTile.y)
if (!worldPoint.worldPositionX || !worldPoint.worldPositionX) return
waypoint.value = {
visible: true,
x: worldPoint.worldPositionX,
y: worldPoint.worldPositionY + config.tile_size.height + 15
}
}
function handlePointerDown(pointer: Phaser.Input.Pointer) {
pointerStartPosition.value = { x: pointer.x, y: pointer.y }
gameStore.setPlayerDraggingCamera(true)
}
function handlePointerMove(pointer: Phaser.Input.Pointer) {
updateWaypoint(pointer.worldX, pointer.worldY)
if (!gameStore.game.isPlayerDraggingCamera) return
const distance = Phaser.Math.Distance.Between(pointerStartPosition.value.x, pointerStartPosition.value.y, pointer.x, pointer.y)
// If the distance is less than the drag threshold, return
// We do this to prevent the camera from scrolling too quickly
if (distance <= dragThreshold) return
camera.setScroll(camera.scrollX - (pointer.x - pointer.prevPosition.x) / camera.zoom, camera.scrollY - (pointer.y - pointer.prevPosition.y) / camera.zoom)
}
function handlePointerUp(pointer: Phaser.Input.Pointer) {
gameStore.setPlayerDraggingCamera(false)
const distance = Phaser.Math.Distance.Between(pointerStartPosition.value.x, pointerStartPosition.value.y, pointer.x, pointer.y)
// If the distance is greater than the drag threshold, return
// We do this to prevent the camera from scrolling too quickly
if (distance > dragThreshold) return
const pointerTile = getTile(layer, pointer.worldX, pointer.worldY)
if (!pointerTile) return
gameStore.connection?.emit('map:character:move', {
positionX: pointerTile.x,
positionY: pointerTile.y
})
}
const setupPointerHandlers = () => {
scene.input.on(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
}
const cleanupPointerHandlers = () => {
scene.input.off(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
scene.input.off(Phaser.Input.Events.POINTER_UP, handlePointerUp)
}
return { setupPointerHandlers, cleanupPointerHandlers }
}

View File

@ -1,83 +0,0 @@
import config from '@/application/config'
import { getTile, tileToWorldXY } from '@/composables/mapComposable'
import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { computed, ref, type Ref } from 'vue'
export function useMapEditorPointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
const gameStore = useGameStore()
const mapEditorStore = useMapEditorStore()
const isMoveTool = computed(() => mapEditorStore.tool === 'move')
const pointerStartPosition = ref({ x: 0, y: 0 })
const dragThreshold = 5 // pixels
function updateWaypoint(worldX: number, worldY: number) {
const pointerTile = getTile(layer, worldX, worldY)
if (!pointerTile) {
waypoint.value.visible = false
return
}
const worldPoint = tileToWorldXY(layer, pointerTile.x, pointerTile.y)
if (!worldPoint.worldPositionX || !worldPoint.worldPositionX) return
waypoint.value = {
visible: true,
x: worldPoint.worldPositionX,
y: worldPoint.worldPositionY + config.tile_size.height + 15
}
}
function handlePointerDown(pointer: Phaser.Input.Pointer) {
pointerStartPosition.value = { x: pointer.x, y: pointer.y }
if (isMoveTool.value || pointer.event.shiftKey) {
gameStore.setPlayerDraggingCamera(true)
}
}
function dragMap(pointer: Phaser.Input.Pointer) {
if (!gameStore.game.isPlayerDraggingCamera) return
const distance = Phaser.Math.Distance.Between(pointerStartPosition.value.x, pointerStartPosition.value.y, pointer.x, pointer.y)
if (distance <= dragThreshold) return
camera.setScroll(camera.scrollX - (pointer.x - pointer.prevPosition.x) / camera.zoom, camera.scrollY - (pointer.y - pointer.prevPosition.y) / camera.zoom)
}
function handlePointerMove(pointer: Phaser.Input.Pointer) {
if (isMoveTool.value || pointer.event.shiftKey) {
dragMap(pointer)
}
updateWaypoint(pointer.worldX, pointer.worldY)
}
function handlePointerUp(pointer: Phaser.Input.Pointer) {
gameStore.setPlayerDraggingCamera(false)
}
function handleZoom(pointer: Phaser.Input.Pointer) {
if (pointer.event instanceof WheelEvent && pointer.event.shiftKey) {
const deltaY = pointer.event.deltaY
const zoomLevel = camera.zoom - deltaY * 0.005
if (zoomLevel > 0 && zoomLevel < 3) {
camera.setZoom(zoomLevel)
}
}
}
const setupPointerHandlers = () => {
scene.input.on(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
scene.input.on(Phaser.Input.Events.POINTER_WHEEL, handleZoom)
}
const cleanupPointerHandlers = () => {
scene.input.off(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
scene.input.off(Phaser.Input.Events.POINTER_UP, handlePointerUp)
scene.input.off(Phaser.Input.Events.POINTER_WHEEL, handleZoom)
}
return { setupPointerHandlers, cleanupPointerHandlers }
}

View File

@ -1,15 +0,0 @@
import { useGameStore } from '@/stores/gameStore'
import { useMapStore } from '@/stores/mapStore'
export function useCameraControls(scene: Phaser.Scene) {
const gameStore = useGameStore()
const camera = scene.cameras.main
const onPointerDown = () => gameStore.setPlayerDraggingCamera(true)
const onPointerUp = () => gameStore.setPlayerDraggingCamera(false)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, onPointerDown)
scene.input.on(Phaser.Input.Events.POINTER_UP, onPointerUp)
return { camera }
}

View File

@ -0,0 +1,136 @@
import { Direction } from '@/application/enums'
import { type MapCharacter } from '@/application/types'
import { loadSpriteTextures } from '@/composables/gameComposable'
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/mapComposable'
import { CharacterTypeStorage } from '@/storage/storages'
import { refObj } from 'phavuer'
import { computed, ref } from 'vue'
export function useCharacterSprite(scene: Phaser.Scene, tilemap: Phaser.Tilemaps.Tilemap, mapCharacter: MapCharacter) {
const characterContainer = refObj<Phaser.GameObjects.Container>()
const characterSpriteId = ref('')
const characterSprite = refObj<Phaser.GameObjects.Sprite>()
const currentPositionX = ref(0)
const currentPositionY = ref(0)
const isometricDepth = ref(1)
const isInitialPosition = ref(true)
const tween = ref<Phaser.Tweens.Tween | null>(null)
const updateIsometricDepth = (positionX: number, positionY: number) => {
isometricDepth.value = calculateIsometricDepth(positionX, positionY, 28, 94, true)
}
const updatePosition = (positionX: number, positionY: number, direction: Direction) => {
const newPositionX = tileToWorldX(tilemap, positionX, positionY)
const newPositionY = tileToWorldY(tilemap, positionX, positionY)
if (isInitialPosition.value) {
currentPositionX.value = newPositionX
currentPositionY.value = newPositionY
isInitialPosition.value = false
return
}
if (tween.value?.isPlaying()) {
tween.value.stop()
}
const distance = Math.sqrt(Math.pow(newPositionX - currentPositionX.value, 2) + Math.pow(newPositionY - currentPositionY.value, 2))
const baseSpeed = 150 // pixels per second
const duration = (distance / baseSpeed) * 1000 // Convert to milliseconds
tween.value = tilemap.scene.tweens.add({
targets: characterContainer.value,
x: newPositionX,
y: newPositionY,
duration,
ease: 'Linear',
onStart: () => {
if (direction === Direction.POSITIVE) {
updateIsometricDepth(positionX, positionY)
}
},
onUpdate: () => {
currentPositionX.value = characterContainer.value?.x ?? currentPositionX.value
currentPositionY.value = characterContainer.value?.y ?? currentPositionY.value
},
onComplete: () => {
if (direction === Direction.NEGATIVE) {
updateIsometricDepth(positionX, positionY)
}
}
})
}
const calcDirection = (oldPositionX: number, oldPositionY: number, newPositionX: number, newPositionY: number): Direction => {
if (newPositionY < oldPositionY || newPositionX < oldPositionX) return Direction.NEGATIVE
if (newPositionX > oldPositionX || newPositionY > oldPositionY) return Direction.POSITIVE
return Direction.UNCHANGED
}
const isFlippedX = computed(() => [6, 4].includes(mapCharacter.character.rotation ?? 0))
const currentDirection = computed(() => {
return [0, 6].includes(mapCharacter.character.rotation ?? 0) ? 'left_up' : 'right_down'
})
const currentAction = computed(() => {
return mapCharacter.isMoving ? 'walk' : 'idle'
})
const charTexture = computed(() => {
const spriteId = characterSpriteId.value ?? 'idle_right_down'
return `${spriteId}-${currentAction.value}_${currentDirection.value}`
})
const updateSprite = () => {
if (!characterSprite.value) return
if (mapCharacter.isMoving) {
characterSprite.value.anims.play(charTexture.value, true)
} else {
characterSprite.value.anims.stop()
characterSprite.value.setFrame(0)
characterSprite.value.setTexture(charTexture.value)
}
}
const initializeSprite = async () => {
const characterTypeStorage = new CharacterTypeStorage()
const spriteId = await characterTypeStorage.getSpriteId(mapCharacter.character.characterType!)
if (!spriteId) return
characterSpriteId.value = spriteId
await loadSpriteTextures(scene, spriteId)
if (characterContainer.value) {
characterContainer.value.setName(mapCharacter.character.name)
}
if (characterSprite.value) {
characterSprite.value.setTexture(charTexture.value)
characterSprite.value.setFlipX(isFlippedX.value)
}
updatePosition(mapCharacter.character.positionX, mapCharacter.character.positionY, mapCharacter.character.rotation)
}
const cleanup = () => {
tween.value?.stop()
}
return {
characterContainer,
characterSpriteId,
characterSprite,
currentPositionX,
currentPositionY,
isometricDepth,
isFlippedX,
updatePosition,
calcDirection,
updateSprite,
initializeSprite,
cleanup
}
}

View File

@ -0,0 +1,18 @@
import { useGameControlsComposable } from '@/composables/controls/useGameControlsComposable'
import { useMapEditorControlsComposable } from '@/composables/controls/useMapEditorControlsComposable'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { computed, type Ref } from 'vue'
export function useControlsComposable(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>) {
const camera = scene.cameras.main
const gameHandlers = useGameControlsComposable(scene, layer, waypoint, camera)
const mapEditorHandlers = useMapEditorControlsComposable(scene, layer, waypoint, camera)
const mapEditor = useMapEditorComposable()
const currentHandlers = computed(() => (mapEditor.active.value ? mapEditorHandlers : gameHandlers))
const setupControls = () => currentHandlers.value.setupControls()
const cleanupControls = () => currentHandlers.value.cleanupControls()
return { setupControls, cleanupControls, camera }
}

View File

@ -1,7 +1,26 @@
import type { Map } from '@/application/types' import type { Map, MapObject } from '@/application/types'
import { ref } from 'vue' import { ref } from 'vue'
export type TeleportSettings = {
toMapId: string
toPositionX: number
toPositionY: number
toRotation: number
}
const currentMap = ref<Map | null>(null) const currentMap = ref<Map | null>(null)
const active = ref(false)
const tool = ref('move')
const drawMode = ref('tile')
const selectedTile = ref('')
const selectedMapObject = ref<MapObject | null>(null)
const shouldClearTiles = ref(false)
const teleportSettings = ref<TeleportSettings>({
toMapId: '',
toPositionX: 0,
toPositionY: 0,
toRotation: 0
})
export function useMapEditorComposable() { export function useMapEditorComposable() {
const loadMap = (map: Map) => { const loadMap = (map: Map) => {
@ -16,16 +35,75 @@ export function useMapEditorComposable() {
const clearMap = () => { const clearMap = () => {
if (!currentMap.value) return if (!currentMap.value) return
currentMap.value.placedMapObjects = [] currentMap.value.placedMapObjects = []
currentMap.value.mapEventTiles = [] currentMap.value.mapEventTiles = []
currentMap.value.tiles = [] currentMap.value.tiles = []
} }
const toggleActive = () => {
if (active.value) reset()
active.value = !active.value
}
const setTool = (newTool: string) => {
tool.value = newTool
}
const setDrawMode = (mode: string) => {
drawMode.value = mode
}
const setSelectedTile = (tile: string) => {
selectedTile.value = tile
}
const setSelectedMapObject = (object: MapObject) => {
selectedMapObject.value = object
}
const setTeleportSettings = (settings: TeleportSettings) => {
teleportSettings.value = settings
}
const triggerClearTiles = () => {
shouldClearTiles.value = true
}
const resetClearTilesFlag = () => {
shouldClearTiles.value = false
}
const reset = () => {
tool.value = 'move'
drawMode.value = 'tile'
selectedTile.value = ''
selectedMapObject.value = null
shouldClearTiles.value = false
}
return { return {
// State
currentMap, currentMap,
active,
tool,
drawMode,
selectedTile,
selectedMapObject,
shouldClearTiles,
teleportSettings,
// Methods
loadMap, loadMap,
updateProperty, updateProperty,
clearMap clearMap,
toggleActive,
setTool,
setDrawMode,
setSelectedTile,
setSelectedMapObject,
setTeleportSettings,
triggerClearTiles,
resetClearTilesFlag,
reset
} }
} }

View File

@ -1,25 +0,0 @@
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { computed, watch, type Ref } from 'vue'
import { useGamePointerHandlers } from './pointerHandlers/useGamePointerHandlers'
import { useMapEditorPointerHandlers } from './pointerHandlers/useMapEditorPointerHandlers'
export function usePointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
const mapEditorStore = useMapEditorStore()
const gameHandlers = useGamePointerHandlers(scene, layer, waypoint, camera)
const mapEditorHandlers = useMapEditorPointerHandlers(scene, layer, waypoint, camera)
const currentHandlers = computed(() => (mapEditorStore.active ? mapEditorHandlers : gameHandlers))
const setupPointerHandlers = () => currentHandlers.value.setupPointerHandlers()
const cleanupPointerHandlers = () => currentHandlers.value.cleanupPointerHandlers()
watch(
() => mapEditorStore.active,
() => {
cleanupPointerHandlers()
setupPointerHandlers()
}
)
return { setupPointerHandlers, cleanupPointerHandlers }
}

View File

@ -0,0 +1,98 @@
import config from '@/application/config'
import type { Tile } from '@/application/types'
import type { TileAnalysisResult, TileWorkerMessage } from '@/types/tileTypes'
import { ref } from 'vue'
// Constants for image processing
const DOWNSCALE_WIDTH = 32
const DOWNSCALE_HEIGHT = 16
const COLOR_SIMILARITY_THRESHOLD = 30
const EDGE_SIMILARITY_THRESHOLD = 20
const BATCH_SIZE = 4
export function useTileProcessingComposable() {
const tileAnalysisCache = ref<Map<string, { color: { r: number; g: number; b: number }; edge: number; namePrefix: string }>>(new Map())
const processingQueue = ref<Tile[]>([])
let isProcessing = false
const worker = new Worker(new URL('@/workers/tileAnalyzerWorker.ts', import.meta.url), { type: 'module' })
worker.onmessage = (e: MessageEvent<TileAnalysisResult>) => {
const { tileId, color, edge, namePrefix } = e.data
tileAnalysisCache.value.set(tileId, { color, edge, namePrefix })
isProcessing = false
processBatch()
}
async function processTileAsync(tile: Tile): Promise<void> {
if (tileAnalysisCache.value.has(tile.id)) return
return new Promise((resolve) => {
const img = new Image()
img.crossOrigin = 'Anonymous'
img.onload = () => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
if (!ctx) {
resolve()
return
}
canvas.width = DOWNSCALE_WIDTH
canvas.height = DOWNSCALE_HEIGHT
ctx.drawImage(img, 0, 0, DOWNSCALE_WIDTH, DOWNSCALE_HEIGHT)
const imageData = ctx.getImageData(0, 0, DOWNSCALE_WIDTH, DOWNSCALE_HEIGHT)
const message: TileWorkerMessage = {
imageData,
tileId: tile.id,
tileName: tile.name
}
worker.postMessage(message)
resolve()
}
img.onerror = () => resolve()
img.src = `${config.server_endpoint}/textures/tiles/${tile.id}.png`
})
}
function processBatch() {
if (isProcessing || processingQueue.value.length === 0) return
isProcessing = true
const batch = processingQueue.value.splice(0, BATCH_SIZE)
Promise.all(batch.map((tile) => processTileAsync(tile))).then(() => {
isProcessing = false
if (processingQueue.value.length > 0) {
setTimeout(processBatch, 0)
}
})
}
function processTile(tile: Tile) {
if (!processingQueue.value.includes(tile)) {
processingQueue.value.push(tile)
processBatch()
}
}
function areTilesRelated(tile1: Tile, tile2: Tile): boolean {
const data1 = tileAnalysisCache.value.get(tile1.id)
const data2 = tileAnalysisCache.value.get(tile2.id)
if (!data1 || !data2) return false
const colorDifference = Math.sqrt(Math.pow(data1.color.r - data2.color.r, 2) + Math.pow(data1.color.g - data2.color.g, 2) + Math.pow(data1.color.b - data2.color.b, 2))
return colorDifference <= COLOR_SIMILARITY_THRESHOLD && Math.abs(data1.edge - data2.edge) <= EDGE_SIMILARITY_THRESHOLD && data1.namePrefix === data2.namePrefix
}
function cleanup() {
worker.terminate()
}
return {
processTile,
areTilesRelated,
cleanup
}
}

View File

@ -40,4 +40,9 @@ export class CharacterHairStorage extends BaseStorage<any> {
constructor() { constructor() {
super('characterHairs', 'id, name, createdAt, updatedAt') super('characterHairs', 'id, name, createdAt, updatedAt')
} }
async getSpriteId(characterTypeId: string) {
const characterType = await this.get(characterTypeId)
return characterType?.sprite
}
} }

View File

@ -32,7 +32,6 @@ export class TextureStorage {
updatedAt: texture.updatedAt, updatedAt: texture.updatedAt,
originX: texture.originX, originX: texture.originX,
originY: texture.originY, originY: texture.originY,
isAnimated: texture.isAnimated,
frameRate: texture.frameRate, frameRate: texture.frameRate,
frameWidth: texture.frameWidth, frameWidth: texture.frameWidth,
frameHeight: texture.frameHeight, frameHeight: texture.frameHeight,

View File

@ -1,4 +1,4 @@
import type { MapObject } from '@/application/types' import type { MapObject, Map as MapT } from '@/application/types'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
export type TeleportSettings = { export type TeleportSettings = {
@ -11,17 +11,11 @@ export type TeleportSettings = {
export const useMapEditorStore = defineStore('mapEditor', { export const useMapEditorStore = defineStore('mapEditor', {
state: () => { state: () => {
return { return {
active: false, active: true,
tool: 'move', tool: 'move',
drawMode: 'tile', drawMode: 'tile',
eraserMode: 'tile',
selectedTile: '', selectedTile: '',
selectedMapObject: null as MapObject | null, selectedMapObject: null as MapObject | null,
isTileListModalShown: false,
isMapObjectListModalShown: false,
isMapListModalShown: false,
isCreateMapModalShown: false,
isSettingsModalShown: false,
shouldClearTiles: false, shouldClearTiles: false,
teleportSettings: { teleportSettings: {
toMapId: '', toMapId: '',
@ -32,35 +26,18 @@ export const useMapEditorStore = defineStore('mapEditor', {
} }
}, },
actions: { actions: {
toggleActive() {
if (this.active) this.reset()
this.active = !this.active
},
setTool(tool: string) { setTool(tool: string) {
this.tool = tool this.tool = tool
}, },
setDrawMode(mode: string) { setDrawMode(mode: string) {
this.drawMode = mode this.drawMode = mode
}, },
setEraserMode(mode: string) {
this.eraserMode = mode
},
setSelectedTile(tile: string) { setSelectedTile(tile: string) {
this.selectedTile = tile this.selectedTile = tile
}, },
setSelectedMapObject(object: MapObject) { setSelectedMapObject(object: MapObject) {
this.selectedMapObject = object this.selectedMapObject = object
}, },
toggleSettingsModal() {
this.isSettingsModalShown = !this.isSettingsModalShown
},
toggleMapListModal() {
this.isMapListModalShown = !this.isMapListModalShown
this.isCreateMapModalShown = false
},
toggleCreateMapModal() {
this.isCreateMapModalShown = !this.isCreateMapModalShown
},
setTeleportSettings(teleportSettings: TeleportSettings) { setTeleportSettings(teleportSettings: TeleportSettings) {
this.teleportSettings = teleportSettings this.teleportSettings = teleportSettings
}, },
@ -75,11 +52,6 @@ export const useMapEditorStore = defineStore('mapEditor', {
this.drawMode = 'tile' this.drawMode = 'tile'
this.selectedTile = '' this.selectedTile = ''
this.selectedMapObject = null this.selectedMapObject = null
this.isTileListModalShown = false
this.isMapObjectListModalShown = false
this.isSettingsModalShown = false
this.isMapListModalShown = false
this.isCreateMapModalShown = false
this.shouldClearTiles = false this.shouldClearTiles = false
} }
} }

View File

@ -31,6 +31,13 @@ export const useMapStore = defineStore('map', {
const index = this.characters.findIndex((char) => char.character.id === updatedCharacter.character.id) const index = this.characters.findIndex((char) => char.character.id === updatedCharacter.character.id)
if (index !== -1) this.characters[index] = updatedCharacter if (index !== -1) this.characters[index] = updatedCharacter
}, },
// Property is mapCharacter key
updateCharacterProperty<K extends keyof MapCharacter>(characterId: UUID, property: K, value: MapCharacter[K]) {
const character = this.characters.find((char) => char.character.id === characterId)
if (character) {
character[property] = value
}
},
removeCharacter(characterId: UUID) { removeCharacter(characterId: UUID) {
this.characters = this.characters.filter((char) => char.character.id !== characterId) this.characters = this.characters.filter((char) => char.character.id !== characterId)
}, },

20
src/types/tileTypes.ts Normal file
View File

@ -0,0 +1,20 @@
export interface TileAnalysisResult {
tileId: string
color: {
r: number
g: number
b: number
}
edge: number
namePrefix: string
}
export interface TileWorkerMessage {
imageData: ImageData
tileId: string
tileName: string
}
export interface TileCache {
[key: string]: TileAnalysisResult
}

View File

@ -0,0 +1,68 @@
import type { TileAnalysisResult } from '@/types/tileTypes'
const PIXEL_SAMPLE_RATE = 4
self.onmessage = async (e: MessageEvent) => {
const { imageData, tileId, tileName } = e.data
const result = analyzeTile(imageData, tileId, tileName)
self.postMessage(result)
}
function analyzeTile(imageData: ImageData, tileId: string, tileName: string): TileAnalysisResult {
const { r, g, b } = getDominantColorFast(imageData)
const edge = getEdgeComplexityFast(imageData)
const namePrefix = tileName.split('_')[0]
return {
tileId,
color: { r, g, b },
edge,
namePrefix
}
}
function getDominantColorFast(imageData: ImageData) {
const data = new Uint8ClampedArray(imageData.data.buffer)
let r = 0,
g = 0,
b = 0,
total = 0
const length = data.length
for (let i = 0; i < length; i += 4 * PIXEL_SAMPLE_RATE) {
if (data[i + 3] > 0) {
r += data[i]
g += data[i + 1]
b += data[i + 2]
total++
}
}
return total > 0
? {
r: Math.round(r / total),
g: Math.round(g / total),
b: Math.round(b / total)
}
: { r: 0, g: 0, b: 0 }
}
function getEdgeComplexityFast(imageData: ImageData) {
const data = new Uint8ClampedArray(imageData.data.buffer)
const width = imageData.width
const height = imageData.height
let edgePixels = 0
for (let y = 0; y < height; y += PIXEL_SAMPLE_RATE) {
for (let x = 0; x < width; x += PIXEL_SAMPLE_RATE) {
const i = (y * width + x) * 4
if (
data[i + 3] > 0 &&
(x === 0 || y === 0 || x >= width - PIXEL_SAMPLE_RATE || y >= height - PIXEL_SAMPLE_RATE || data[i - 4 * PIXEL_SAMPLE_RATE + 3] === 0 || data[i + 4 * PIXEL_SAMPLE_RATE + 3] === 0 || data[i - width * 4 * PIXEL_SAMPLE_RATE + 3] === 0 || data[i + width * 4 * PIXEL_SAMPLE_RATE + 3] === 0)
) {
edgePixels++
}
}
}
return edgePixels * PIXEL_SAMPLE_RATE
}