Compare commits

..

23 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
32 changed files with 892 additions and 752 deletions

158
package-lock.json generated
View File

@ -1161,9 +1161,9 @@
} }
}, },
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.32.1", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.32.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.0.tgz",
"integrity": "sha512-/pqA4DmqyCm8u5YIDzIdlLcEmuvxb0v8fZdFhVMszSpDTgbQKdw3/mB3eMUHIbubtJ6F9j+LtmyCnHTEqIHyzA==", "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.1", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.32.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.0.tgz",
"integrity": "sha512-If3PDskT77q7zgqVqYuj7WG3WC08G1kwXGVFi9Jr8nY6eHucREHkfpX79c0ACAjLj3QIWKPJR7w4i+f5EdLH5Q==", "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.1", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.32.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.0.tgz",
"integrity": "sha512-zCpKHioQ9KgZToFp5Wvz6zaWbMzYQ2LJHQ+QixDKq52KKrF65ueu6Af4hLlLWHjX1Wf/0G5kSJM9PySW9IrvHA==", "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.1", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.32.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.0.tgz",
"integrity": "sha512-sFvF+t2+TyUo/ZQqUcifrJIgznx58oFZbdHS9TvHq3xhPVL9nOp+yZ6LKrO9GWTP+6DbFtoyLDbjTpR62Mbr3Q==", "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.1", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.32.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.0.tgz",
"integrity": "sha512-NbOa+7InvMWRcY9RG+B6kKIMD/FsnQPH0MWUvDlQB1iXnF/UcKSudCXZtv4lW+C276g3w5AxPbfry5rSYvyeYA==", "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.1", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.32.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.0.tgz",
"integrity": "sha512-JRBRmwvHPXR881j2xjry8HZ86wIPK2CcDw0EXchE1UgU0ubWp9nvlT7cZYKc6bkypBt745b4bglf3+xJ7hXWWw==", "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.1", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.32.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.0.tgz",
"integrity": "sha512-PKvszb+9o/vVdUzCCjL0sKHukEQV39tD3fepXxYrHE3sTKrRdCydI7uldRLbjLmDA3TFDmh418XH19NOsDRH8g==", "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.1", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.32.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.0.tgz",
"integrity": "sha512-9WHEMV6Y89eL606ReYowXuGF1Yb2vwfKWKdD1A5h+OYnPZSJvxbEjxTRKPgi7tkP2DSnW0YLab1ooy+i/FQp/Q==", "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.1", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.32.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.0.tgz",
"integrity": "sha512-tZWc9iEt5fGJ1CL2LRPw8OttkCBDs+D8D3oEM8mH8S1ICZCtFJhD7DZ3XMGM8kpqHvhGUTvNUYVDnmkj4BDXnw==", "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.1", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.32.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.0.tgz",
"integrity": "sha512-FTYc2YoTWUsBz5GTTgGkRYYJ5NGJIi/rCY4oK/I8aKowx1ToXeoVVbIE4LGAjsauvlhjfl0MYacxClLld1VrOw==", "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.1", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.32.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.0.tgz",
"integrity": "sha512-F51qLdOtpS6P1zJVRzYM0v6MrBNypyPEN1GfMiz0gPu9jN8ScGaEFIZQwteSsGKg799oR5EaP7+B2jHgL+d+Kw==", "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.1", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.32.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.0.tgz",
"integrity": "sha512-wO0WkfSppfX4YFm5KhdCCpnpGbtgQNj/tgvYzrVYFKDpven8w2N6Gg5nB6w+wAMO3AIfSTWeTjfVe+uZ23zAlg==", "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.1", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.32.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.0.tgz",
"integrity": "sha512-iWswS9cIXfJO1MFYtI/4jjlrGb/V58oMu4dYJIKnR5UIwbkzR0PJ09O0PDZT0oJ3LYWXBSWahNf/Mjo6i1E5/g==", "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.1", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.32.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.0.tgz",
"integrity": "sha512-RKt8NI9tebzmEthMnfVgG3i/XeECkMPS+ibVZjZ6mNekpbbUmkNWuIN2yHsb/mBPyZke4nlI4YqIdFPgKuoyQQ==", "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.1", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.32.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.0.tgz",
"integrity": "sha512-WQFLZ9c42ECqEjwg/GHHsouij3pzLXkFdz0UxHa/0OM12LzvX7DzedlY0SIEly2v18YZLRhCRoHZDxbBSWoGYg==", "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.1", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.32.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.0.tgz",
"integrity": "sha512-BLoiyHDOWoS3uccNSADMza6V6vCNiphi94tQlVIL5de+r6r/CCQuNnerf+1g2mnk2b6edp5dk0nhdZ7aEjOBsA==", "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.1", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.32.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.0.tgz",
"integrity": "sha512-w2l3UnlgYTNNU+Z6wOR8YdaioqfEnwPjIsJ66KxKAf0p+AuL2FHeTX6qvM+p/Ue3XPBVNyVSfCrfZiQh7vZHLQ==", "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.1", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.32.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.0.tgz",
"integrity": "sha512-Am9H+TGLomPGkBnaPWie4F3x+yQ2rr4Bk2jpwy+iV+Gel9jLAu/KqT8k3X4jxFPW6Zf8OMnehyutsd+eHoq1WQ==", "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.1", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.32.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.0.tgz",
"integrity": "sha512-ar80GhdZb4DgmW3myIS9nRFYcpJRSME8iqWgzH2i44u+IdrzmiXVxeFnExQ5v4JYUSpg94bWjevMG8JHf1Da5Q==", "integrity": "sha512-g2ASy1QwHP88y5KWvblUolJz9rN+i4ZOsYzkEwcNfaNooxNUXG+ON6F5xFo0NIItpHqxcdAyls05VXpBnludGw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -4250,9 +4250,9 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.32.1", "version": "4.34.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.32.1.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.0.tgz",
"integrity": "sha512-z+aeEsOeEa3mEbS1Tjl6sAZ8NE3+AalQz1RJGj81M+fizusbdDMoEJwdJNHfaB40Scr4qNu+welOfes7maKonA==", "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.1", "@rollup/rollup-android-arm-eabi": "4.34.0",
"@rollup/rollup-android-arm64": "4.32.1", "@rollup/rollup-android-arm64": "4.34.0",
"@rollup/rollup-darwin-arm64": "4.32.1", "@rollup/rollup-darwin-arm64": "4.34.0",
"@rollup/rollup-darwin-x64": "4.32.1", "@rollup/rollup-darwin-x64": "4.34.0",
"@rollup/rollup-freebsd-arm64": "4.32.1", "@rollup/rollup-freebsd-arm64": "4.34.0",
"@rollup/rollup-freebsd-x64": "4.32.1", "@rollup/rollup-freebsd-x64": "4.34.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.32.1", "@rollup/rollup-linux-arm-gnueabihf": "4.34.0",
"@rollup/rollup-linux-arm-musleabihf": "4.32.1", "@rollup/rollup-linux-arm-musleabihf": "4.34.0",
"@rollup/rollup-linux-arm64-gnu": "4.32.1", "@rollup/rollup-linux-arm64-gnu": "4.34.0",
"@rollup/rollup-linux-arm64-musl": "4.32.1", "@rollup/rollup-linux-arm64-musl": "4.34.0",
"@rollup/rollup-linux-loongarch64-gnu": "4.32.1", "@rollup/rollup-linux-loongarch64-gnu": "4.34.0",
"@rollup/rollup-linux-powerpc64le-gnu": "4.32.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.34.0",
"@rollup/rollup-linux-riscv64-gnu": "4.32.1", "@rollup/rollup-linux-riscv64-gnu": "4.34.0",
"@rollup/rollup-linux-s390x-gnu": "4.32.1", "@rollup/rollup-linux-s390x-gnu": "4.34.0",
"@rollup/rollup-linux-x64-gnu": "4.32.1", "@rollup/rollup-linux-x64-gnu": "4.34.0",
"@rollup/rollup-linux-x64-musl": "4.32.1", "@rollup/rollup-linux-x64-musl": "4.34.0",
"@rollup/rollup-win32-arm64-msvc": "4.32.1", "@rollup/rollup-win32-arm64-msvc": "4.34.0",
"@rollup/rollup-win32-ia32-msvc": "4.32.1", "@rollup/rollup-win32-ia32-msvc": "4.34.0",
"@rollup/rollup-win32-x64-msvc": "4.32.1", "@rollup/rollup-win32-x64-msvc": "4.34.0",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },

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

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

View File

@ -183,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 = {

View File

@ -1,134 +1,34 @@
<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" />
<CharacterHair :mapCharacter="props.mapCharacter" :currentX="currentPositionX" :currentY="currentPositionY" /> <HealthBar :mapCharacter="props.mapCharacter" />
<Sprite ref="charSprite" :depth="isometricDepth" :x="currentPositionX" :y="currentPositionY" :origin-y="1" :flipX="isFlippedX" /> <CharacterHair :mapCharacter="props.mapCharacter" />
<Sprite ref="characterSprite" :origin-y="1" :flipX="isFlippedX" />
</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 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 { 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 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) => {
isometricDepth.value = calculateIsometricDepth(positionX, positionY, 28, 94, true)
}
const updatePosition = (positionX: number, positionY: number, direction: Direction) => {
const newPositionX = tileToWorldX(props.tilemap, positionX, positionY)
const newPositionY = tileToWorldY(props.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))
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 currentDirection = computed(() => {
return [0, 6].includes(props.mapCharacter.character.rotation ?? 0) ? 'left_up' : 'right_down'
})
const currentAction = computed(() => {
return props.mapCharacter.isMoving ? 'walk' : 'idle'
})
const charTexture = computed(() => {
const spriteId = charSpriteId.value ?? 'idle_right_down'
return `${spriteId}-${currentAction.value}_${currentDirection.value}`
})
const updateSprite = () => {
if (!charSprite.value) return
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)
}
}
const handlePositionUpdate = (newValues: any, oldValues: any) => { const handlePositionUpdate = (newValues: any, oldValues: any) => {
if (!newValues) return if (!newValues) return
@ -148,40 +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
}), }),
handlePositionUpdate handlePositionUpdate
) )
onMounted(async () => { onMounted(async () => {
let character = props.mapCharacter.character await initializeSprite()
const characterTypeStorage = new CharacterTypeStorage() if (props.mapCharacter.character.id === gameStore.character!.id) {
const spriteId = await characterTypeStorage.getSpriteId(character.characterType!)
if (!spriteId) return
charSpriteId.value = spriteId
await loadSpriteTextures(scene, spriteId)
if (charSprite.value) {
charSprite.value.setTexture(charTexture.value)
charSprite.value.setFlipX(isFlippedX.value)
charSprite.value.setName(props.mapCharacter.character.name)
}
if (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(charSprite.value as Phaser.GameObjects.Sprite)
} }
updatePosition(character.positionX, character.positionY, character.rotation)
}) })
onUnmounted(() => { onUnmounted(() => {
tween.value?.stop() cleanup()
}) })
</script> </script>

View File

@ -12,8 +12,6 @@ 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()
@ -39,9 +37,7 @@ const imageProps = computed(() => {
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.currentY,
x: props.currentX
} }
}) })

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

@ -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

@ -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" alt="Zoom-in button icon"/> <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" alt=""/> <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" alt="Zoom-out button icon"/> <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" alt=""/> <img class="w-full h-full" src="/assets/ui-elements/button-ui-box-textured.svg" alt="" />
</button> </button>
</div> </div>
</div> </div>

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

@ -48,23 +48,20 @@
<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 <SpriteActionsInput v-model="action.sprites" @tempOffsetChange="(index, offset) => handleTempOffsetChange(action, index, offset)" />
v-model="action.sprites"
@tempOffsetChange="(index, offset) => handleTempOffsetChange(action, index, offset)"
/>
</div> </div>
</form> </form>
</template> </template>
</Accordion> </Accordion>
<SpritePreview <SpritePreview
v-if="selectedAction" v-if="selectedAction"
:sprites="selectedAction.sprites" :sprites="selectedAction.sprites"
:frame-rate="selectedAction.frameRate" :frame-rate="selectedAction.frameRate"
:is-modal-open="isModalOpen" :is-modal-open="isModalOpen"
:temp-offset-index="tempOffsetData.index" :temp-offset-index="tempOffsetData.index"
:temp-offset="tempOffsetData.offset" :temp-offset="tempOffsetData.offset"
@update:frame-rate="updateFrameRate" @update:frame-rate="updateFrameRate"
@update:is-modal-open="isModalOpen = $event" @update:is-modal-open="isModalOpen = $event"
/> />
</div> </div>
</div> </div>
@ -199,9 +196,9 @@ function updateFrameRate(value: number) {
} }
} }
const tempOffsetData = ref<{ index: number | undefined; offset: { x: number; y: number } | undefined }>({ const tempOffsetData = ref<{ index: number | undefined; offset: { x: number; y: number } | undefined }>({
index: undefined, index: undefined,
offset: undefined offset: undefined
}) })
function handleTempOffsetChange(action: SpriteAction, index: number, offset: { x: number; y: number }) { function handleTempOffsetChange(action: SpriteAction, index: number, offset: { x: number; y: number }) {

View File

@ -2,9 +2,7 @@
<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.url" class="max-w-full max-h-full object-contain pointer-events-none" alt="Uploaded image" @load="updateImageDimensions($event, index)" /> <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="image.dimensions" class="absolute bottom-1 right-1 bg-black/50 text-white text-xs px-1 py-0.5 rounded transition-opacity font-default"> <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>
{{ 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">
@ -61,10 +59,6 @@ interface SpriteImage {
x: number x: number
y: number y: number
} }
dimensions?: {
width: number
height: number
}
} }
interface Props { interface Props {
@ -78,7 +72,7 @@ const props = withDefaults(defineProps<Props>(), {
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:modelValue', value: SpriteImage[]): void (e: 'update:modelValue', value: SpriteImage[]): void
(e: 'close'): void (e: 'close'): void
(e: 'tempOffsetChange', index: number, offset: { x: number, y: number }): void (e: 'tempOffsetChange', index: number, offset: { x: number; y: number }): void
}>() }>()
const fileInput = ref<HTMLInputElement | null>(null) const fileInput = ref<HTMLInputElement | null>(null)
@ -179,16 +173,13 @@ const onOffsetChange = () => {
watch(tempOffset, onOffsetChange, { deep: true }) watch(tempOffset, onOffsetChange, { deep: true })
const imageDimensions = ref<{ [key: number]: { width: number; height: number } }>({})
const updateImageDimensions = (event: Event, index: number) => { const updateImageDimensions = (event: Event, index: number) => {
const img = event.target as HTMLImageElement const img = event.target as HTMLImageElement
const newImages = [...props.modelValue] imageDimensions.value[index] = {
newImages[index] = { width: img.naturalWidth,
...newImages[index], height: img.naturalHeight
dimensions: {
width: img.naturalWidth,
height: img.naturalHeight
}
} }
updateImages(newImages)
} }
</script> </script>

View File

@ -39,15 +39,7 @@
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col">
<label class="block mb-2 text-white">Frame: {{ currentFrame + 1 }} of {{ sprites.length }}</label> <label class="block mb-2 text-white">Frame: {{ currentFrame + 1 }} of {{ sprites.length }}</label>
<input <input type="range" v-model.number="currentFrame" :min="0" :max="sprites.length - 1" step="1" class="w-full accent-cyan-500" @input="stopAnimation" />
type="range"
v-model.number="currentFrame"
:min="0"
:max="sprites.length - 1"
step="1"
class="w-full accent-cyan-500"
@input="stopAnimation"
/>
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col">
<label class="block mb-2 text-white">Zoom: {{ zoomLevel }}%</label> <label class="block mb-2 text-white">Zoom: {{ zoomLevel }}%</label>
@ -69,7 +61,7 @@ const props = defineProps<{
frameRate: number frameRate: number
isModalOpen?: boolean isModalOpen?: boolean
tempOffsetIndex?: number tempOffsetIndex?: number
tempOffset?: { x: number, y: number } tempOffset?: { x: number; y: number }
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{

View File

@ -27,28 +27,20 @@ function handlePointer(pointer: Phaser.Input.Pointer) {
// Check if left mouse button is pressed // Check if left mouse button is pressed
if (!pointer.isDown) return if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera // Check if shift is pressed or if we're in move mode, this means we are moving the camera
if (pointer.event.shiftKey) return if (pointer.event.shiftKey || mapEditor.tool.value === 'move') return
// Check if draw mode is tile // Check if draw mode is tile
switch (mapEditor.drawMode.value) { switch (mapEditor.drawMode.value) {
case 'tile': case 'tile':
mapTiles.value.handlePointer(pointer) mapTiles.value.handlePointer(pointer)
break
case 'object': case 'object':
mapObjects.value.handlePointer(pointer) mapObjects.value.handlePointer(pointer)
case 'teleport': break
case 'event':
eventTiles.value.handlePointer(pointer) eventTiles.value.handlePointer(pointer)
break
} }
} }
onMounted(() => {
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointer)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, handlePointer)
})
onUnmounted(() => {
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointer)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, handlePointer)
mapEditor.reset()
})
</script> </script>

View File

@ -1,66 +1,69 @@
<template> <template>
<Modal ref="modalRef" :modal-width="645" :modal-height="260" :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="mapEditor.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': mapEditor.selectedMapObject.value?.id === mapObject.id, }"
'border-transparent hover:border-gray-300': mapEditor.selectedMapObject.value?.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 { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { MapObjectStorage } from '@/storage/storages' import { MapObjectStorage } from '@/storage/storages'
import { liveQuery } from 'dexie' import { liveQuery } from 'dexie'
import { computed, onMounted, onUnmounted, ref, useTemplateRef } 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 mapEditor = useMapEditorComposable()
const searchQuery = ref('') const searchQuery = ref('')
const selectedTags = ref<string[]>([]) const selectedTags = ref<string[]>([])
const mapObjectList = ref<MapObject[]>([]) const mapObjectList = ref<MapObject[]>([])
const modalRef = useTemplateRef('modalRef')
defineExpose({
open: () => modalRef.value?.open(),
close: () => modalRef.value?.close()
})
const uniqueTags = computed(() => { const uniqueTags = computed(() => {
const allTags = mapObjectList.value.flatMap((obj) => obj.tags || []) const allTags = mapObjectList.value.flatMap((obj) => obj.tags || [])
@ -86,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,106 +1,111 @@
<template> <template>
<Modal ref="modalRef" :modal-width="645" :modal-height="600" :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 { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { useTileProcessingComposable } from '@/composables/useTileProcessingComposable'
import { TileStorage } from '@/storage/storages' import { TileStorage } from '@/storage/storages'
import { liveQuery } from 'dexie' import { computed, onMounted, onUnmounted, ref } from 'vue'
import { computed, onMounted, onUnmounted, ref, useTemplateRef } from 'vue'
const isOpen = ref(false)
const tileStorage = new TileStorage() const tileStorage = new TileStorage()
const mapEditor = useMapEditorComposable() 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[]>([])
const modalRef = useTemplateRef('modalRef')
defineExpose({ defineExpose({
open: () => modalRef.value?.open(), open: () => (isOpen.value = true),
close: () => modalRef.value?.close() close: () => (isOpen.value = false),
toggle: () => (isOpen.value = !isOpen.value)
}) })
const uniqueTags = computed(() => { const uniqueTags = computed(() => {
@ -117,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 {
@ -128,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)
@ -162,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) || ''
} }
@ -235,22 +161,19 @@ function isActiveTile(tile: Tile): boolean {
return mapEditor.selectedTile.value === 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,6 +1,6 @@
<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': mapEditor.tool.value === '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': mapEditor.tool.value !== '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>
@ -109,7 +109,7 @@ defineExpose({ tileListShown, mapObjectListShown })
// drawMode // drawMode
function setDrawMode(value: string) { function setDrawMode(value: string) {
if (mapEditor.tool.value === 'paint' || mapEditor.tool.value === 'pencil') { if (mapEditor.tool.value === 'paint' || mapEditor.tool.value === 'pencil' || mapEditor.tool.value === 'eraser') {
emit('close-lists') emit('close-lists')
if (value === 'tile') emit('open-tile-list') if (value === 'tile') emit('open-tile-list')
if (value === 'map_object') emit('open-map-object-list') if (value === 'map_object') emit('open-map-object-list')
@ -139,6 +139,10 @@ function handleModeClick(mode: string, type: 'pencil' | 'eraser') {
function handleClick(tool: string) { function handleClick(tool: string) {
if (tool === 'settings') { if (tool === 'settings') {
emit('open-settings') emit('open-settings')
emit('close-lists')
} else if (tool === 'move') {
emit('close-lists')
mapEditor.setTool(tool)
} else { } else {
mapEditor.setTool(tool) mapEditor.setTool(tool)
} }

View File

@ -12,14 +12,14 @@
@open-maps="mapModal?.open" @open-maps="mapModal?.open"
@open-settings="mapSettingsModal?.open" @open-settings="mapSettingsModal?.open"
@close-editor="mapEditor.toggleActive" @close-editor="mapEditor.toggleActive"
@close-lists="tileModal?.close" @close-lists="tileList?.close"
@closeLists="objectModal?.close" @closeLists="objectList?.close"
@open-tile-list="tileModal?.open" @open-tile-list="tileList?.open"
@open-map-object-list="objectModal?.open" @open-map-object-list="objectList?.open"
/> />
<MapList ref="mapModal" @open-create-map="mapSettingsModal?.open" /> <MapList ref="mapModal" @open-create-map="mapSettingsModal?.open" />
<TileList ref="tileModal" /> <TileList ref="tileList" />
<ObjectList ref="objectModal" /> <ObjectList ref="objectList" />
<MapSettings ref="mapSettingsModal" /> <MapSettings ref="mapSettingsModal" />
<TeleportModal ref="teleportModal" /> <TeleportModal ref="teleportModal" />
</div> </div>
@ -52,8 +52,8 @@ const gameStore = useGameStore()
const toolbar = useTemplateRef('toolbar') const toolbar = useTemplateRef('toolbar')
const mapModal = useTemplateRef('mapModal') const mapModal = useTemplateRef('mapModal')
const tileModal = useTemplateRef('tileModal') const tileList = useTemplateRef('tileList')
const objectModal = useTemplateRef('objectModal') const objectList = useTemplateRef('objectList')
const mapSettingsModal = useTemplateRef('mapSettingsModal') const mapSettingsModal = useTemplateRef('mapSettingsModal')
const teleportModal = useTemplateRef('teleportModal') const teleportModal = useTemplateRef('teleportModal')

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

@ -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

@ -1,70 +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 = pointer.position
gameStore.setPlayerDraggingCamera(true)
}
function handlePointerMove(pointer: Phaser.Input.Pointer) {
updateWaypoint(pointer.worldX, pointer.worldY)
if (!gameStore.game.isPlayerDraggingCamera) return
// If the distance is less than the drag threshold, return
// We do this to prevent the camera from scrolling too quickly
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 handlePointerUp(pointer: Phaser.Input.Pointer) {
gameStore.setPlayerDraggingCamera(false)
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,86 +0,0 @@
import config from '@/application/config'
import { getTile, tileToWorldXY } from '@/composables/mapComposable'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { useGameStore } from '@/stores/gameStore'
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 mapEditor = useMapEditorComposable()
const isMoveTool = computed(() => mapEditor.tool.value === '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 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 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,25 +0,0 @@
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
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 mapEditor = useMapEditorComposable()
const gameHandlers = useGamePointerHandlers(scene, layer, waypoint, camera)
const mapEditorHandlers = useMapEditorPointerHandlers(scene, layer, waypoint, camera)
const currentHandlers = computed(() => (mapEditor.active.value ? mapEditorHandlers : gameHandlers))
const setupPointerHandlers = () => currentHandlers.value.setupPointerHandlers()
const cleanupPointerHandlers = () => currentHandlers.value.cleanupPointerHandlers()
watch(
() => mapEditor.active.value,
() => {
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

@ -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
}