Compare commits

...

41 Commits

Author SHA1 Message Date
f75528b2af Merge remote-tracking branch 'origin/feature/#215-dexie-caching-implementation' into feature/character-profile
# Conflicts:
#	src/screens/Game.vue
2024-10-25 22:20:34 +02:00
9f50b062b0 Worked on dynamic texture loading 2024-10-25 22:17:02 +02:00
141cd225c8 Fix hotkey event not working 2024-10-22 20:28:49 +02:00
2870fea15e Added global listener for character profile hotkey
Also re-organised assets folder
2024-10-22 20:23:58 +02:00
8374dc91da WIP Character Profile popup, cleaned up old assets 2024-10-21 22:39:28 +02:00
347fc0e1e8 MVP Dexie loader 2024-10-21 18:35:36 +02:00
715fe5c318 Cookies 2024-10-21 02:15:29 +02:00
df19c1094c feature/#215-dexie-caching-implementation 2024-10-21 02:08:27 +02:00
279b9bc7a3 Reversed order of login screen (img>form) 2024-10-20 01:10:29 +02:00
6adfc727c5 return if shift is pressed for tiles, objects and event tiles (camera drag = active) 2024-10-19 22:36:08 +02:00
1aa07daa35 Also added overflow to tile group 2024-10-19 22:25:25 +02:00
36901782ea Adjusted gm panel + toolbar styling, fixed tilelist overflow 2024-10-19 22:11:20 +02:00
62f2e1124c #211 Fixed overflow scroll in left panel not working 2024-10-19 21:27:04 +02:00
f6e1a54e74 Body view fixes? 2024-10-19 21:20:43 +02:00
6e2885cba6 Added tile sorting logic, minor improvements 2024-10-19 21:15:13 +02:00
252d9c87fd Firefox styling fixes 2024-10-19 17:13:06 +02:00
856829b605 Possible fix for text alignment on Windows 2024-10-19 17:00:58 +02:00
a0f0b40ed3 Replaced modal resize icon, started writing components for character type management, spride field made optional 2024-10-19 02:15:48 +02:00
68222ab511 Styling fixes 2024-10-18 23:08:01 +02:00
fe804037d0 #137 : Listen for zoneEffects in effects component 2024-10-18 22:41:42 +02:00
5d288772b5 #137 : Added logic to set effects per zone in zone editor 2024-10-18 22:23:56 +02:00
3c9b92ccbd Better func. naming 2024-10-18 19:53:02 +02:00
13cb46658f nginx conf. test 2024-10-18 19:45:25 +02:00
222614b856 Moved GmTools and GmPanel to App.vue 2024-10-18 19:27:22 +02:00
9f10db142b don't have ZE enabled by default 2024-10-18 19:21:06 +02:00
860fe705c6 Saving maps works again 2024-10-18 19:10:41 +02:00
352ec3fad8 Moved search input into modal 2024-10-18 17:55:58 +02:00
c7a3d74408 Removed todo comment 2024-10-18 17:47:11 +02:00
1e0da5f7cc Removed unused imports, re-added paint func, more refactor work 2024-10-18 17:45:50 +02:00
7ce054191a Re-added & Improved delete logics for tiles, zone objects and event tiles. Added more code comments for better DX. 2024-10-18 15:43:50 +02:00
34f547f0a6 Added deleteTile() func. to zone composable 2024-10-18 15:40:28 +02:00
14e07aa4a1 npm update 2024-10-18 15:40:14 +02:00
95dcf237cf npm run format 2024-10-18 02:49:35 +02:00
5cc1821922 Removed unused import 2024-10-18 02:49:04 +02:00
9d774bcb18 Removed redundant code , refactor event tile code 2024-10-18 02:48:27 +02:00
c6869f47b1 Typescript improvements, added move zone object logic. 2024-10-18 02:33:51 +02:00
390b9517e0 Moved styling to main.scss as it's global 2024-10-18 00:42:15 +02:00
2497da30b7 Re-added initial-scale=1 2024-10-18 00:40:50 +02:00
66e56d3626 Renamed Keybindings > Hotkeys 2024-10-18 00:33:06 +02:00
d68ee120ab Merge remote-tracking branch 'origin/feature/refactor-zone-editor'
# Conflicts:
#	src/components/gui/Keybindings.vue
#	src/components/gui/Minimap.vue
2024-10-18 00:21:03 +02:00
3c8744dc75 Reworked layout for keybindings, hide minimap on smaller screens 2024-10-17 23:26:25 +02:00
84 changed files with 1990 additions and 958 deletions

View File

@ -27,5 +27,6 @@ RUN npm run build-ntc
# Production stage
FROM nginx:1.26.1-alpine
COPY --from=builder /usr/src/app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Sylvan Quest - Play</title>
</head>
<body>

16
nginx.conf Normal file
View File

@ -0,0 +1,16 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Redirect example
location /discord {
return 301 https://discord.gg/JTev3nzeDa;
}
# Serve static files
location / {
try_files $uri $uri/ /index.html;
}
}

593
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -18,6 +18,7 @@
"@vueuse/core": "^10.5.0",
"@vueuse/integrations": "^10.5.0",
"axios": "^1.7.7",
"dexie": "^4.0.8",
"phaser": "^3.86.0",
"pinia": "^2.1.6",
"socket.io-client": "^4.8.0",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 440 KiB

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="438.529px" height="438.529px" viewBox="0 0 438.529 438.529" style="enable-background:new 0 0 438.529 438.529;"
xml:space="preserve">
<g>
<g>
<path d="M180.156,225.828c-1.903-1.902-4.093-2.854-6.567-2.854c-2.475,0-4.665,0.951-6.567,2.854l-94.787,94.787l-41.112-41.117
c-3.617-3.61-7.895-5.421-12.847-5.421c-4.952,0-9.235,1.811-12.851,5.421c-3.617,3.621-5.424,7.905-5.424,12.854v127.907
c0,4.948,1.807,9.229,5.424,12.847c3.619,3.613,7.902,5.424,12.851,5.424h127.906c4.949,0,9.23-1.811,12.847-5.424
c3.615-3.617,5.424-7.898,5.424-12.847s-1.809-9.233-5.424-12.854l-41.112-41.104l94.787-94.793
c1.902-1.903,2.853-4.086,2.853-6.564c0-2.478-0.953-4.66-2.853-6.57L180.156,225.828z"/>
<path d="M433.11,5.424C429.496,1.807,425.212,0,420.263,0H292.356c-4.948,0-9.227,1.807-12.847,5.424
c-3.614,3.615-5.421,7.898-5.421,12.847s1.807,9.233,5.421,12.847l41.106,41.112l-94.786,94.787
c-1.901,1.906-2.854,4.093-2.854,6.567s0.953,4.665,2.854,6.567l32.552,32.548c1.902,1.903,4.086,2.853,6.563,2.853
s4.661-0.95,6.563-2.853l94.794-94.787l41.104,41.109c3.62,3.616,7.905,5.428,12.854,5.428s9.229-1.812,12.847-5.428
c3.614-3.614,5.421-7.898,5.421-12.847V18.268C438.53,13.315,436.734,9.04,433.11,5.424z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#b1b2b5" d="M226.125 32.032a84.287 84.287 0 0 0-3.51.026c-11.4.318-24.464 2.935-40.945 8.63l-2.682.926-2.726-.777c-38.965-11.123-65.026.52-90.262 14.957 5.557 9.074 10.35 19.569 8.365 32.049l-.045.29-.066.286c-2.043 9.01-7.64 22.012-14.363 35.676-1.723 3.502-3.57 6.712-5.37 10.1 12.791-6.326 24.681-12.344 38.95-13.885l2.53-.274 2.302 1.094c-.018-.008 2.436.899 5.261 1.853 2.826.955 6.331 2.16 9.977 3.68 5.207 2.172 10.979 4.697 15.512 9.256 22.303-6.732 40.75-6.51 60.802-1.414 18.213-10.407 34.866-11.39 47.99-6.512 8.553 3.178 15.291 8.373 20.93 13.842l10.557-22.625 20.805 3.62c9.72-15.164 9.644-33.821 3.234-49.167-7.012-16.786-20.608-27.857-34.928-26.795l-2.404.178-2.172-1.047c-16.758-8.082-30.442-13.73-47.742-13.967zM112.791 138.688a44.845 44.845 0 0 0-3.287.745c-7.307 47.909-26.314 187.24-6.9 293.404 158.558 12.92 313.945 3.207 334.82 1.793.099-2.715.066-5.485-.121-8.313-1.354-20.44-10.822-42.312-22.235-55.045-6.318-7.049-23.99-13.695-48.029-16.789-24.04-3.094-54.057-3.4-85.057-2.056l-6.337.275c-21.183 1.115-40.742 2.29-58.89 5.111-4.051 8.895-13.356 14.998-23.735 14.998-13.904 0-25.885-10.947-25.885-24.892s11.981-24.89 25.885-24.89c5.778 0 11.22 1.896 15.613 5.107 8.084-10.448 16.365-21.222 25.375-31.897-8.386-8.22-17.33-15.993-27.44-22.722-3.941 2.49-8.595 3.943-13.548 3.943-13.842 0-25.39-11.288-25.39-25.14 0-13.854 11.548-25.141 25.39-25.141 2.64 0 5.194.413 7.603 1.174l33.02-38.875c-8.516-9.7-17.736-19.149-28.409-28.37-4.074 2.75-8.973 4.364-14.2 4.364-10.517 0-19.703-6.518-23.513-15.723-5.762 1.045-11.908 2.718-18.841 5.137l-7.551 2.634-3.504-7.189c-.353-.723-5.078-4.58-11.012-7.055-2.967-1.237-6.073-2.315-8.81-3.24-1.348-.455-2.457-.895-3.582-1.31l-1.43-.038zm96.748 2.565c13.9 11.186 25.49 22.656 35.904 34.33l18.24-21.475c-.711-3.822-1.422-7.642-2.142-11.478l-52.002-1.377zm58.313 35.754l-10.586 12.463c5.451 6.674 10.675 13.403 15.857 20.174-1.556-10.837-3.35-21.714-5.271-32.637zm-22.471 26.455l-30.586 36.012a24.652 24.652 0 0 1 3.613 12.845c0 2.354-.34 4.63-.96 6.793 10.699 7.168 20.067 15.208 28.613 23.489 9.55-10.298 19.972-20.277 31.78-29.457a476.305 476.305 0 0 0-.661-8.694c-11.152-14.168-21.175-27.806-31.8-40.988zm-52.361 41.717c-4.261 0-7.39 3.165-7.39 7.14 0 3.976 3.129 7.14 7.39 7.14 4.26 0 7.388-3.164 7.388-7.14 0-3.975-3.128-7.14-7.388-7.14zm85.732 30.572c-7.128 6.278-13.756 12.912-20.057 19.762 6.337 6.753 12.324 13.432 18.213 19.724 1.44-13.148 2-26.304 1.844-39.486zm-32.096 33.514c-8.101 9.677-15.774 19.602-23.41 29.44 15.249-1.938 31.118-2.91 47.58-3.776-8.238-8.276-16.088-17.033-24.17-25.664zm-53.636 31.763c-4.748 0-7.885 3.282-7.885 6.89 0 3.61 3.137 6.893 7.885 6.893 4.747 0 7.884-3.283 7.884-6.892 0-3.609-3.137-6.89-7.884-6.89zm-86.577 110.164c1.968 8.31 4.2 16.358 6.746 24.059 103.476 5.837 209.68 7.195 303.832-1.3 8.328-5.386 13.8-12.612 16.975-21.06-36.35 2.27-180.346 9.84-327.553-1.699z"/></svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#b1b2b5" d="M162 35.75l-94.49 27.1c-12.05 6.3-23.47 23.9-31.01 46.35-6.07 18.2-9.62 38.9-10.93 58.3L136.7 112zm188 .1L375.4 112l111 55.6c-1.3-19.3-4.9-40.2-10.9-58.3-5.7-17.05-13.6-31.35-22.5-40.05-2.7-2.8-5.5-4.9-8.4-6.4zm-172.9 11.5l-25.7 77.45-92.9 46.4 14.08 53.5 88.82 44.4 94.6-15.9 94.6 15.9 88.8-44.4 14.1-53.5-92.8-46.4-25.8-77.35h-10.5l-59.3 73.95-.1 61.1h-18.1l.1-61-59.3-74.15zM78.65 247.7l22.05 83.9 146.2-43.8v-14.7l-88.4 14.7zm354.75 0l-80 40.1-88.4-14.7v14.7l146.3 43.8zm-186.5 58.7l-31.6 9.6-35.1 70.2 66.7-33.3zm18.1 0v46.5l66.9 33.4-35.2-70.3zM191.7 323l-86.4 26 25.3 96.3zm128.6.1l61.1 122.1 25.3-96.2zm-55.3 50l.1 43.2 100.7 37.8-20.4-40.8zm-18.1 0l-80.2 40.1-20.5 40.9L247 416.3zm.1 62.4l-81.6 30.6 81.6 10.2zm18.1 0v40.7l81.7-10.2z"/></svg>

Before

Width:  |  Height:  |  Size: 985 B

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#b1b2b5" d="M256 68.02L169.7 240.7l14.8 44.5-17 5.6-15.2-45.6-66.42-53-12.18 60.9 12.62 12.6-12.66 12.6-15.21-15.2-30.53-20.4 25.15 163.4 39.17-65.2 89.66 35.8 30.9 46.4h86.4l30.9-46.4 89.7-35.8 39.1 65.2 25.2-163.4-30.6 20.4-15.2 15.2-12.6-12.6 12.6-12.6-12.2-60.9-66.4 53-15.2 45.6-17-5.6 14.8-44.5zm0 122.58l55.7 92.8 1.9 3.2-17.5 70-20.4 20.3h-39.4l-20.4-20.3-17.5-70zm0 34.8l-38.4 64 14.5 58 11.6 11.7h24.6l11.6-11.7 14.5-58z"/></svg>

Before

Width:  |  Height:  |  Size: 661 B

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#b1b2b5" d="M256 16c-36.446 0-73.264 13.433-139.97 40h279.94C329.263 29.433 292.445 16 256 16zM95.344 72L64 448c56 28 112 31.5 168 31.938V240H112v-48h288v48H280v239.938C336 479.5 392 476 448 448L416.656 72H95.344zm64.875 88a8 8 0 0 1 7.78 8 8 8 0 0 1-16 0 8 8 0 0 1 8.22-8zm48 0a8 8 0 0 1 7.78 8 8 8 0 0 1-16 0 8 8 0 0 1 8.22-8zm48 0a8 8 0 0 1 7.78 8 8 8 0 0 1-16 0 8 8 0 0 1 8.22-8zm48 0a8 8 0 0 1 7.78 8 8 8 0 0 1-16 0 8 8 0 0 1 8.22-8zm48 0a8 8 0 0 1 7.78 8 8 8 0 0 1-16 0 8 8 0 0 1 8.22-8zM248 240v240c2.667.002 5.333 0 8 0s5.333.002 8 0V240h-16zm-120 48h16v16h-16v-16zm32 0h16v16h-16v-16zm32 0h16v16h-16v-16zm112 0h16v16h-16v-16zm32 0h16v16h-16v-16zm32 0h16v16h-16v-16zm-240 32h16v16h-16v-16zm32 0h16v16h-16v-16zm32 0h16v16h-16v-16zm112 0h16v16h-16v-16zm32 0h16v16h-16v-16zm32 0h16v16h-16v-16zm-240 32h16v16h-16v-16zm32 0h16v16h-16v-16zm32 0h16v16h-16v-16zm112 0h16v16h-16v-16zm32 0h16v16h-16v-16zm32 0h16v16h-16v-16z"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#b1b2b5" d="M144 37.28c-25.883 0-63.05 25.96-65.22 82.845l65.345 28.47 71.625-28.626C213.124 63.69 169.852 37.28 144 37.28zm224 0c-25.852 0-69.124 26.412-71.75 82.69l71.625 28.624 65.344-28.47c-2.17-56.883-39.337-82.843-65.22-82.843zM76.594 136.626c-10.887 40.29-12.11 90.026-9.844 141.03 46.136 10.675 93.357 7.68 141.406.44 8.464-50.25 12.923-99.296 3.625-139.314l-64.81 25.907-3.095 1.25-3.063-1.343-64.218-27.97zm358.812 0l-64.22 27.97-3.06 1.343-3.095-1.25-64.81-25.907c-9.3 40.02-4.84 89.064 3.624 139.314 48.05 7.242 95.27 10.236 141.406-.438 2.266-51.005 1.043-100.74-9.844-141.03zm-250.25 160.72c-31.678 3.654-63.5 4.865-95.25.75 12.238 12.217 23.424 24.845 35.47 36.217 2.198 2.077 4.425 4.107 6.686 6.094 18.086-12.095 35.813-26.27 53.094-43.062zm141.688 0c17.28 16.792 35.008 30.966 53.094 43.06 2.26-1.986 4.488-4.016 6.687-6.092 12.045-11.373 23.23-24 35.47-36.22-31.752 4.117-63.573 2.906-95.25-.75zm-258.97 1.842c1.515 24.774 3.633 49.29 5.69 73.188 15.054-6.598 29.912-14.187 44.53-23.03-1.24-1.13-2.477-2.264-3.688-3.408-16.52-15.597-30.655-32.307-46.53-46.75zm376.25 0c-15.875 14.443-30.01 31.153-46.53 46.75-1.21 1.144-2.448 2.278-3.688 3.407 14.618 8.844 29.476 16.433 44.53 23.03 2.057-23.897 4.175-48.413 5.69-73.187zm-240.093 1.844c-19.095 19.538-38.774 35.968-58.936 49.845 12.672 9.234 27 16.825 44.78 21.625 4.59-23.255 9.642-47.365 14.157-71.47zm103.94 0c4.514 24.105 9.566 48.215 14.155 71.47 17.78-4.8 32.11-12.39 44.78-21.625-20.16-13.877-39.84-30.307-58.936-49.844zM130.936 360.19c-15.606 9.753-31.47 18.076-47.5 25.28 11.79 18.345 27.05 33.88 44.282 47.97 18.94-13.89 36.69-28.745 51.124-47.532-18.94-6.09-34.415-15.227-47.906-25.72zm250.125 0c-13.49 10.49-28.966 19.628-47.906 25.718 14.435 18.787 32.183 33.642 51.125 47.53 17.234-14.087 32.494-29.623 44.283-47.967-16.03-7.206-31.894-15.53-47.5-25.283zM76.125 403.125c1.866 23.033 3.352 44.754 3.75 64.22 11.39-8.415 22.977-16.29 34.313-24.25-14.114-11.857-27.09-24.923-38.063-39.97zm359.75 0c-10.973 15.047-23.95 28.113-38.063 39.97 11.336 7.96 22.924 15.835 34.313 24.25.398-19.466 1.884-41.187 3.75-64.22zm-252.156 2.063c-12.975 14.874-27.61 27.23-42.75 38.53 11.24 8.348 23.104 16.282 35.218 24.126.75-19.013 3.624-40.192 7.53-62.656zm144.56 0c3.908 22.464 6.783 43.643 7.533 62.656 12.113-7.844 23.977-15.778 35.218-24.125-15.14-11.303-29.775-23.658-42.75-38.533zm-200.967 48.375c-13.318 9.394-26.726 18.344-39.47 27.812 26.524 12.555 53.04 11.06 79.563-.156-13.63-8.813-27.194-17.85-40.094-27.658zm257.375 0c-12.9 9.808-26.464 18.844-40.094 27.656 26.523 11.214 53.04 12.71 79.562.155-12.743-9.468-26.15-18.418-39.47-27.813z"/></svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#b1b2b5" d="M186.438 20.56l-13.184 26.365c6.8-.26 13.626-.488 20.47-.686l3.84-7.68h116.874l3.77 7.54c6.838.187 13.658.408 20.456.66l-13.102-26.2H186.437zm69.56 42.742c-45.757.056-91.452 1.566-135.38 4.363-3.24 50.58-8.4 100.987-.786 145.824 89.297 12.395 180.102 12.985 272.764-.054 7.055-30.988 5.117-84.68-1.04-145.89-43.974-2.893-89.73-4.3-135.558-4.244zm153.783 5.54c6.42 64.12 9.113 119.825-1.135 155.22l-1.61 5.56-5.726.842c-98.8 14.528-195.613 13.81-290.605.002l-6.285-.914-1.246-6.23c-9.89-49.49-4.085-102.785-.664-154.42-4.89.354-9.765.72-14.602 1.107-8.596 58.568-9.39 116.957-.05 175.292 110.24 12.088 222.275 12.205 336.203-.01 8.502-57.83 8.29-116.25-.017-175.313-4.725-.4-9.485-.776-14.262-1.14zM255.966 92.3c32.526-.025 65.067 2.746 97.574 8.39l7.46 1.295v7.572c0 15.554 1.683 35.105-12.69 50.25-9.912 10.444-25.655 17.337-51.31 20.585v18.164h-82v-18.452c-23.992-3.37-39.352-10.175-49.363-20.185C150.807 145.093 151 125.56 151 109.56v-7.594l7.484-1.278c32.444-5.54 64.955-8.362 97.48-8.386zm.012 17.994c-28.96.022-57.913 2.444-86.858 6.996.265 12.28 1.635 22.296 9.243 29.904 5.914 5.914 16.952 11.416 36.637 14.582v-29.22h82v29.51c21.367-3.115 32.66-8.755 38.254-14.65 7.033-7.41 7.696-17.502 7.73-30.124-29-4.63-58.006-7.02-87.007-6.998zM233 150.56v30h46v-30h-46zm209.674 92.42c-.503 3.625-1.042 7.25-1.61 10.87.214 2.352.42 4.706.63 7.06L471 290.213v-22.24l-28.326-24.995zm-373.485.12L41 267.973v22.24l29.318-29.318c.205-2.327.406-4.655.616-6.982-.618-3.605-1.202-7.21-1.745-10.813zm354.634 20.397c-10.29 1.09-20.564 2.076-30.824 2.967v74.095h16v66h-16v80.615c10.318-.633 20.63-1.313 30.928-2.082 9.445-74.01 6.478-147.698-.104-221.596zm-335.576.03C81.725 338.09 78.58 412.1 88.06 485.1c10.324.79 20.638 1.504 30.94 2.145V406.56h-16v-66h16v-74.024c-10.266-.902-20.517-1.903-30.752-3.01zm286.752 4.4c-10.014.76-20.014 1.424-30 1.992v70.64h30v-72.632zm-238 .085v72.547h30v-70.55c-10.015-.568-20.014-1.237-30-1.997zm190 2.825c-47.65 2.173-94.984 2.19-142 .078v19.314c23.95-5.165 47.8-7.652 71.516-7.59 23.638.06 47.145 2.654 70.484 7.626v-19.43zM68.05 288.62L41 315.67v56.89h23.06c.376-27.987 1.88-55.975 3.99-83.94zm375.948.047c2.12 27.872 3.61 55.83 3.957 83.892H471v-56.89l-27.002-27.003zm-187.52 11.95c-23.68-.063-47.487 2.577-71.478 8.052v31.89h16v18.443c17.033 5.346 31.73 8.493 46 9.426v-2.87h18v2.868c14.27-.932 28.967-4.08 46-9.425V340.56h16v-31.866c-23.42-5.267-46.907-8.016-70.523-8.078zM121 358.558v30h22v-23h18v23h22v-30h-62zm208 0v30h22v-23h18v23h22v-30h-62zM201 377.8v28.76h-16v15.857c48.528 10.865 95.713 10.664 142 .045V406.56h-16V377.8c-16.332 4.747-31.283 7.52-46 8.326v11.433h-18v-11.434c-14.717-.806-29.668-3.58-46-8.326zM41 390.56v14h23.14c-.09-4.667-.143-9.334-.163-14H41zm407.012 0c-.027 4.663-.083 9.33-.18 14H471v-14h-22.988zM137 406.56v19.798c6.137 7.214 11.222 9.77 14.934 9.844 3.734.075 8.697-2.122 15.066-9.79V406.56h-6v7h-18v-7h-6zm208 0v19.798c6.137 7.214 11.222 9.77 14.934 9.844 3.734.075 8.697-2.122 15.066-9.79V406.56h-6v7h-18v-7h-6zm-304 16v35.154c5.596 5.51 8.677 8.25 11.846 9.306 2.454.818 7.713 1.15 15.045 1.317-1.544-15.25-2.586-30.51-3.204-45.778H41zm406.27 0c-.628 15.224-1.674 30.483-3.21 45.78 7.358-.168 12.635-.5 15.094-1.32 3.17-1.056 6.25-3.795 11.846-9.306V422.56h-23.73zM185 440.842v49.498c47.55 1.51 94.877 1.446 142-.074V440.9c-46.316 10.03-93.74 10.185-142-.057zm-48 9.123v38.318c10.01.54 20.01 1.008 30 1.408v-39.678c-4.86 2.786-10.01 4.293-15.43 4.184-5.192-.104-10.036-1.624-14.57-4.232zm208 0v39.654c10.01-.403 20.01-.878 30-1.412v-38.194c-4.86 2.786-10.01 4.293-15.43 4.184-5.192-.104-10.036-1.624-14.57-4.232z"/></svg>

Before

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#b1b2b5" d="M329.5 29.12l-8.1 11.4L359 67.16l8.1-11.44zm-88 5.04l24.2 45.36 1.8 1.29 14.8-40.36zm57.6 12.63l-16.4 44.8 40.7 28.81 35.3-31.54c-.9-.58-1.9-1.19-2.8-1.84zM59.83 48.56l10.84 45.83 29.63 2.6 2.7-29.63zM470.9 75.41c-5.6 4.71-12.2 8.59-19.5 11.74 5 46.45-14.7 83.45-45.2 109.75-26.5 22.9-60.9 38.4-95 47.9-2.5 4.8-5 9.2-7.4 13.1 41.5 5.4 93.2-21.2 129.2-60 19.8-21.3 34.8-45.9 41.1-69.2 5.2-19.4 4.7-37.42-3.2-53.29zm-351.3 8.71l-3 32.48-32.35-2.9 226.55 271 20-16.7 15.3-12.8zM434 93.09c-4.2 1-8.5 2-12.8 2.7-14.9 2.5-30.1 3.1-43.5.3l-41 36.61c4 7 5 15.7 4.5 24.5-.6 12.6-4.3 26.7-9.3 40.9-3 8.3-6.3 16.6-9.9 24.6 26.9-9.2 52.6-22.3 72.5-39.4 26.2-22.8 42.5-51.6 39.5-90.21zM274 107.4l-51.2 72.2 30.6 36.5 58.2-82.1zM173.8 248.8L34.53 445.2l37.53 26.6L204.3 285.3zm233 79.2L273.3 439.5l19.2 23.1L426 351zm-18.3 77.9l-35.3 29.4 39.7 47.6 35.3-29.4z"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 485 KiB

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.15761 1.28332C3.96537 1.15583 3.71087 1.17639 3.54159 1.3331C3.23088 1.62074 2.98798 2.00897 2.80986 2.34288C2.69323 2.27414 2.5689 2.21392 2.43729 2.1694C2.27798 2.11552 2.10225 2.14533 1.96963 2.24875C1.83701 2.35216 1.76526 2.51531 1.77868 2.68295C1.96117 4.96193 2.51766 7.79669 3.14534 10.1876C3.15716 10.2326 3.17507 10.2753 3.19825 10.3146C2.89349 10.543 2.6183 10.8624 2.48626 11.2592C2.45915 11.3406 2.45376 11.4266 2.46923 11.5092L1.01702 12.5343C0.971683 12.5663 0.93199 12.6056 0.899569 12.6506C0.521457 13.1759 0.404537 13.8064 0.579107 14.3656C0.635354 14.5458 0.788273 14.679 0.974476 14.7099C2.62972 14.9848 3.76851 14.6009 4.99802 13.9624C5.09724 13.9109 5.17356 13.8294 5.21926 13.7331C5.28208 13.7365 5.3457 13.7281 5.40668 13.7074L6.36918 13.3813C6.58065 13.3096 6.71897 13.1064 6.70811 12.8834L6.66986 12.0978C6.6651 11.9999 6.6317 11.9057 6.5738 11.8266C6.56495 11.8145 6.55602 11.8025 6.54703 11.7905C6.68693 11.6781 6.75773 11.4969 6.72648 11.3158C6.51037 10.0625 6.7448 8.79384 7.001 7.4073C7.02616 7.27116 7.05153 7.13387 7.0767 6.99535C7.35132 5.48411 7.59458 3.84747 7.06807 2.17953C6.98994 1.93202 6.73543 1.78545 6.48214 1.84209C6.12136 1.92278 5.70715 2.0529 5.33106 2.21214C5.27748 2.23483 5.22893 2.26639 5.1871 2.30496C5.15959 2.27265 5.13071 2.23811 5.09985 2.201C5.09087 2.1902 5.08173 2.17919 5.07242 2.16799C5.00591 2.0879 4.93138 1.99816 4.85218 1.90837C4.6703 1.7022 4.44556 1.47429 4.15761 1.28332ZM12.4584 1.33311C12.2891 1.1764 12.0346 1.15583 11.8424 1.28331C11.5544 1.47427 11.3296 1.70219 11.1478 1.90836C11.0685 1.99816 10.994 2.08791 10.9275 2.16801C10.9182 2.1792 10.9091 2.19021 10.9001 2.201C10.8692 2.2381 10.8404 2.27263 10.8129 2.30493C10.771 2.26634 10.7224 2.23476 10.6688 2.21207C10.2927 2.05285 9.87856 1.9228 9.51784 1.8421C9.26454 1.78544 9.01 1.93201 8.93187 2.17953C8.40536 3.84747 8.64862 5.48411 8.92324 6.99535C8.94841 7.13386 8.97378 7.27114 8.99893 7.40728C9.25513 8.79381 9.48956 10.0625 9.27343 11.3157C9.24218 11.4969 9.31299 11.6781 9.45292 11.7905C9.44392 11.8025 9.435 11.8145 9.42615 11.8266C9.36825 11.9056 9.33484 11.9999 9.33008 12.0978L9.29183 12.8834C9.28097 13.1064 9.41929 13.3096 9.63076 13.3813L10.5933 13.7074C10.6543 13.7281 10.7179 13.7365 10.7807 13.7331C10.8264 13.8294 10.9028 13.9109 11.002 13.9624C12.2315 14.6008 13.3703 14.9848 15.0255 14.7099C15.2117 14.679 15.3647 14.5458 15.4209 14.3656C15.5955 13.8064 15.4785 13.1759 15.1004 12.6506C15.068 12.6056 15.0283 12.5663 14.983 12.5343L13.5307 11.5091C13.5462 11.4266 13.5408 11.3406 13.5137 11.2592C13.3816 10.8624 13.1064 10.543 12.8017 10.3146C12.8248 10.2753 12.8428 10.2326 12.8546 10.1876C13.4823 7.79669 14.0388 4.9619 14.2212 2.68292C14.2347 2.51528 14.1629 2.35213 14.0303 2.24872C13.8977 2.14531 13.7219 2.11549 13.5626 2.16937C13.431 2.21389 13.3067 2.27411 13.1901 2.34286C13.0119 2.00898 12.769 1.62075 12.4584 1.33311Z" stroke="#666666" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -0,0 +1,3 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.504 1.63801L12.252 1.84501C11.652 2.34001 11.352 2.58801 11 2.58801C10.648 2.58801 10.348 2.34001 9.74799 1.84501L9.49599 1.63801C9.00899 1.23701 8.76599 1.03601 8.47499 1.00401C8.18299 0.973011 7.90499 1.11601 7.34699 1.40401L7.04399 1.55901C6.76699 1.70201 6.62799 1.77401 6.55299 1.91901C6.47799 2.06401 6.49499 2.20301 6.52999 2.48101C6.80599 4.70301 8.49099 6.79701 10.61 7.42601C10.735 7.47216 10.8668 7.49717 11 7.50001C11.1332 7.49717 11.265 7.47216 11.39 7.42601C13.509 6.79701 15.194 4.70301 15.47 2.48101C15.505 2.20301 15.522 2.06401 15.447 1.91901C15.372 1.77401 15.233 1.70201 14.957 1.55901L14.653 1.40301C14.096 1.11601 13.817 0.973011 13.525 1.00401C13.234 1.03601 12.991 1.23701 12.505 1.63801M6.47799 3.00001C4.39199 3.40701 2.99599 4.15001 1.71899 5.23001C1.34599 5.54501 1.15899 5.70301 1.07099 5.95401C0.780994 6.78001 1.41499 9.62101 2.19699 9.94901C2.68399 10.153 3.36499 9.73501 4.72799 8.89901C5.96499 8.14001 7.48299 7.35501 8.99999 6.93201M15.522 3.00001C17.608 3.40701 19.004 4.15001 20.281 5.23001C20.654 5.54501 20.841 5.70301 20.929 5.95401C21.219 6.78001 20.585 9.62101 19.803 9.94901C19.316 10.153 18.635 9.73501 17.271 8.89901C16.036 8.14001 14.518 7.35501 13 6.93201M17 9.00001L16.395 14.442C16.207 16.134 16.113 16.98 15.544 17.49C14.974 18 14.123 18 12.42 18H9.57999C7.87699 18 7.02599 18 6.45599 17.49C5.88599 16.98 5.79299 16.134 5.60499 14.442L4.99999 9.00001M14.385 18H7.61499C7.35999 18 7.23299 18 7.11199 18.014C6.67794 18.064 6.271 18.2506 5.94999 18.547C5.86099 18.628 5.77999 18.724 5.61699 18.915C5.29699 19.291 5.13599 19.479 5.07699 19.622C5.02428 19.7507 4.9991 19.8889 5.00309 20.0279C5.00709 20.1669 5.04016 20.3034 5.10017 20.4288C5.16019 20.5543 5.24582 20.6657 5.35155 20.756C5.45727 20.8462 5.58074 20.9134 5.71399 20.953C5.86399 21 6.11499 21 6.61599 21H15.384C15.884 21 16.135 21 16.286 20.953C16.4193 20.9136 16.5429 20.8467 16.6487 20.7565C16.7545 20.6664 16.8403 20.5551 16.9004 20.4297C16.9605 20.3044 16.9937 20.1678 16.9977 20.0289C17.0018 19.8899 16.9767 19.7516 16.924 19.623C16.864 19.479 16.704 19.291 16.384 18.915C16.22 18.724 16.139 18.628 16.05 18.547C15.7286 18.2512 15.3219 18.0646 14.888 18.014C14.767 18 14.64 18 14.385 18Z" stroke="#666666" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,3 @@
<svg width="20" height="22" viewBox="0 0 20 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.00001 19H12M10 7H10.009M2.53801 14.094C0.888011 7.61 3.99601 3.564 10 1C16.005 3.563 19.112 7.61 17.462 14.094C17.342 14.564 17.358 15.063 17.534 15.514L18.91 19.038C19.445 20.409 17.466 20.735 16.625 20.912C14.311 21.399 12.067 19.811 11.693 17.42C11.429 15.733 12.383 15.456 13.631 14.817C13.631 14.817 15.187 14.286 15.187 12.691C15.187 11.517 14.257 10.566 13.112 10.566C12.394 10.566 11.762 10.774 11.135 11.452C10.527 12.109 10.223 12.437 10 12.437C9.77701 12.437 9.47301 12.109 8.86601 11.452C8.23801 10.774 7.60601 10.566 6.88801 10.566C5.74201 10.566 4.81301 11.517 4.81301 12.691C4.81301 14.286 6.36901 14.817 6.36901 14.817C7.61701 15.456 8.57101 15.733 8.30701 17.42C7.93301 19.81 5.68901 21.4 3.37501 20.912C2.53401 20.735 0.555011 20.409 1.09101 19.038L2.46601 15.514C2.64201 15.063 2.65701 14.564 2.53801 14.094Z" stroke="#666666" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1010 B

View File

@ -0,0 +1,3 @@
<svg width="20" height="22" viewBox="0 0 20 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.16 4V3C16.16 2.057 16.16 1.586 15.86 1.293C15.559 1 15.075 1 14.107 1H5.89301C4.92501 1 4.44101 1 4.14001 1.293C3.83901 1.586 3.84001 2.057 3.84001 3V4M16.16 4H3.84001M16.16 4L17.6096 11M3.84001 4L2.39047 11M2.39047 11L1.78774 14.3265M2.39047 11C3.48635 11.3237 4.13173 12.9685 4.99991 13C6.07854 13.0392 7.37081 11.3877 8.76949 11M8.76949 11C9.1675 9.93337 9.99991 9 9.99991 9C9.99991 9 10.8327 9.93306 11.2308 11M8.76949 11C8.55307 11.58 8.31985 12.3176 8.03701 13.213L7.68545 14.3265M11.2308 11C11.4472 11.5799 11.6802 12.3175 11.963 13.213L12.3148 14.3265M11.2308 11C12.6566 11.3875 13.9154 13.041 14.9999 13C15.8598 12.9675 16.5386 11.3237 17.6096 11M17.6096 11L18.2123 14.326M1.78774 14.3265L1.18501 18.653C0.987006 19.741 0.889006 20.285 1.19701 20.643C1.50401 21 2.07201 21 3.20701 21H4.37601C5.10701 21 5.47401 21 5.74601 20.804C6.01801 20.608 6.12501 20.268 6.34001 19.588L7.68545 14.3265M1.78774 14.3265C2.76898 14.5762 3.5689 16.9715 4.37601 17C5.35607 17.0346 6.34671 14.6477 7.68545 14.3265M12.3148 14.3265L13.661 19.588C13.875 20.269 13.983 20.609 14.255 20.804C14.527 21 14.893 21 15.623 21H16.793C17.928 21 18.496 21 18.803 20.642C19.111 20.285 19.012 19.742 18.815 18.652L18.2123 14.326M12.3148 14.3265C13.6343 14.7183 14.621 16.5451 15.623 16.5C16.4184 16.4642 17.2235 14.6375 18.2123 14.326" stroke="#666666" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 335 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 453 KiB

View File

Before

Width:  |  Height:  |  Size: 598 KiB

After

Width:  |  Height:  |  Size: 598 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 470 KiB

View File

Before

Width:  |  Height:  |  Size: 471 KiB

After

Width:  |  Height:  |  Size: 471 KiB

View File

Before

Width:  |  Height:  |  Size: 301 KiB

After

Width:  |  Height:  |  Size: 301 KiB

View File

Before

Width:  |  Height:  |  Size: 400 KiB

After

Width:  |  Height:  |  Size: 400 KiB

View File

Before

Width:  |  Height:  |  Size: 470 KiB

After

Width:  |  Height:  |  Size: 470 KiB

View File

@ -1,5 +1,8 @@
<template>
<Notifications />
<GmTools v-if="gameStore.character?.role === 'gm'" />
<GmPanel v-if="gameStore.character?.role === 'gm'" />
<component :is="currentScreen" />
</template>
@ -7,9 +10,12 @@
import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import Notifications from '@/components/utilities/Notifications.vue'
import GmTools from '@/components/gameMaster/GmTools.vue'
import GmPanel from '@/components/gameMaster/GmPanel.vue'
import Login from '@/screens/Login.vue'
import Characters from '@/screens/Characters.vue'
import Game from '@/screens/Game.vue'
// import Loading from '@/screens/Loading.vue'
import ZoneEditor from '@/screens/ZoneEditor.vue'
import { computed } from 'vue'
@ -17,13 +23,11 @@ const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore()
const currentScreen = computed(() => {
// if (!gameStore.isAssetsLoaded) return Loading
if (!gameStore.connection) return Login
if (!gameStore.token) return Login
if (!gameStore.character) return Characters
if (zoneEditorStore.active) return ZoneEditor
return Game
})
// Disable right click
addEventListener('contextmenu', (event) => event.preventDefault())
</script>

View File

@ -20,6 +20,9 @@ body {
-webkit-user-select: none; /* Safari */
-ms-user-select: none; /* IE 10 and IE 11 */
user-select: none; /* Standard syntax */
// Disable pinch zoom
touch-action: pan-x pan-y;
}
h1,
@ -129,3 +132,12 @@ button {
::-webkit-scrollbar {
@apply hidden;
}
canvas {
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-crisp-edges;
image-rendering: pixelated;
position: fixed;
left: 0;
top: 0;
}

View File

@ -4,26 +4,18 @@
<script setup lang="ts">
import { Scene } from 'phavuer'
import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { onBeforeMount, onBeforeUnmount, ref } from 'vue'
import { useZoneStore } from '@/stores/zoneStore'
import { onBeforeUnmount, ref, watch } from 'vue'
const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore()
// See if there's a dat
const zoneStore = useZoneStore()
const sceneRef = ref<Phaser.Scene | null>(null)
// Effect-related refs
const dayNightCycle = ref<Phaser.GameObjects.Graphics | null>(null)
const lightEffect = ref<Phaser.GameObjects.Graphics | null>(null)
const rainEmitter = ref<Phaser.GameObjects.Particles.ParticleEmitter | null>(null)
const fogSprite = ref<Phaser.GameObjects.Sprite | null>(null)
// Effect parameters
const dayNightDuration = 300000 // 5 minutes in milliseconds
const maxDarkness = 0.7
const preloadScene = async (scene: Phaser.Scene) => {
scene.load.image('raindrop', 'assets/raindrop.png')
scene.load.image('fog', 'assets/fog.png')
@ -31,28 +23,18 @@ const preloadScene = async (scene: Phaser.Scene) => {
const createScene = async (scene: Phaser.Scene) => {
sceneRef.value = scene
createDayNightCycle(scene)
createLightEffect(scene)
createRainEffect(scene)
createFogEffect(scene)
}
const updateScene = (scene: Phaser.Scene, time: number) => {
updateDayNightCycle(time)
updateFogEffect()
const updateScene = () => {
updateEffects()
}
const createDayNightCycle = (scene: Phaser.Scene) => {
dayNightCycle.value = scene.add.graphics()
dayNightCycle.value.setDepth(1000)
}
const updateDayNightCycle = (time: number) => {
if (!dayNightCycle.value) return
const darkness = Math.sin(((time % dayNightDuration) / dayNightDuration) * Math.PI) * maxDarkness
dayNightCycle.value.clear()
dayNightCycle.value.fillStyle(0x000000, darkness)
dayNightCycle.value.fillRect(0, 0, window.innerWidth, window.innerHeight)
const createLightEffect = (scene: Phaser.Scene) => {
lightEffect.value = scene.add.graphics()
lightEffect.value.setDepth(1000)
}
const createRainEffect = (scene: Phaser.Scene) => {
@ -67,13 +49,7 @@ const createRainEffect = (scene: Phaser.Scene) => {
blendMode: 'ADD'
})
rainEmitter.value.setDepth(900)
toggleRain(true) // Start with rain off
}
const toggleRain = (isRaining: boolean) => {
if (rainEmitter.value) {
rainEmitter.value.setVisible(isRaining)
}
rainEmitter.value.stop()
}
const createFogEffect = (scene: Phaser.Scene) => {
@ -83,28 +59,52 @@ const createFogEffect = (scene: Phaser.Scene) => {
fogSprite.value.setDepth(950)
}
const updateFogEffect = () => {
if (fogSprite.value) {
// Example: Oscillate fog opacity
const fogOpacity = ((Math.sin(Date.now() / 5000) + 1) / 2) * 0.3
fogSprite.value.setAlpha(fogOpacity)
}
}
const updateEffects = () => {
const effects = zoneStore.zone?.zoneEffects || []
// Expose methods to control effects
const controlEffects = {
toggleRain,
setFogDensity: (density: number) => {
if (fogSprite.value) {
fogSprite.value.setAlpha(density)
effects.forEach((effect) => {
switch (effect.effect) {
case 'light':
updateLightEffect(effect.strength)
break
case 'rain':
updateRainEffect(effect.strength)
break
case 'fog':
updateFogEffect(effect.strength)
break
}
})
}
const updateLightEffect = (strength: number) => {
if (!lightEffect.value) return
const darkness = 1 - strength / 100
lightEffect.value.clear()
lightEffect.value.fillStyle(0x000000, darkness)
lightEffect.value.fillRect(0, 0, window.innerWidth, window.innerHeight)
}
const updateRainEffect = (strength: number) => {
if (!rainEmitter.value) return
if (strength > 0) {
rainEmitter.value.start()
rainEmitter.value.setQuantity(Math.floor((strength / 100) * 10))
} else {
rainEmitter.value.stop()
}
}
// Make control methods available to parent components
defineExpose(controlEffects)
const updateFogEffect = (strength: number) => {
if (!fogSprite.value) return
fogSprite.value.setAlpha(strength / 100)
}
watch(() => zoneStore.zone?.zoneEffects, updateEffects, { deep: true })
onBeforeUnmount(() => {
if (sceneRef.value) sceneRef.value.scene.remove('effects')
})
// @TODO : Fix resize issue
</script>

View File

@ -1,10 +1,10 @@
<template>
<div class="flex flex-wrap items-center input-field gap-1">
<div v-for="(chip, i) in internalValue" :key="i" class="flex gap-2.5 items-center bg-cyan rounded py-1 px-2">
<span class="text-xs">{{ chip }}</span>
<span class="text-xs text-white">{{ chip }}</span>
<button type="button" class="text-xs cursor-pointer text-white font-light font-default not-italic hover:text-gray-50" @click="deleteChip(i)" aria-label="Remove chip">×</button>
</div>
<input class="outline-none border-none p-1" placeholder="Tag name" v-model="currentInput" @keypress.enter.prevent="addChip" @keydown.backspace="handleBackspace" />
<input class="outline-none border-none p-1 text-gray-300" placeholder="Tag name" v-model="currentInput" @keypress.enter.prevent="addChip" @keydown.backspace="handleBackspace" />
</div>
</template>

View File

@ -1,7 +1,6 @@
<template>
<Modal :isModalOpen="gameStore.uiSettings.isGmPanelOpen" @modal:close="() => gameStore.toggleGmPanel()" :modal-width="1000" :modal-height="650" :can-full-screen="true">
<template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">GM Panel</h3>
<div class="flex gap-1.5 flex-wrap">
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">General</button>
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Users</button>

View File

@ -1,17 +1,17 @@
<template>
<div class="flex h-full w-full relative">
<div class="w-2/12 flex flex-col relative">
<div class="w-2/12 flex flex-col relative overflow-auto">
<!-- Asset Categories -->
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'tiles' }" @click="() => (selectedCategory = 'tiles')">
<span>Tiles</span>
<span :class="{ 'text-white': selectedCategory === 'tiles' }">Tiles</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': selectedCategory === 'objects' }" @click="() => (selectedCategory = 'objects')">
<span>Objects</span>
<span :class="{ 'text-white': selectedCategory === 'objects' }">Objects</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': selectedCategory === 'sprites' }" @click="() => (selectedCategory = 'sprites')">
<span>Sprites</span>
<span :class="{ 'text-white': selectedCategory === 'sprites' }">Sprites</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">
@ -22,8 +22,16 @@
<span>NPC's</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">
<span>Characters</span>
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'shops' }" @click="() => (selectedCategory = 'shops')">
<span :class="{ 'text-white': selectedCategory === 'shops' }">Shops</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': selectedCategory === 'characterTypes' }" @click="() => (selectedCategory = 'characterTypes')">
<span :class="{ 'text-white': selectedCategory === 'characterTypes' }">Character types</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': selectedCategory === 'characterHair' }" @click="() => (selectedCategory = 'characterHair')">
<span :class="{ 'text-white': selectedCategory === 'characterHair' }">Character hair</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">
@ -34,6 +42,10 @@
<span>Pets</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">
<span>Emoticons</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>
@ -42,6 +54,7 @@
<TileList v-if="selectedCategory === 'tiles'" />
<ObjectList v-if="selectedCategory === 'objects'" />
<SpriteList v-if="selectedCategory === 'sprites'" />
<CharacterTypeList v-if="selectedCategory === 'characterTypes'" />
</div>
<div class="absolute w-px bg-gray-500 h-full top-0 left-1/2"></div>
@ -51,6 +64,7 @@
<TileDetails v-if="selectedCategory === 'tiles' && assetManagerStore.selectedTile" />
<ObjectDetails v-if="selectedCategory === 'objects' && assetManagerStore.selectedObject" />
<SpriteDetails v-if="selectedCategory === 'sprites' && assetManagerStore.selectedSprite" />
<CharacterTypeDetails v-if="selectedCategory === 'characterTypes' && assetManagerStore.selectedCharacterType" />
</div>
</div>
</template>
@ -64,6 +78,8 @@ import ObjectList from '@/components/gameMaster/assetManager/partials/object/Obj
import ObjectDetails from '@/components/gameMaster/assetManager/partials/object/ObjectDetails.vue'
import SpriteList from '@/components/gameMaster/assetManager/partials/sprite/SpriteList.vue'
import SpriteDetails from '@/components/gameMaster/assetManager/partials/sprite/SpriteDetails.vue'
import CharacterTypeList from '@/components/gameMaster/assetManager/partials/characterType/CharacterTypeList.vue'
import CharacterTypeDetails from '@/components/gameMaster/assetManager/partials/characterType/CharacterTypeDetails.vue'
const assetManagerStore = useAssetManagerStore()
const selectedCategory = ref('tiles')

View File

@ -0,0 +1,117 @@
<template>
<div class="h-full overflow-auto">
<div class="m-2.5 p-2.5 block">
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveCharacterType">
<div class="form-field-full">
<label for="name">Name</label>
<input v-model="characterName" class="input-field" type="text" name="name" placeholder="Character Type Name" />
</div>
<div class="form-field-half">
<label for="gender">Gender</label>
<select v-model="characterGender" class="input-field" name="gender">
<option v-for="gender in genderOptions" :key="gender" :value="gender">{{ gender }}</option>
</select>
</div>
<div class="form-field-half">
<label for="race">Race</label>
<select v-model="characterRace" class="input-field" name="race">
<option v-for="race in raceOptions" :key="race" :value="race">{{ race }}</option>
</select>
</div>
<div class="form-field-full">
<label for="spriteId">Sprite ID</label>
<input v-model="characterSpriteId" class="input-field" type="text" name="spriteId" placeholder="Sprite ID" />
</div>
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeCharacterType">Remove</button>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import type { CharacterType, CharacterGender, CharacterRace } from '@/types'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore'
const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore()
const selectedCharacterType = computed(() => assetManagerStore.selectedCharacterType)
const characterName = ref('')
const characterGender = ref<CharacterGender>('MALE' as CharacterGender.MALE)
const characterRace = ref<CharacterRace>('HUMAN' as CharacterRace.HUMAN)
const characterSpriteId = ref('')
const genderOptions: CharacterGender[] = ['MALE' as CharacterGender.MALE, 'FEMALE' as CharacterGender.FEMALE]
const raceOptions: CharacterRace[] = ['HUMAN' as CharacterRace.HUMAN, 'ELF' as CharacterRace.ELF, 'DWARF' as CharacterRace.DWARF, 'ORC' as CharacterRace.ORC, 'GOBLIN' as CharacterRace.GOBLIN]
if (!selectedCharacterType.value) {
console.error('No character type selected')
}
if (selectedCharacterType.value) {
characterName.value = selectedCharacterType.value.name
characterGender.value = selectedCharacterType.value.gender
characterRace.value = selectedCharacterType.value.race
characterSpriteId.value = selectedCharacterType.value.spriteId
}
function removeCharacterType() {
if (!selectedCharacterType.value) return
gameStore.connection?.emit('gm:characterType:remove', { id: selectedCharacterType.value.id }, (response: boolean) => {
if (!response) {
console.error('Failed to remove character type')
return
}
refreshCharacterTypeList()
})
}
function refreshCharacterTypeList(unsetSelectedCharacterType = true) {
gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => {
assetManagerStore.setCharacterTypeList(response)
if (unsetSelectedCharacterType) {
assetManagerStore.setSelectedCharacterType(null)
}
})
}
function saveCharacterType() {
const characterTypeData = {
id: selectedCharacterType.value?.id,
name: characterName.value,
gender: characterGender.value,
race: characterRace.value,
spriteId: characterSpriteId.value
}
gameStore.connection?.emit('gm:characterType:update', characterTypeData, (response: boolean) => {
if (!response) {
console.error('Failed to save character type')
return
}
refreshCharacterTypeList(false)
})
}
watch(selectedCharacterType, (characterType: CharacterType | null) => {
if (!characterType) return
characterName.value = characterType.name
characterGender.value = characterType.gender
characterRace.value = characterType.race
characterSpriteId.value = characterType.spriteId
})
onMounted(() => {
if (!selectedCharacterType.value) return
})
onBeforeUnmount(() => {
assetManagerStore.setSelectedCharacterType(null)
})
</script>

View File

@ -0,0 +1,93 @@
<template>
<div class="relative p-2.5 flex items-center gap-x-2.5">
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
<label for="create-character" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
<button class="p-0 h-5" id="create-character" @click="createNewCharacterType">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="white">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
</button>
</label>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</div>
<div v-bind="containerProps" class="overflow-y-auto relative" @scroll="onScroll">
<div v-bind="wrapperProps" ref="elementToScroll">
<a v-for="{ data: characterType } in list" :key="characterType.id" class="relative p-2.5 cursor-pointer block" :class="{ 'bg-cyan/80': assetManagerStore.selectedCharacterType?.id === characterType.id }" @click="assetManagerStore.setSelectedCharacterType(characterType as CharacterType)">
<div class="flex items-center gap-2.5">
<span>{{ characterType.name }}</span>
</div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
</div>
<button class="left-[calc(50%_-_60px)] fixed bottom-2.5 min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
<img class="absolute invert w-8 h-8 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
</button>
</div>
</template>
<script setup lang="ts">
import { useGameStore } from '@/stores/gameStore'
import { onMounted, ref, computed } from 'vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import type { CharacterType } from '@/types'
import { useVirtualList } from '@vueuse/core'
const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore()
const searchQuery = ref('')
const hasScrolled = ref(false)
const elementToScroll = ref()
const handleSearch = () => {
// Trigger a re-render of the virtual list
virtualList.value?.scrollTo(0)
}
const createNewCharacterType = () => {
gameStore.connection?.emit('gm:characterType:create', {}, (response: boolean) => {
if (!response) {
console.error('Failed to create new character type')
return
}
gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => {
assetManagerStore.setCharacterTypeList(response)
})
})
}
const filteredCharacterTypes = computed(() => {
if (!searchQuery.value) {
return assetManagerStore.characterTypeList
}
return assetManagerStore.characterTypeList.filter((character) => character.name.toLowerCase().includes(searchQuery.value.toLowerCase()))
})
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(filteredCharacterTypes, {
itemHeight: 48
})
const virtualList = ref({ scrollTo })
const onScroll = () => {
let scrollTop = elementToScroll.value.style.marginTop.replace('px', '')
if (scrollTop > 80) {
hasScrolled.value = true
} else if (scrollTop <= 80) {
hasScrolled.value = false
}
}
function toTop() {
virtualList.value?.scrollTo(0)
}
onMounted(() => {
gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => {
assetManagerStore.setCharacterTypeList(response)
})
})
</script>

View File

@ -118,7 +118,6 @@ function saveObject() {
return
}
console.log(objectTags.value)
gameStore.connection?.emit(
'gm:object:update',
{

View File

@ -1,7 +1,7 @@
<template>
<div class="relative p-2.5 flex items-center gap-x-2.5">
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
<label for="upload-asset" class="bg-cyan/50 border border-solid border-white/25 rounded drop-shadow-20 p-2 inline-flex items-center justify-center hover:bg-cyan hover:cursor-pointer">
<label for="upload-asset" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
<input class="hidden" id="upload-asset" ref="objectUploadField" type="file" accept="image/png" multiple @change="handleFileUpload" />
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
@ -21,7 +21,7 @@
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
</div>
<button class="left-[calc(50%_-_60px)] fixed bottom-2.5 min-w-[unset] w-12 h-12 rounded-md bg-cyan/50 p-0 hover:bg-cyan" v-show="hasScrolled" @click="toTop">
<button class="left-[calc(50%_-_60px)] fixed bottom-2.5 min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
<img class="absolute invert w-8 h-8 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
</button>
</div>

View File

@ -1,7 +1,7 @@
<template>
<div class="relative p-2.5 flex items-center gap-x-2.5">
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
<button @click.prevent="newButtonClickHandler" class="bg-cyan/50 border border-solid border-white/25 rounded drop-shadow-20 p-2 inline-flex items-center justify-center hover:bg-cyan hover:cursor-pointer">
<button @click.prevent="newButtonClickHandler" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
@ -17,7 +17,7 @@
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
</div>
<button class="left-[calc(50%_-_60px)] fixed bottom-2.5 min-w-[unset] w-12 h-12 rounded-md bg-cyan/50 p-0 hover:bg-cyan" v-show="hasScrolled" @click="toTop">
<button class="left-[calc(50%_-_60px)] fixed bottom-2.5 min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
<img class="absolute invert w-8 h-8 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
</button>
</div>

View File

@ -1,7 +1,7 @@
<template>
<div class="relative p-2.5 flex items-center gap-x-2.5">
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
<label for="upload-asset" class="bg-cyan/50 border border-solid border-white/25 rounded drop-shadow-20 p-2 inline-flex items-center justify-center hover:bg-cyan hover:cursor-pointer">
<label for="upload-asset" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
<input class="hidden" id="upload-asset" ref="tileUploadField" type="file" accept="image/png" multiple @change="handleFileUpload" />
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
@ -21,7 +21,7 @@
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
</div>
<button class="left-[calc(50%_-_60px)] fixed bottom-2.5 min-w-[unset] w-12 h-12 rounded-md bg-cyan/50 p-0 hover:bg-cyan" v-show="hasScrolled" @click="toTop">
<button class="left-[calc(50%_-_60px)] fixed bottom-2.5 min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
<img class="absolute invert w-8 h-8 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
</button>
</div>

View File

@ -1,14 +1,118 @@
<template></template>
<template>
<Image v-for="tile in zoneEditorStore.zone?.zoneEventTiles" v-bind="getImageProps(tile)" />
</template>
<script setup lang="ts">
import type { ZoneEventTile } from '@/types'
import { tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
import { type ZoneEventTile, ZoneEventTileType } from '@/types'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { Image, useScene } from 'phavuer'
import { getTile, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
import { uuidv4 } from '@/utilities'
import { onBeforeMount, onBeforeUnmount } from 'vue'
// function getEventTileImageProps(tile: ZoneEventTile) {
// return {
// x: tileToWorldX(zoneTilemap as any, tile.positionX, tile.positionY),
// y: tileToWorldY(zoneTilemap as any, tile.positionX, tile.positionY),
// texture: tile.type
// }
// }
const scene = useScene()
const zoneEditorStore = useZoneEditorStore()
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
}>()
function getImageProps(tile: ZoneEventTile) {
return {
x: tileToWorldX(props.tilemap, tile.positionX, tile.positionY),
y: tileToWorldY(props.tilemap, tile.positionX, tile.positionY),
texture: tile.type,
depth: 999
}
}
function pencil(pointer: Phaser.Input.Pointer) {
// Check if zone is set
if (!zoneEditorStore.zone) return
// Check if tool is pencil
if (zoneEditorStore.tool !== 'pencil') return
// Check if draw mode is blocking tile or teleport
if (zoneEditorStore.drawMode !== 'blocking tile' && zoneEditorStore.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
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if event tile already exists on position
const existingEventTile = zoneEditorStore.zone.zoneEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y)
if (existingEventTile) return
// If teleport, check if there is a selected zone
if (zoneEditorStore.drawMode === 'teleport' && !zoneEditorStore.teleportSettings.toZoneId) return
const newEventTile = {
id: uuidv4(),
zoneId: zoneEditorStore.zone.id,
zone: zoneEditorStore.zone,
type: zoneEditorStore.drawMode === 'blocking tile' ? ZoneEventTileType.BLOCK : ZoneEventTileType.TELEPORT,
positionX: tile.x,
positionY: tile.y,
teleport:
zoneEditorStore.drawMode === 'teleport'
? {
toZoneId: zoneEditorStore.teleportSettings.toZoneId,
toPositionX: zoneEditorStore.teleportSettings.toPositionX,
toPositionY: zoneEditorStore.teleportSettings.toPositionY,
toRotation: zoneEditorStore.teleportSettings.toRotation
}
: undefined
}
zoneEditorStore.zone.zoneEventTiles = zoneEditorStore.zone.zoneEventTiles.concat(newEventTile as ZoneEventTile)
}
function eraser(pointer: Phaser.Input.Pointer) {
// Check if zone is set
if (!zoneEditorStore.zone) return
// Check if tool is pencil
if (zoneEditorStore.tool !== 'eraser') return
// Check if draw mode is blocking tile or teleport
if (zoneEditorStore.eraserMode !== 'blocking tile' && zoneEditorStore.eraserMode !== '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
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if event tile already exists on position
const existingEventTile = zoneEditorStore.zone.zoneEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y)
if (!existingEventTile) return
// Remove existing event tile
zoneEditorStore.zone.zoneEventTiles = zoneEditorStore.zone.zoneEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id)
}
onBeforeMount(() => {
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)
})
onBeforeUnmount(() => {
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>

View File

@ -1,10 +1,6 @@
<template>
<SelectedZoneObject v-if="selectedZoneObject" :zoneObject="selectedZoneObject" />
<Image
v-for="object in zoneEditorStore.zone?.zoneObjects"
v-bind="getObjectImageProps(object)"
@pointerup="() => selectedZoneObject = object"
/>
<SelectedZoneObject v-if="selectedZoneObject" :zoneObject="selectedZoneObject" @move="moveZoneObject" @rotate="rotateZoneObject" @delete="deleteZoneObject" />
<Image v-for="object in zoneEditorStore.zone?.zoneObjects" v-bind="getImageProps(object)" @pointerup="() => (selectedZoneObject = object)" />
</template>
<script setup lang="ts">
@ -19,26 +15,28 @@ import { onBeforeMount, onBeforeUnmount, ref, watch } from 'vue'
const scene = useScene()
const zoneEditorStore = useZoneEditorStore()
const selectedZoneObject = ref<ZoneObject | null>(null)
const movingZoneObject = ref<ZoneObject | null>(null)
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
}>()
function getObjectImageProps(object: ZoneObject) {
function getImageProps(zoneObject: ZoneObject) {
return {
// alpha: object.id === movingZoneObject.value?.id ? .5 : 1,
depth: calculateIsometricDepth(object.positionX, object.positionY, object.object.frameWidth, object.object.frameHeight),
tint: selectedZoneObject.value?.id === object.id ? 0x00ff00 : 0xffffff,
x: tileToWorldX(props.tilemap as any, object.positionX, object.positionY),
y: tileToWorldY(props.tilemap as any, object.positionX, object.positionY),
flipX: object.isRotated,
texture: object.object.id,
originY: Number(object.object.originX),
originX: Number(object.object.originY)
alpha: zoneObject.id === movingZoneObject.value?.id ? 0.5 : 1,
depth: calculateIsometricDepth(zoneObject.positionX, zoneObject.positionY, zoneObject.object.frameWidth, zoneObject.object.frameHeight),
tint: selectedZoneObject.value?.id === zoneObject.id ? 0x00ff00 : 0xffffff,
x: tileToWorldX(props.tilemap, zoneObject.positionX, zoneObject.positionY),
y: tileToWorldY(props.tilemap, zoneObject.positionX, zoneObject.positionY),
flipX: zoneObject.isRotated,
texture: zoneObject.object.id,
originY: Number(zoneObject.object.originX),
originX: Number(zoneObject.object.originY)
}
}
function addZoneObject(pointer: Phaser.Input.Pointer) {
function pencil(pointer: Phaser.Input.Pointer) {
// Check if zone is set
if (!zoneEditorStore.zone) return
// Check if tool is pencil
@ -47,16 +45,19 @@ function addZoneObject(pointer: Phaser.Input.Pointer) {
// Check if draw mode is object
if (zoneEditorStore.drawMode !== 'object') return
// Check if there is a selected object
if (!zoneEditorStore.selectedObject) return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if there is a tile @TODO chekc if props.tilemap words
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if there is a tile
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if there is a selected object
if (!zoneEditorStore.selectedObject) return
// Check if object already exists on position
const existingObject = zoneEditorStore.zone?.zoneObjects.find((object) => object.positionX === tile.x && object.positionY === tile.y)
if (existingObject) return
@ -65,6 +66,7 @@ function addZoneObject(pointer: Phaser.Input.Pointer) {
id: uuidv4(),
zoneId: zoneEditorStore.zone.id,
zone: zoneEditorStore.zone,
objectId: zoneEditorStore.selectedObject.id,
object: zoneEditorStore.selectedObject,
depth: 0,
isRotated: false,
@ -76,26 +78,105 @@ function addZoneObject(pointer: Phaser.Input.Pointer) {
zoneEditorStore.zone.zoneObjects = zoneEditorStore.zone.zoneObjects.concat(newObject as ZoneObject)
}
function eraser(pointer: Phaser.Input.Pointer) {
// Check if zone is set
if (!zoneEditorStore.zone) return
// Check if tool is eraser
if (zoneEditorStore.tool !== 'eraser') return
// Check if draw mode is object
if (zoneEditorStore.eraserMode !== '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 there is a tile
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if object already exists on position
const existingObject = zoneEditorStore.zone.zoneObjects.find((object) => object.positionX === tile.x && object.positionY === tile.y)
if (!existingObject) return
// Remove existing object
zoneEditorStore.zone.zoneObjects = zoneEditorStore.zone.zoneObjects.filter((object) => object.id !== existingObject.id)
}
function moveZoneObject(id: string) {
// Check if zone is set
if (!zoneEditorStore.zone) return
movingZoneObject.value = zoneEditorStore.zone.zoneObjects.find((object) => object.id === id) as ZoneObject
function handlePointerMove(pointer: Phaser.Input.Pointer) {
if (!movingZoneObject.value) return
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
movingZoneObject.value.positionX = tile.x
movingZoneObject.value.positionY = tile.y
}
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
function handlePointerUp() {
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
movingZoneObject.value = null
}
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
}
function rotateZoneObject(id: string) {
// Check if zone is set
if (!zoneEditorStore.zone) return
zoneEditorStore.zone.zoneObjects = zoneEditorStore.zone.zoneObjects.map((object) => {
if (object.id === id) {
return {
...object,
isRotated: !object.isRotated
}
}
return object
})
}
function deleteZoneObject(id: string) {
// Check if zone is set
if (!zoneEditorStore.zone) return
zoneEditorStore.zone.zoneObjects = zoneEditorStore.zone.zoneObjects.filter((object) => object.id !== id)
selectedZoneObject.value = null
}
onBeforeMount(() => {
scene.input.on(Phaser.Input.Events.POINTER_DOWN, addZoneObject)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, addZoneObject)
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)
})
onBeforeUnmount(() => {
scene.input.off(Phaser.Input.Events.POINTER_DOWN, addZoneObject)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, addZoneObject)
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)
})
// watch zoneEditorStore.objectList and update originX and originY of objects in zoneObjects
watch(
zoneEditorStore.objectList,
() => zoneEditorStore.objectList,
(newObjects) => {
// Check if zoneEditorStore.zone is set
if (!zoneEditorStore.zone) return
// Update zoneObjects
zoneEditorStore.zone.zoneObjects = zoneEditorStore.zone.zoneObjects.map((zoneObject) => {
const updatedObject = newObjects.find((obj) => obj.id === zoneObject.objectId)
const updatedZoneObjects = zoneEditorStore.zone.zoneObjects.map((zoneObject) => {
const updatedObject = newObjects.find((obj) => obj.id === zoneObject.object.id)
if (updatedObject) {
return {
...zoneObject,
@ -109,6 +190,12 @@ watch(
return zoneObject
})
// Update the zone with the new zoneObjects
zoneEditorStore.setZone({
...zoneEditorStore.zone,
zoneObjects: updatedZoneObjects
})
// Update selectedObject if it's set
if (zoneEditorStore.selectedObject) {
const updatedObject = newObjects.find((obj) => obj.id === zoneEditorStore.selectedObject?.id)

View File

@ -8,7 +8,7 @@ import { useScene } from 'phavuer'
import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { onBeforeMount, onBeforeUnmount } from 'vue'
import { getTile, placeTile, setAllTiles } from '@/composables/zoneComposable'
import { createTileArray, getTile, placeTile, setLayerTiles } from '@/composables/zoneComposable'
import Controls from '@/components/utilities/Controls.vue'
const emit = defineEmits(['tilemap:create'])
@ -19,7 +19,6 @@ const zoneEditorStore = useZoneEditorStore()
const zoneTilemap = createTilemap()
const tiles = createTileLayer()
let tileArray = createTileArray()
function createTilemap() {
const zoneData = new Phaser.Tilemaps.MapData({
@ -47,42 +46,98 @@ function createTileLayer() {
return layer
}
function createTileArray() {
return Array.from({ length: zoneTilemap.height || 0 }, () => Array.from({ length: zoneTilemap.width || 0 }, () => 'blank_tile'))
}
function pencil(pointer: Phaser.Input.Pointer) {
// Check if zone is set
if (!zoneEditorStore.zone) return
function handleTileClick(pointer: Phaser.Input.Pointer) {
// Check if tool is pencil
if (zoneEditorStore.tool !== 'pencil') return
// Check if draw mode is tile
if (zoneEditorStore.drawMode !== 'tile') return
// Check if there is a selected tile
if (!zoneEditorStore.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 there is a tile
const tile = getTile(tiles, pointer.worldX, pointer.worldY)
if (!tile) return
// Place tile
placeTile(zoneTilemap, tiles, tile.x, tile.y, zoneEditorStore.selectedTile.id)
// Adjust zoneEditorStore.zone.tiles
zoneEditorStore.zone.tiles[tile.y][tile.x] = zoneEditorStore.selectedTile.id
}
function eraser(pointer: Phaser.Input.Pointer) {
// Check if zone is set
if (!zoneEditorStore.zone) return
// Check if tool is pencil
if (zoneEditorStore.tool !== 'eraser') return
// Check if draw mode is tile
if (zoneEditorStore.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 there is a tile
const tile = getTile(tiles, pointer.worldX, pointer.worldY)
if (!tile) return
// Place tile
placeTile(zoneTilemap, tiles, tile.x, tile.y, 'blank_tile')
// Adjust zoneEditorStore.zone.tiles
zoneEditorStore.zone.tiles[tile.y][tile.x] = 'blank_tile'
}
function paint(pointer: Phaser.Input.Pointer) {
// Check if zone is set
if (!zoneEditorStore.zone) return
// Check if tool is pencil
if (zoneEditorStore.tool !== 'paint') return
// Check if there is a selected tile
if (!zoneEditorStore.selectedTile) return
placeTile(zoneTilemap, tiles, tile.x, tile.y, zoneEditorStore.selectedTile.id)
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Set new tileArray with selected tile
setLayerTiles(zoneTilemap, tiles, createTileArray(zoneTilemap.width, zoneTilemap.height, zoneEditorStore.selectedTile.id))
// Adjust zoneEditorStore.zone.tiles
zoneEditorStore.zone.tiles = createTileArray(zoneTilemap.width, zoneTilemap.height, zoneEditorStore.selectedTile.id)
}
onBeforeMount(() => {
if (!zoneEditorStore.zone?.tiles) {
return
}
setAllTiles(zoneTilemap, tiles, zoneEditorStore.zone.tiles)
tileArray = zoneEditorStore.zone.tiles.map((row) => row.map((tileId) => tileId || 'blank_tile'))
setLayerTiles(zoneTilemap, tiles, zoneEditorStore.zone.tiles)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handleTileClick)
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)
})
onBeforeUnmount(() => {
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handleTileClick)
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)
zoneTilemap.destroyLayer('tiles')
zoneTilemap.removeAllLayers()

View File

@ -14,12 +14,10 @@
</template>
<script setup lang="ts">
import { onBeforeMount, onUnmounted, ref } from 'vue'
import { useScene } from 'phavuer'
import { onUnmounted, ref } from 'vue'
import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { loadAssets } from '@/composables/zoneComposable'
import { type ZoneObject, type ZoneEventTile, type Zone } from '@/types'
import { type Zone } from '@/types'
// Components
import Toolbar from '@/components/gameMaster/zoneEditor/partials/Toolbar.vue'
@ -32,14 +30,10 @@ import Tiles from '@/components/gameMaster/zoneEditor/Tiles.vue'
import Objects from '@/components/gameMaster/zoneEditor/Objects.vue'
import EventTiles from '@/components/gameMaster/zoneEditor/EventTiles.vue'
const scene = useScene()
const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore()
const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null)
const tileArray = ref<string[][]>([])
const zoneObjects = ref<ZoneObject[]>([])
const zoneEventTiles = ref<ZoneEventTile[]>([])
function save() {
if (!zoneEditorStore.zone) return
@ -49,10 +43,11 @@ function save() {
name: zoneEditorStore.zoneSettings.name,
width: zoneEditorStore.zoneSettings.width,
height: zoneEditorStore.zoneSettings.height,
tiles: tileArray,
tiles: zoneEditorStore.zone.tiles,
pvp: zoneEditorStore.zone.pvp,
zoneEventTiles: zoneEventTiles.value.map(({ id, zoneId, type, positionX, positionY, teleport }) => ({ id, zoneId, type, positionX, positionY, teleport })),
zoneObjects: zoneObjects.value.map(({ id, zoneId, objectId, depth, isRotated, positionX, positionY }) => ({ id, zoneId, objectId, depth, isRotated, positionX, positionY }))
zoneEffects: zoneEditorStore.zone.zoneEffects.map(({ id, zoneId, effect, strength }) => ({ id, zoneId, effect, strength })),
zoneEventTiles: zoneEditorStore.zone.zoneEventTiles.map(({ id, zoneId, type, positionX, positionY, teleport }) => ({ id, zoneId, type, positionX, positionY, teleport })),
zoneObjects: zoneEditorStore.zone.zoneObjects.map(({ id, zoneId, objectId, depth, isRotated, positionX, positionY }) => ({ id, zoneId, objectId, depth, isRotated, positionX, positionY }))
}
if (zoneEditorStore.isSettingsModalShown) {
@ -64,11 +59,6 @@ function save() {
})
}
onBeforeMount(async () => {
await gameStore.fetchAllZoneAssets()
await loadAssets(scene)
})
onUnmounted(() => {
zoneEditorStore.reset()
})

View File

@ -1,49 +1,43 @@
<template>
<Teleport to="body">
<Modal :isModalOpen="zoneEditorStore.isObjectListModalShown" :modal-width="645" :modal-height="260" @modal:close="() => (zoneEditorStore.isObjectListModalShown = false)">
<template #modalHeader>
<h3 class="text-lg text-white">Objects</h3>
<div class="flex">
<div class="w-full flex gap-1.5 flex-row">
<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>-->
<!-- <label class="mb-1.5 font-titles hidden" for="depth">Depth</label>-->
<!-- <input v-model="objectDepth" @mousedown.stop class="input-field" type="number" name="depth" placeholder="Depth" />-->
<!-- </div>-->
<Modal :isModalOpen="zoneEditorStore.isObjectListModalShown" :modal-width="645" :modal-height="260" @modal:close="() => (zoneEditorStore.isObjectListModalShown = false)">
<template #modalHeader>
<h3 class="text-lg text-white">Objects</h3>
</template>
<template #modalBody>
<div class="flex pt-4 pl-4">
<div class="w-full flex gap-1.5 flex-row">
<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>
</template>
<template #modalBody>
<div class="flex flex-col h-full p-4">
<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 overflow-auto">
<div class="flex justify-between flex-wrap gap-2.5 items-center">
<div v-for="(object, index) in filteredObjects" :key="index" class="max-w-1/4 inline-block">
<img
class="border-2 border-solid max-w-full"
:src="`${config.server_endpoint}/assets/objects/${object.id}.png`"
alt="Object"
@click="zoneEditorStore.setSelectedObject(object)"
:class="{
'cursor-pointer transition-all duration-300': true,
'border-cyan shadow-lg scale-105': zoneEditorStore.selectedObject?.id === object.id,
'border-transparent hover:border-gray-300': zoneEditorStore.selectedObject?.id !== object.id
}"
/>
</div>
</div>
<div class="flex flex-col h-full p-4">
<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 overflow-auto">
<div class="flex justify-between flex-wrap gap-2.5 items-center">
<div v-for="(object, index) in filteredObjects" :key="index" class="max-w-1/4 inline-block">
<img
class="border-2 border-solid max-w-full"
:src="`${config.server_endpoint}/assets/objects/${object.id}.png`"
alt="Object"
@click="zoneEditorStore.setSelectedObject(object)"
:class="{
'cursor-pointer transition-all duration-300': true,
'border-cyan shadow-lg scale-105': zoneEditorStore.selectedObject?.id === object.id,
'border-transparent hover:border-gray-300': zoneEditorStore.selectedObject?.id !== object.id
}"
/>
</div>
</div>
</div>
</template>
</Modal>
</Teleport>
</div>
</template>
</Modal>
</template>
<script setup lang="ts">

View File

@ -1,10 +1,6 @@
<template>
<div class="flex flex-col items-center py-5 px-3 fixed bottom-14 right-0">
<div class="self-end mt-2 flex gap-2">
<div>
<label class="mb-1.5 font-titles block text-sm text-gray-700 hidden" for="depth">Depth</label>
<input v-model="objectDepth" @mousedown.stop @input="handleDepthInput" class="input-field max-w-24 px-2 py-1 border rounded" type="number" name="depth" placeholder="Depth" />
</div>
<button @mousedown.stop @click="handleDelete" class="btn-red py-1.5 px-4">
<img src="/assets/icons/trashcan.svg" class="w-4 h-4" alt="Delete" />
</button>
@ -15,38 +11,23 @@
</template>
<script setup lang="ts">
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { ref, computed, watch } from 'vue'
import type { ZoneObject } from '@/types'
const emit = defineEmits(['update_depth', 'move', 'delete', 'rotate'])
const zoneEditorStore = useZoneEditorStore()
const props = defineProps<{
zoneObject: ZoneObject
}>()
const objectDepth = ref(zoneEditorStore.objectDepth)
const emit = defineEmits(['move', 'rotate', 'delete'])
watch(
() => zoneEditorStore.selectedZoneObject,
(selectedZoneObject) => {
objectDepth.value = selectedZoneObject?.depth ?? 0
}
)
const handleDepthInput = () => {
const depth = parseFloat(objectDepth.value.toString())
if (!isNaN(depth)) {
emit('update_depth', depth)
}
const handleMove = () => {
emit('move', props.zoneObject.id)
}
const handleRotate = () => {
emit('rotate', zoneEditorStore.selectedZoneObject?.id)
}
const handleMove = () => {
emit('move', zoneEditorStore.selectedZoneObject?.id)
emit('rotate', props.zoneObject.id)
}
const handleDelete = () => {
emit('delete', zoneEditorStore.selectedZoneObject?.id)
zoneEditorStore.setSelectedZoneObject(null)
emit('delete', props.zoneObject.id)
}
</script>

View File

@ -1,9 +1,11 @@
<template>
<Teleport to="body">
<Modal :isModalOpen="zoneEditorStore.isTileListModalShown" :modal-width="645" :modal-height="600" @modal:close="() => (zoneEditorStore.isTileListModalShown = false)">
<template #modalHeader>
<h3 class="text-lg text-white">Tiles</h3>
<div class="flex">
<Modal :isModalOpen="zoneEditorStore.isTileListModalShown" :modal-width="645" :modal-height="600" @modal:close="() => (zoneEditorStore.isTileListModalShown = false)">
<template #modalHeader>
<h3 class="text-lg text-white">Tiles</h3>
</template>
<template #modalBody>
<div class="h-full overflow-auto" v-if="!selectedGroup">
<div class="flex pt-4 pl-4">
<div class="w-full flex gap-1.5 flex-row">
<div>
<label class="mb-1.5 font-titles hidden" for="search">Search...</label>
@ -11,8 +13,6 @@
</div>
</div>
</div>
</template>
<template #modalBody>
<div class="flex flex-col h-full p-4">
<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) }">
@ -21,25 +21,63 @@
</div>
<div class="h-[calc(100%_-_60px)] flex-grow overflow-y-auto">
<div class="grid grid-cols-8 gap-2 justify-items-center">
<div v-for="tile in filteredTiles" :key="tile.id" class="flex items-center justify-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"
:src="`${config.server_endpoint}/assets/tiles/${tile.id}.png`"
alt="Tile"
@click="zoneEditorStore.setSelectedTile(tile)"
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
:src="`${config.server_endpoint}/assets/tiles/${group.parent.id}.png`"
:alt="group.parent.name"
@click="openGroup(group)"
@load="() => processTile(group.parent)"
:class="{
'cursor-pointer transition-all duration-300': true,
'border-cyan shadow-lg scale-105': zoneEditorStore.selectedTile?.id === tile.id,
'border-transparent hover:border-gray-300': zoneEditorStore.selectedTile?.id !== tile.id
'border-cyan shadow-lg scale-105': 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>
</template>
</Modal>
</Teleport>
</div>
<div v-else class="h-full overflow-auto">
<div class="p-4">
<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}/assets/tiles/${selectedGroup.parent.id}.png`"
:alt="selectedGroup.parent.name"
@click="selectTile(selectedGroup.parent)"
: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}/assets/tiles/${childTile.id}.png`"
:alt="childTile.name"
@click="selectTile(childTile)"
: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>
<script setup lang="ts">
@ -55,20 +93,60 @@ const isModalOpen = ref(false)
const zoneEditorStore = useZoneEditorStore()
const searchQuery = ref('')
const selectedTags = ref<string[]>([])
const tileCategories = ref<Map<string, string>>(new Map())
const selectedGroup = ref<{ parent: Tile; children: Tile[] } | null>(null)
const uniqueTags = computed(() => {
const allTags = zoneEditorStore.tileList.flatMap((tile) => tile.tags || [])
return Array.from(new Set(allTags))
})
const filteredTiles = computed(() => {
return zoneEditorStore.tileList.filter((tile) => {
const groupedTiles = computed(() => {
const groups: { parent: Tile; children: Tile[] }[] = []
const filteredTiles = zoneEditorStore.tileList.filter((tile) => {
const matchesSearch = !searchQuery.value || tile.name.toLowerCase().includes(searchQuery.value.toLowerCase())
const matchesTags = selectedTags.value.length === 0 || (tile.tags && selectedTags.value.some((tag) => tile.tags.includes(tag)))
return matchesSearch && matchesTags
})
filteredTiles.forEach((tile) => {
const parentGroup = groups.find((group) => areTilesRelated(group.parent, tile))
if (parentGroup && parentGroup.parent.id !== tile.id) {
parentGroup.children.push(tile)
} else {
groups.push({ parent: tile, children: [] })
}
})
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) => {
if (selectedTags.value.includes(tag)) {
selectedTags.value = selectedTags.value.filter((t) => t !== tag)
@ -77,10 +155,82 @@ 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}/assets/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 {
return tileCategories.value.get(tile.id) || ''
}
function openGroup(group: { parent: Tile; children: Tile[] }) {
selectedGroup.value = group
}
function closeGroup() {
selectedGroup.value = null
}
function selectTile(tile: Tile) {
zoneEditorStore.setSelectedTile(tile)
}
function isActiveTile(tile: Tile): boolean {
return zoneEditorStore.selectedTile?.id === tile.id
}
onMounted(async () => {
isModalOpen.value = true
gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => {
zoneEditorStore.setTileList(response)
response.forEach((tile) => processTile(tile))
})
})
</script>

View File

@ -3,32 +3,32 @@
<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 ref="toolbar" class="tools flex gap-2.5" v-if="zoneEditorStore.zone">
<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': zoneEditorStore.tool === 'move' }" @click="handleClick('move')">
<img class="invert w-5 h-5" src="/assets/icons/zoneEditor/move.svg" alt="Move camera" /> <span :class="{ 'ml-2.5': zoneEditorStore.tool !== 'move' }">(M)</span>
<img class="invert w-5 h-5" src="/assets/icons/zoneEditor/move.svg" alt="Move camera" /> <span class="h-5" :class="{ 'ml-2.5': zoneEditorStore.tool !== 'move' }">(M)</span>
</button>
<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': zoneEditorStore.tool === 'pencil' }" @click="handleClick('pencil')">
<img class="invert w-5 h-5" src="/assets/icons/zoneEditor/pencil.svg" alt="Pencil" /> <span :class="{ 'ml-2.5': zoneEditorStore.tool !== 'pencil' }">(P)</span>
<img class="invert w-5 h-5" src="/assets/icons/zoneEditor/pencil.svg" alt="Pencil" /> <span class="h-5" :class="{ 'ml-2.5': zoneEditorStore.tool !== 'pencil' }">(P)</span>
<div class="select" v-if="zoneEditorStore.tool === 'pencil'">
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectPencilOpen }">
{{ zoneEditorStore.drawMode }}
<img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/zoneEditor/chevron.svg" />
</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 && zoneEditorStore.tool === 'pencil'">
<span class="py-2 px-2.5 relative hover:bg-cyan/50" @click="setDrawMode('tile')">
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('tile')">
Tile
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span>
<span class="py-2 px-2.5 relative hover:bg-cyan/50" @click="setDrawMode('object')">
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('object')">
Object
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span>
<span class="py-2 px-2.5 relative hover:bg-cyan/50" @click="setDrawMode('teleport')">
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('teleport')">
Teleport
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span>
<span class="py-2 px-2.5 relative hover:bg-cyan/50" @click="setDrawMode('blocking tile')">Blocking tile</span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('blocking tile')">Blocking tile</span>
</div>
</div>
</button>
@ -36,26 +36,26 @@
<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': zoneEditorStore.tool === 'eraser' }" @click="handleClick('eraser')">
<img class="invert w-5 h-5" src="/assets/icons/zoneEditor/eraser.svg" alt="Eraser" /> <span :class="{ 'ml-2.5': zoneEditorStore.tool !== 'eraser' }">(E)</span>
<img class="invert w-5 h-5" src="/assets/icons/zoneEditor/eraser.svg" alt="Eraser" /> <span class="h-5" :class="{ 'ml-2.5': zoneEditorStore.tool !== 'eraser' }">(E)</span>
<div class="select" v-if="zoneEditorStore.tool === 'eraser'">
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectEraserOpen }">
{{ zoneEditorStore.eraserMode }}
<img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/zoneEditor/chevron.svg" />
</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">
<span class="py-2 px-2.5 relative hover:bg-cyan/50" @click="setEraserMode('tile')">
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('tile')">
Tile
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span>
<span class="py-2 px-2.5 relative hover:bg-cyan/50" @click="setEraserMode('object')">
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('object')">
Object
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span>
<span class="py-2 px-2.5 relative hover:bg-cyan/50" @click="setEraserMode('teleport')">
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('teleport')">
Teleport
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span>
<span class="py-2 px-2.5 relative hover:bg-cyan/50" @click="setEraserMode('blocking tile')">Blocking tile</span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('blocking tile')">Blocking tile</span>
</div>
</div>
</button>
@ -63,12 +63,12 @@
<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': zoneEditorStore.tool === 'paint' }" @click="handleClick('paint')">
<img class="invert w-5 h-5" src="/assets/icons/zoneEditor/paint.svg" alt="Paint bucket" /> <span :class="{ 'ml-2.5': zoneEditorStore.tool !== 'paint' }">(B)</span>
<img class="invert w-5 h-5" src="/assets/icons/zoneEditor/paint.svg" alt="Paint bucket" /> <span class="h-5" :class="{ 'ml-2.5': zoneEditorStore.tool !== 'paint' }">(B)</span>
</button>
<div class="w-px bg-cyan"></div>
<button class="flex justify-center items-center min-w-10 p-0 relative" @click="handleClick('settings')" v-if="zoneEditorStore.zone"><img class="invert w-5 h-5" src="/assets/icons/zoneEditor/gear.svg" alt="Zone settings" /> <span class="ml-2.5">(Z)</span></button>
<button class="flex justify-center items-center min-w-10 p-0 relative" @click="handleClick('settings')" v-if="zoneEditorStore.zone"><img class="invert w-5 h-5" src="/assets/icons/zoneEditor/gear.svg" alt="Zone settings" /> <span class="h-5 ml-2.5">(Z)</span></button>
</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">
@ -138,7 +138,9 @@ function cycleToolMode(tool: 'pencil' | 'eraser') {
}
function initKeyShortcuts(event: KeyboardEvent) {
// Check if zone is set
if (!zoneEditorStore.zone) return
// prevent if focused on composables
if (document.activeElement?.tagName === 'INPUT') return

View File

@ -6,19 +6,23 @@
<template #modalBody>
<div class="m-4">
<form method="post" @submit.prevent="" class="inline">
<div class="gap-2.5 flex flex-wrap">
<div class="space-x-2">
<button class="btn-cyan py-1.5 px-4" type="button" @click.prevent="screen = 'settings'">Settings</button>
<button class="btn-cyan py-1.5 px-4" type="button" @click.prevent="screen = 'effects'">Effects</button>
</div>
<form method="post" @submit.prevent="" class="inline" v-if="screen === 'settings'">
<div class="gap-2.5 flex flex-wrap mt-4">
<div class="form-field-full">
<label for="name">Name</label>
<input class="input-field" v-model="name" name="name" id="name" />
</div>
<div class="form-field-half">
<label for="name">Width</label>
<input class="input-field" v-model="width" name="name" id="name" type="number" />
<label for="width">Width</label>
<input class="input-field" v-model="width" name="width" id="width" type="number" />
</div>
<div class="form-field-half">
<label for="name">Height</label>
<input class="input-field" v-model="height" name="name" id="name" type="number" />
<label for="height">Height</label>
<input class="input-field" v-model="height" name="height" id="height" type="number" />
</div>
<div class="form-field-full">
<label for="pvp">PVP enabled</label>
@ -29,6 +33,14 @@
</div>
</div>
</form>
<form method="post" @submit.prevent="" class="inline" v-if="screen === 'effects'">
<div v-for="(effect, index) in zoneEffects" :key="effect.id" class="mb-2 flex items-center space-x-2 mt-4">
<input class="input-field flex-grow" v-model="effect.effect" placeholder="Effect name" />
<input class="input-field w-20" v-model.number="effect.strength" type="number" placeholder="Strength" />
<button class="btn-red py-1 px-2" type="button" @click="removeEffect(index)">Delete</button>
</div>
<button class="btn-green py-1 px-2 mt-2" type="button" @click="addEffect">Add Effect</button>
</form>
</div>
</template>
</Modal>
@ -40,16 +52,19 @@ import Modal from '@/components/utilities/Modal.vue'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
const zoneEditorStore = useZoneEditorStore()
const screen = ref('settings')
zoneEditorStore.setZoneName(zoneEditorStore.zone?.name)
zoneEditorStore.setZoneWidth(zoneEditorStore.zone?.width)
zoneEditorStore.setZoneHeight(zoneEditorStore.zone?.height)
zoneEditorStore.setZonePvp(zoneEditorStore.zone?.pvp)
zoneEditorStore.setZoneEffects(zoneEditorStore.zone?.zoneEffects)
const name = ref(zoneEditorStore.zoneSettings?.name)
const width = ref(zoneEditorStore.zoneSettings?.width)
const height = ref(zoneEditorStore.zoneSettings?.height)
const pvp = ref(zoneEditorStore.zoneSettings?.pvp)
const zoneEffects = ref(zoneEditorStore.zoneSettings?.zoneEffects || [])
watch(name, (value) => {
zoneEditorStore.setZoneName(value)
@ -66,4 +81,26 @@ watch(height, (value) => {
watch(pvp, (value) => {
zoneEditorStore.setZonePvp(value)
})
watch(
zoneEffects,
(value) => {
zoneEditorStore.setZoneEffects(value)
},
{ deep: true }
)
const addEffect = () => {
zoneEffects.value.push({
id: Date.now().toString(), // Simple unique id generation
zoneId: zoneEditorStore.zone?.id,
zone: zoneEditorStore.zone,
effect: '',
strength: 1
})
}
const removeEffect = (index) => {
zoneEffects.value.splice(index, 1)
}
</script>

View File

@ -0,0 +1,242 @@
<template>
<div class="relative" v-if="gameStore.uiSettings.isCharacterProfileOpen" :style="modalStyle">
<img src="/assets/ui-elements/profile-ui-box-outer.svg" class="absolute w-full h-full" />
<img src="/assets/ui-elements/profile-ui-box-inner.svg" class="absolute left-2 bottom-2 w-[calc(100%_-_16px)] h-[calc(100%_-_40px)]" />
<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>
<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/close-button-white.svg" class="w-full h-full" />
</button>
</div>
<div class="py-4 px-6 flex flex-col gap-7 relative z-10">
<div class="flex flex-col gap-2.5">
<div class="flex justify-between">
<div>
<p class="text-sm m-0 font-bold text-white tracking-wide">{{ gameStore.character?.name }}</p>
<span class="text-xs">{{ gameStore.character?.experience }} / 18.600XP</span>
</div>
<a class="hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-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" />
</a>
</div>
<div class="flex justify-between">
<div class="flex flex-col gap-0.5">
<div class="w-9 h-9 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600">
<span class="absolute w-full top-1/2 -translate-y-1/2 text-[6px] text-center">CROWN</span>
</div>
<div class="w-9 h-9 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600">
<span class="absolute w-full top-1/2 -translate-y-1/2 text-[6px] text-center">R-HAND</span>
</div>
<div class="flex gap-0.5 items-end">
<div class="w-9 h-9 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600">
<span class="absolute w-full top-1/2 -translate-y-1/2 text-[6px] text-center">L-HAND</span>
</div>
<div class="w-6 h-6 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600">
<span class="absolute w-full top-1/2 -translate-y-1/2 text-[6px] text-center">RING</span>
</div>
</div>
</div>
<img src="/assets/placeholders/inventory_player.png" class="w-8 h-auto" />
<div class="flex flex-col items-end gap-0.5">
<div class="w-9 h-9 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600">
<img class="absolute w-6 h-6 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/profile/helmet.svg" />
</div>
<div class="w-9 h-9 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600">
<img class="absolute w-6 h-6 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/profile/chestplate.svg" />
</div>
<div class="flex gap-0.5 items-end">
<div class="w-6 h-6 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600">
<img class="absolute w-4 h-4 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/profile/boots.svg" />
</div>
<div class="w-9 h-9 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600">
<img class="absolute w-6 h-6 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/profile/legs.svg" />
</div>
</div>
</div>
</div>
<div class="flex justify-between">
<div class="flex flex-col">
<div class="w-[105px] h-px mb-[3px] flex justify-between">
<div class="w-px h-full bg-gray-300"></div>
<div class="w-[101px] h-full bg-gray-300"></div>
<div class="w-px h-full bg-gray-300"></div>
</div>
<div class="flex gap-11">
<p class="m-0 text-xs text-white tracking-wide">Health</p>
<span class="m-0 text-xs text-white tracking-wide">+ 15</span>
</div>
<div class="w-[105px] h-px my-[3px] flex justify-between">
<div class="w-px h-full bg-gray-300"></div>
<div class="w-[101px] h-full bg-gray-300"></div>
<div class="w-px h-full bg-gray-300"></div>
</div>
<div class="flex gap-11">
<p class="m-0 text-xs text-white tracking-wide">Health</p>
<span class="m-0 text-xs text-white tracking-wide">+ 15</span>
</div>
<div class="w-[105px] h-px mt-[3px] flex justify-between">
<div class="w-px h-full bg-gray-300"></div>
<div class="w-[101px] h-full bg-gray-300"></div>
<div class="w-px h-full bg-gray-300"></div>
</div>
</div>
<div class="flex flex-col">
<div class="w-[105px] h-px mb-[3px] flex justify-between">
<div class="w-px h-full bg-gray-300"></div>
<div class="w-[101px] h-full bg-gray-300"></div>
<div class="w-px h-full bg-gray-300"></div>
</div>
<div class="flex gap-11">
<p class="m-0 text-xs text-white tracking-wide">Health</p>
<span class="m-0 text-xs text-white tracking-wide">+ 15</span>
</div>
<div class="w-[105px] h-px my-[3px] flex justify-between">
<div class="w-px h-full bg-gray-300"></div>
<div class="w-[101px] h-full bg-gray-300"></div>
<div class="w-px h-full bg-gray-300"></div>
</div>
<div class="flex gap-11">
<p class="m-0 text-xs text-white tracking-wide">Health</p>
<span class="m-0 text-xs text-white tracking-wide">+ 15</span>
</div>
<div class="w-[105px] h-px mt-[3px] flex justify-between">
<div class="w-px h-full bg-gray-300"></div>
<div class="w-[101px] h-full bg-gray-300"></div>
<div class="w-px h-full bg-gray-300"></div>
</div>
</div>
</div>
</div>
<div class="grid grid-rows-4 grid-cols-6 gap-0.5">
<div v-for="n in 24" class="w-9 h-9 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600"></div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref, watch, computed } from 'vue'
import { useGameStore } from '@/stores/gameStore'
const gameStore = useGameStore()
let startX = 0
let startY = 0
let initialX = 0
let initialY = 0
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 modalStyle = computed(() => ({
top: `${y.value}px`,
left: `${x.value}px`,
width: `${width.value}px`,
height: `${height.value}px`,
maxWidth: '100vw',
maxHeight: '100vh'
}))
function startDrag(event: MouseEvent) {
isDragging.value = true
startX = event.clientX
startY = event.clientY
initialX = x.value
initialY = y.value
event.preventDefault()
}
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.max(0, Math.min(x.value, window.innerWidth - width.value))
y.value = Math.max(0, 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) {
if (event.altKey && event.key === 'c') {
gameStore.toggleCharacterProfile()
}
}
onMounted(() => {
addEventListener('keydown', keyPress)
addEventListener('mousemove', drag)
addEventListener('mouseup', stopDrag)
initializePosition()
})
onUnmounted(() => {
removeEventListener('mousemove', drag)
removeEventListener('mouseup', stopDrag)
})
</script>

View File

@ -0,0 +1,52 @@
<template>
<div class="absolute top-4 left-[300px] w-[422px]">
<div class="flex gap-2.5">
<div class="relative">
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
<span class="z-10 text-pixel absolute top-1 left-2">F1</span>
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f1-icon.png')] bg-no-repeat"></div>
</div>
<div class="relative">
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
<span class="z-10 text-pixel absolute top-1 left-2">F2</span>
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f2-icon.png')] bg-no-repeat"></div>
</div>
<div class="relative">
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
<span class="z-10 text-pixel absolute top-1 left-2">F3</span>
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f3-icon.png')] bg-no-repeat"></div>
</div>
<div class="relative">
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
<span class="z-10 text-pixel absolute top-1 left-2">F4</span>
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f4-icon.png')] bg-no-repeat"></div>
</div>
<div class="relative">
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
<span class="z-10 text-pixel absolute top-1 left-2">F5</span>
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f5-icon.png')] bg-no-repeat"></div>
</div>
<div class="relative">
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
<span class="z-10 text-pixel absolute top-1 left-2">F6</span>
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f6-icon.png')] bg-no-repeat"></div>
</div>
<div class="relative">
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
<span class="z-10 text-pixel absolute top-1 left-2">F7</span>
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f7-icon.png')] bg-no-repeat"></div>
</div>
<div class="relative">
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
<span class="z-10 text-pixel absolute top-1 left-2">F8</span>
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f8-icon.png')] bg-no-repeat"></div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useGameStore } from '@/stores/gameStore'
const gameStore = useGameStore()
</script>

View File

@ -1,5 +1,5 @@
<template>
<div class="absolute left-[66px] top-4 bg-[url('/assets/ui-rect-border-4-corners.svg')] bg-no-repeat px-4 py-2 w-[181px] h-[26px] flex flex-col justify-between">
<div class="absolute left-[66px] top-4 bg-[url('/assets/ui-elements/ui-rect-border-4-corners.svg')] bg-no-repeat px-4 py-2 w-[181px] h-[26px] flex flex-col justify-between">
<div class="w-full flex items-center gap-2">
<label class="text-xs leading-3 text-pixel" for="hp">HP</label>
<progress class="h-2 rounded-sm w-full max-w-44 appearance-none accent-green" id="hp" :value="gameStore.character?.hitpoints" max="100">{{ gameStore.character?.hitpoints }}%</progress>

View File

@ -1,48 +0,0 @@
<template>
<div class="absolute left-[300px] top-4">
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
<span class="z-10 text-pixel absolute top-1 left-2">F1</span>
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f1-icon.png')] bg-no-repeat"></div>
</div>
<div class="absolute left-[346px] top-4">
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
<span class="z-10 text-pixel absolute top-1 left-2">F2</span>
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f2-icon.png')] bg-no-repeat"></div>
</div>
<div class="absolute left-[392px] top-4">
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
<span class="z-10 text-pixel absolute top-1 left-2">F3</span>
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f3-icon.png')] bg-no-repeat"></div>
</div>
<div class="absolute left-[438px] top-4">
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
<span class="z-10 text-pixel absolute top-1 left-2">F4</span>
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f4-icon.png')] bg-no-repeat"></div>
</div>
<div class="absolute left-[484px] top-4">
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
<span class="z-10 text-pixel absolute top-1 left-2">F5</span>
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f5-icon.png')] bg-no-repeat"></div>
</div>
<div class="absolute left-[530px] top-4">
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
<span class="z-10 text-pixel absolute top-1 left-2">F6</span>
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f6-icon.png')] bg-no-repeat"></div>
</div>
<div class="absolute left-[576px] top-4">
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
<span class="z-10 text-pixel absolute top-1 left-2">F7</span>
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f7-icon.png')] bg-no-repeat"></div>
</div>
<div class="absolute left-[622px] top-4">
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
<span class="z-10 text-pixel absolute top-1 left-2">F8</span>
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f8-icon.png')] bg-no-repeat"></div>
</div>
</template>
<script setup lang="ts">
import { useGameStore } from '@/stores/gameStore'
const gameStore = useGameStore()
</script>

View File

@ -5,16 +5,16 @@
<p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">Open menu</p>
<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>
<a class="group-hover:cursor-pointer bg-[url('/assets/ui-border-4-corners.svg')] bg-no-repeat block w-[42px] h-[42px] relative">
<a class="group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners.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/menu-icon.svg" />
</a>
</li>
<li class="menu-item group relative" @click="gameStore.toggleUserPanel">
<li class="menu-item group relative" @click="gameStore.toggleCharacterProfile">
<div class="group-hover:block absolute top-1/2 left-14 -translate-y-1/2 w-20 h-6 text-center bg-gray-800 border-2 border-solid border-gray-500 rounded-3xl hidden">
<p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">User Profile</p>
<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>
<a class="group-hover:cursor-pointer bg-[url('/assets/ui-border-4-corners.svg')] bg-no-repeat block w-[42px] h-[42px] relative">
<a class="group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners.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/avatar/default/head.png" />
<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>
@ -24,7 +24,7 @@
<p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">Open Chat</p>
<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>
<a class="group-hover:bg-gray-800 group-hover:cursor-pointer border border-b-4 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 border-b-4 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/chat-icon.svg" />
</a>
</li>
@ -33,7 +33,7 @@
<p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">World map</p>
<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>
<a class="group-hover:bg-gray-800 group-hover:cursor-pointer border border-b-4 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 border-b-4 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/map-icon.svg" />
</a>
</li>
@ -42,7 +42,7 @@
<p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">Users</p>
<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>
<a class="group-hover:bg-gray-800 group-hover:cursor-pointer border border-b-4 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 border-b-4 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/socials-icon.svg" />
</a>
</li>

View File

@ -1,16 +1,16 @@
<template>
<div class="absolute top-4 right-4">
<div class="absolute top-4 right-4 hidden lg:block">
<div class="w-40 h-40 rounded-full border border-solid border-gray-500 bg-[url('/assets/ui-texture.png')] bg-no-repeat">
<div class="w-40 h-40 rounded-full shadow-inner"></div>
</div>
<div class="absolute -bottom-3 left-1/2 -translate-x-1/2 flex gap-1">
<button class="w-6 h-6 relative p-0">
<img class="absolute w-3 h-3 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/plus-icon.svg" />
<img class="w-full h-full" src="/assets/ui-border-4-corners.svg" />
<img class="w-full h-full" src="/assets/ui-elements/ui-border-4-corners.svg" />
</button>
<button class="w-6 h-6 relative p-0">
<img class="absolute w-3 h-3 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/minus-icon.svg" />
<img class="w-full h-full" src="/assets/ui-border-4-corners.svg" />
<img class="w-full h-full" src="/assets/ui-elements/ui-border-4-corners.svg" />
</button>
</div>
</div>

View File

@ -1,5 +1,5 @@
<template>
<div class="absolute z-50 w-full h-dvh top-0 left-0 bg-black/60" v-show="gameStore.uiSettings.isUserPanelOpen">
<div class="absolute z-50 w-full h-dvh top-0 left-0 bg-black/60" v-show="false">
<div class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 max-w-[875px] max-h-[600px] h-full w-[80%] bg-gray-700 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>
@ -10,7 +10,7 @@
<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" @click="gameStore.toggleUserPanel">
<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/close-button-white.svg" class="w-full h-full" />
</button>
</div>

View File

@ -2,11 +2,11 @@
<!-- Chat bubble -->
<Container ref="charChatContainer" :depth="999" :x="currentX" :y="currentY">
<RoundRectangle @create="createChatBubble" :origin-x="0.5" :origin-y="7.5" :fillColor="0xffffff" :width="194" :height="21" :radius="20" />
<Text @create="createChatText" text="" :origin-x="0.5" :origin-y="10.9" :style="{ fontSize: 13, fontFamily: 'Arial', color: '#000' }" />
<Text @create="createChatText" :style="{ fontSize: 13, fontFamily: 'Arial', color: '#000' }" />
</Container>
<!-- Character name and health -->
<Container :depth="999" :x="currentX" :y="currentY">
<Text @create="createText" :text="character.name" :origin-x="0.5" :origin-y="9" />
<Text @create="createNicknameText" :text="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="36.4" :fillColor="0x00b3b3" :width="70" :height="3" :radius="5" />
</Container>
@ -113,7 +113,7 @@ const isFlippedX = computed(() => [6, 4].includes(props.character.rotation ?? 0)
const charTexture = computed(() => {
const { rotation, characterType, isMoving } = props.character
const spriteId = characterType?.sprite.id ?? 'idle_right_down'
const spriteId = characterType?.sprite?.id ?? 'idle_right_down'
const action = isMoving ? 'walk' : 'idle'
const direction = [0, 6].includes(rotation) ? 'left_up' : 'right_down'
@ -139,20 +139,30 @@ const createChatText = (text: Phaser.GameObjects.Text) => {
text.setName(`${props.character.name}_chatText`)
text.setFontSize(13)
text.setFontFamily('Arial')
text.setOrigin(0.5, 10.9)
// Fix text alignment on Windows and Android
if (game.device.os.windows || game.device.os.android) {
text.setOrigin(0.5, 9.75)
if (game.device.browser.firefox) {
text.setOrigin(0.5, 10.9)
}
}
}
const createText = (text: Phaser.GameObjects.Text) => {
const createNicknameText = (text: Phaser.GameObjects.Text) => {
text.setFontSize(13)
text.setFontFamily('Arial')
text.setOrigin(0.5, 9)
// Fix text alignment on Windows and Android
if (game.device.os.windows || game.device.os.android) {
text.setOrigin(0.5, 8)
if (game.device.browser.firefox) {
text.setOrigin(0.5, 9)
}
}
}

View File

@ -8,7 +8,7 @@
</div>
<div class="flex gap-2.5">
<button @click="toggleFullScreen" class="w-5 h-5 m-0 p-0 relative hover:scale-110 transition-transform duration-300 ease-in-out" v-if="canFullScreen">
<img :alt="isFullScreen ? 'exit full-screen' : 'full-screen'" draggable="false" :src="isFullScreen ? '/assets/icons/minimize.svg' : '/assets/icons/full-screen.svg'" class="w-3.5 h-3.5 invert" />
<img :alt="isFullScreen ? 'exit full-screen' : 'full-screen'" draggable="false" :src="isFullScreen ? '/assets/icons/minimize.svg' : '/assets/icons/increase-size-option.svg'" class="w-3.5 h-3.5 invert" />
</button>
<button @click="close" v-if="closable" 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/close-button-white.svg" class="w-full h-full" />
@ -161,8 +161,6 @@ function initializePosition() {
width.value = Math.min(props.modalWidth, window.innerWidth)
height.value = Math.min(props.modalHeight, window.innerHeight)
if (props.modalPositionX !== 0 && props.modalPositionY !== 0) {
console.log(props.modalPositionX)
console.log(props.modalPositionY)
x.value = props.modalPositionX
y.value = props.modalPositionY
} else {

View File

@ -1,5 +1,5 @@
<template>
<Modal v-for="notification in gameStore.getNotifications" :key="notification.id" :isModalOpen="true" @modal:close="closeNotification(notification.id)">
<Modal v-for="notification in gameStore.notifications" :key="notification.id" :isModalOpen="true" @modal:close="closeNotification(notification.id)">
<template #modalHeader v-if="notification.title">
<h3 class="m-0 font-medium shrink-0 text-white">{{ notification.title }}</h3>
</template>

View File

@ -1,28 +0,0 @@
<template>
<Image v-for="object in zoneStore.zone?.zoneObjects" v-bind="getObjectImageProps(object)" />
</template>
<script setup lang="ts">
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
import { Image, Text } from 'phavuer'
import { useZoneStore } from '@/stores/zoneStore'
import type { ZoneObject } from '@/types'
const zoneStore = useZoneStore()
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
}>()
const getObjectImageProps = (object: ZoneObject) => {
return {
depth: calculateIsometricDepth(object.positionX, object.positionY, object.object.frameWidth, object.object.frameHeight),
x: tileToWorldX(props.tilemap as any, object.positionX, object.positionY),
y: tileToWorldY(props.tilemap as any, object.positionX, object.positionY),
flipX: object.isRotated,
texture: object.object.id,
originY: Number(object.object.originX),
originX: Number(object.object.originY)
}
}
</script>

View File

@ -1,23 +1,20 @@
<template>
<Tiles :key="zoneStore.zone?.id ?? 0" @tilemap:create="tileMap = $event" />
<Objects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
<ZoneTiles :key="zoneStore.zone?.id ?? 0" @tilemap:create="tileMap = $event" />
<ZoneObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
<Characters v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
</template>
<script setup lang="ts">
import { useScene } from 'phavuer'
import { useGameStore } from '@/stores/gameStore'
import { useZoneStore } from '@/stores/zoneStore'
import { onBeforeMount, onBeforeUnmount, ref } from 'vue'
import { onBeforeUnmount, ref, onBeforeMount } from 'vue'
import type { Character as CharacterT, Zone as ZoneT, ExtendedCharacter as ExtendedCharacterT } from '@/types'
import Tiles from '@/components/zone/Tiles.vue'
import Objects from '@/components/zone/Objects.vue'
import ZoneTiles from '@/components/zone/ZoneTiles.vue'
import ZoneObjects from '@/components/zone/ZoneObjects.vue'
import Characters from '@/components/zone/Characters.vue'
import { loadAssets } from '@/composables/zoneComposable'
const gameStore = useGameStore()
const zoneStore = useZoneStore()
const scene = useScene()
const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null)
@ -28,13 +25,6 @@ type zoneLoadData = {
// Event listeners
gameStore.connection!.on('zone:character:teleport', async (data: zoneLoadData) => {
/**
* This is the cause of the bug
*/
// Fetch assets for new zone
await gameStore.fetchZoneAssets(data.zone.id)
await loadAssets(scene)
/**
* @TODO : Update character via global event server-side, remove this and listen for it somewhere not here
*/
@ -61,12 +51,8 @@ gameStore.connection!.on('character:move', (data: ExtendedCharacterT) => {
zoneStore.updateCharacter(data)
})
onBeforeMount(() => {
gameStore.connection!.emit('zone:character:join', async (response: zoneLoadData) => {
// Fetch assets for new zone
await gameStore.fetchZoneAssets(response.zone.id)
await loadAssets(scene)
onBeforeMount(async () => {
gameStore!.connection!.emit('zone:character:join', async (response: zoneLoadData) => {
// Set zone and characters
zoneStore.setZone(response.zone)
zoneStore.setCharacters(response.characters)

View File

@ -0,0 +1,14 @@
<template>
<ZoneObject v-for="zoneObject in zoneStore.zone?.zoneObjects" :tilemap="tilemap" :zoneObject />
</template>
<script setup lang="ts">
import { useZoneStore } from '@/stores/zoneStore'
import ZoneObject from '@/components/zone/partials/ZoneObject.vue'
const zoneStore = useZoneStore()
defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
}>()
</script>

View File

@ -7,17 +7,15 @@ import config from '@/config'
import { useScene } from 'phavuer'
import { useZoneStore } from '@/stores/zoneStore'
import { onBeforeMount, onBeforeUnmount } from 'vue'
import { placeTile, setAllTiles } from '@/composables/zoneComposable'
import { setLayerTiles } from '@/composables/zoneComposable'
import Controls from '@/components/utilities/Controls.vue'
const emit = defineEmits(['tilemap:create'])
const zoneStore = useZoneStore()
const scene = useScene()
const zoneTilemap = createTilemap()
const tiles = createTileLayer()
let tileArray = createTileArray()
function createTilemap() {
const zoneData = new Phaser.Tilemaps.MapData({
@ -51,16 +49,11 @@ function createTileLayer() {
return layer
}
function createTileArray() {
return Array.from({ length: zoneTilemap.height || 0 }, () => Array.from({ length: zoneTilemap.width || 0 }, () => 'blank_tile'))
}
onBeforeMount(() => {
if (!zoneStore.zone?.tiles) {
return
}
setAllTiles(zoneTilemap, tiles, zoneStore.zone.tiles)
tileArray = zoneStore.zone.tiles.map((row) => row.map((tileId) => tileId || 'blank_tile'))
setLayerTiles(zoneTilemap, tiles, zoneStore.zone.tiles)
})
onBeforeUnmount(() => {

View File

@ -0,0 +1,63 @@
<template>
<Image v-if="isTextureLoaded" v-bind="imageProps" />
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { Image, useScene } from 'phavuer'
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
import { useAssetManager } from '@/utilities/assetManager'
import type { ZoneObject } from '@/types'
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
zoneObject: ZoneObject
}>()
const scene = useScene()
const assetManager = useAssetManager
const isTextureLoaded = ref(false)
const imageProps = computed(() => ({
depth: calculateIsometricDepth(props.zoneObject.positionX, props.zoneObject.positionY, props.zoneObject.object.frameWidth, props.zoneObject.object.frameHeight),
x: tileToWorldX(props.tilemap, props.zoneObject.positionX, props.zoneObject.positionY),
y: tileToWorldY(props.tilemap, props.zoneObject.positionX, props.zoneObject.positionY),
flipX: props.zoneObject.isRotated,
texture: props.zoneObject.object.id,
originY: Number(props.zoneObject.object.originX),
originX: Number(props.zoneObject.object.originY)
}))
const loadTexture = async () => {
const textureId = props.zoneObject.object.id
// Check if the texture is already loaded in Phaser
if (scene.textures.exists(textureId)) {
isTextureLoaded.value = true
return
}
let assetData = await assetManager.getAsset(textureId)
if (!assetData) {
await assetManager.downloadAsset(textureId, `/assets/objects/${textureId}.png`, 'objects', props.zoneObject.object.updatedAt)
assetData = await assetManager.getAsset(textureId)
}
if (assetData) {
return new Promise<void>((resolve) => {
scene.textures.addBase64(textureId, assetData.data)
scene.textures.once(`addtexture-${textureId}`, () => {
isTextureLoaded.value = true
resolve()
})
})
}
}
onMounted(() => {
loadTexture().catch((error) => {
console.error('Error loading texture:', error)
})
})
</script>

View File

View File

@ -3,7 +3,6 @@ import Tilemap = Phaser.Tilemaps.Tilemap
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
import Tileset = Phaser.Tilemaps.Tileset
import Tile = Phaser.Tilemaps.Tile
import { useGameStore } from '@/stores/gameStore'
export function getTile(layer: TilemapLayer | Tilemap, x: number, y: number): Tile | undefined {
const tile = layer.getTileAtWorldXY(x, y)
@ -11,31 +10,47 @@ export function getTile(layer: TilemapLayer | Tilemap, x: number, y: number): Ti
return tile
}
export function tileToWorldXY(layer: TilemapLayer, pos_x: number, pos_y: number) {
export function tileToWorldXY(layer: TilemapLayer | Tilemap, pos_x: number, pos_y: number) {
const worldPoint = layer.tileToWorldXY(pos_x, pos_y)
if (!worldPoint) return { positionX: 0, positionY: 0 }
const positionX = worldPoint.x + config.tile_size.y
const positionY = worldPoint.y
return { positionX, positionY }
}
export function tileToWorldX(layer: TilemapLayer, pos_x: number, pos_y: number): number {
export function tileToWorldX(layer: TilemapLayer | Tilemap, pos_x: number, pos_y: number): number {
const worldPoint = layer.tileToWorldXY(pos_x, pos_y)
if (!worldPoint) return 0
return worldPoint.x + config.tile_size.x / 2
}
export function tileToWorldY(layer: TilemapLayer, pos_x: number, pos_y: number): number {
export function tileToWorldY(layer: TilemapLayer | Tilemap, pos_x: number, pos_y: number): number {
const worldPoint = layer.tileToWorldXY(pos_x, pos_y)
if (!worldPoint) return 0
return worldPoint.y + config.tile_size.y * 1.5
}
/**
* Can also be used to replace tiles
* @param zone
* @param layer
* @param x
* @param y
* @param tileName
*/
export function placeTile(zone: Tilemap, layer: TilemapLayer, x: number, y: number, tileName: string) {
const tileImg = zone.getTileset(tileName) as Tileset
if (!tileImg) return
let tileImg = zone.getTileset(tileName) as Tileset
if (!tileImg) {
tileImg = zone.getTileset('blank_tile') as Tileset
}
layer.putTileAt(tileImg.firstgid, x, y)
}
export function setAllTiles(zone: Tilemap, layer: TilemapLayer, tiles: string[][]) {
export function setLayerTiles(zone: Tilemap, layer: TilemapLayer, tiles: string[][]) {
tiles.forEach((row: string[], y: number) => {
row.forEach((tile: string, x: number) => {
placeTile(zone, layer, x, y, tile)
@ -43,46 +58,14 @@ export function setAllTiles(zone: Tilemap, layer: TilemapLayer, tiles: string[][
})
}
export function createTileArray(width: number, height: number, tile: string = 'blank_tile') {
return Array.from({ length: height }, () => Array.from({ length: width }, () => tile))
}
export const calculateIsometricDepth = (x: number, y: number, width: number = 0, height: number = 0, isCharacter: boolean = false) => {
const baseDepth = x + y
if (isCharacter) {
return baseDepth // @TODO: Fix collision, this is a hack
}
// For objects, use their back bottom corner
return baseDepth + (width + height) / (2 * config.tile_size.x)
}
export const sortByIsometricDepth = <T extends { positionX: number; positionY: number }>(items: T[]) => {
return [...items].sort((a, b) => {
return calculateIsometricDepth(a.positionX, a.positionY, 0, 0) - calculateIsometricDepth(b.positionX, b.positionY, 0, 0)
})
}
export const clearAssets = (scene: Phaser.Scene) => {}
export const loadAssets = (scene: Phaser.Scene): Promise<void> => {
return new Promise((resolve) => {
const gameStore = useGameStore()
let addedLoad = false
gameStore.assets.forEach((asset) => {
if (scene.load.textureManager.exists(asset.key)) return
addedLoad = true
if (asset.group === 'sprite_animations') {
scene.load.spritesheet(asset.key, config.server_endpoint + asset.url, { frameWidth: asset.frameWidth ?? 0, frameHeight: asset.frameHeight ?? 0 })
} else {
scene.load.image(asset.key, config.server_endpoint + asset.url)
}
})
if (addedLoad) {
scene.load.start()
scene.load.on(Phaser.Loader.Events.COMPLETE, () => {
resolve()
})
} else {
resolve()
}
})
}

View File

@ -5,7 +5,8 @@ import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
app.use(createPinia())
app.use(pinia)
app.mount('#app')

View File

@ -6,8 +6,8 @@
<div class="flex gap-14 w-full max-h-[650px] overflow-x-auto" v-if="!isLoading">
<!-- CHARACTER LIST -->
<div v-for="character in characters" :key="character.id" class="group first:ml-auto last:mr-auto m-4 w-[170px] h-[275px] flex flex-col shrink-0 relative shadow-character" :class="{ active: selected_character == character.id }">
<img src="/assets/ui-box-outer.svg" class="absolute w-full h-full" />
<img src="/assets/ui-box-inner.svg" class="absolute left-2 bottom-2 w-[calc(100%_-_16px)] h-[calc(100%_-_40px)]" />
<img src="/assets/ui-elements/ui-box-outer.svg" class="absolute w-full h-full" />
<img src="/assets/ui-elements/ui-box-inner.svg" class="absolute left-2 bottom-2 w-[calc(100%_-_16px)] h-[calc(100%_-_40px)]" />
<input class="opacity-0 h-full w-full absolute m-0 z-10" type="radio" :id="character.id" name="character" :value="character.id" v-model="selected_character" />
<label class="font-bold absolute left-1/2 top-4 max-w-32 -translate-x-1/2 -translate-y-1/2 text-center text-ellipsis overflow-hidden whitespace-nowrap drop-shadow-text" :for="character.id">{{ character.name }}</label>
@ -114,13 +114,7 @@ gameStore.connection?.on('character:list', (data: any) => {
})
onMounted(async () => {
/**
* Fetch sprite assets from the server
* This is done here because phaser needs it in createScene in Game.vue.
*/
await gameStore.fetchSpriteAssets()
// wait 0.5 sec
// wait 0.75 sec
setTimeout(() => {
gameStore.connection?.emit('character:list')
isLoading.value = false

View File

@ -1,22 +1,17 @@
<template>
<div class="flex justify-center items-center h-dvh relative">
<GmTools v-if="gameStore.character?.role === 'gm'" />
<GmPanel v-if="gameStore.character?.role === 'gm'" />
<Game :config="gameConfig" @create="createGame">
<Scene name="main" @preload="preloadScene" @create="createScene">
<div v-if="isLoaded">
<Menu />
<Hud />
<Keybindings />
<Minimap />
<Zone />
<Chat />
<ExpBar />
<Menu />
<Hud />
<Hotkeys />
<Minimap />
<Zone />
<Chat />
<ExpBar />
<Inventory />
<Effects />
</div>
<CharacterProfile />
<Effects />
</Scene>
</Game>
</div>
@ -25,31 +20,38 @@
<script setup lang="ts">
import config from '@/config'
import 'phaser'
import { ref, onBeforeUnmount } from 'vue'
import { Game, Scene } from 'phavuer'
import { useGameStore } from '@/stores/gameStore'
import Menu from '@/components/gui/Menu.vue'
import ExpBar from '@/components/gui/ExpBar.vue'
import Hud from '@/components/gui/Hud.vue'
import Zone from '@/components/zone/Zone.vue'
import Keybindings from '@/components/gui/Keybindings.vue'
import Hotkeys from '@/components/gui/Hotkeys.vue'
import Chat from '@/components/gui/Chat.vue'
import GmTools from '@/components/gameMaster/GmTools.vue'
import GmPanel from '@/components/gameMaster/GmPanel.vue'
import Inventory from '@/components/gui/UserPanel.vue'
import CharacterProfile from '@/components/gui/CharacterProfile.vue'
import Effects from '@/components/Effects.vue'
import { loadAssets } from '@/composables/zoneComposable'
import Minimap from '@/components/gui/Minimap.vue'
import AwaitLoaderPlugin from 'phaser3-rex-plugins/plugins/awaitloader-plugin'
import { useAssetManager } from '@/utilities/assetManager'
const gameStore = useGameStore()
const isLoaded = ref(false)
const assetManager = useAssetManager
const gameConfig = {
name: config.name,
width: window.innerWidth,
height: window.innerHeight,
type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS
resolution: 5
resolution: 5,
plugins: {
global: [
{
key: 'rexAwaitLoader',
plugin: AwaitLoaderPlugin,
start: true
}
]
}
}
const createGame = (game: Phaser.Game) => {
@ -70,85 +72,47 @@ const createGame = (game: Phaser.Game) => {
}
}
const preloadScene = async (scene: Phaser.Scene) => {
isLoaded.value = false
/**
* Create loading bar
*/
const width = scene.cameras.main.width
const height = scene.cameras.main.height
const progressBox = scene.add.graphics()
const progressBar = scene.add.graphics()
progressBox.fillStyle(0x222222, 0.8)
progressBox.fillRect(width / 2 - 180, height / 2, 320, 50)
const loadingText = scene.make.text({
x: width / 2,
y: height / 2 - 50,
text: 'Loading...',
style: {
font: '20px monospace',
fill: '#ffffff'
}
})
loadingText.setOrigin(0.5, 0.5)
scene.load.on(Phaser.Loader.Events.PROGRESS, function (value: any) {
progressBar.clear()
progressBar.fillStyle(0x368f8b, 1)
progressBar.fillRect(width / 2 - 180 + 10, height / 2 + 10, 300 * value, 30)
})
scene.load.on(Phaser.Loader.Events.COMPLETE, function () {
progressBar.destroy()
progressBox.destroy()
loadingText.destroy()
isLoaded.value = true
})
function preloadScene(scene: Phaser.Scene) {
/**
* Load the base assets into the Phaser scene
*/
scene.load.image('BLOCK', '/assets/zone/bt_tile.png')
scene.load.image('TELEPORT', '/assets/zone/tp_tile.png')
scene.load.image('blank_tile', '/assets/zone/blank_tile.png')
scene.load.image('blank_object', '/assets/zone/blank_tile.png')
scene.load.image('waypoint', '/assets/waypoint.png')
/**
* Load the assets into the Phaser scene
*/
await loadAssets(scene)
scene.load.rexAwait(async function (successCallback) {
await assetManager.getAssetsByGroup('tiles').then((assets) => {
assets.forEach((asset) => {
if (scene.load.textureManager.exists(asset.key)) return
scene.textures.addBase64(asset.key, asset.data)
})
})
// Load objects
await assetManager.getAssetsByGroup('objects').then((assets) => {
assets.forEach((asset) => {
if (scene.load.textureManager.exists(asset.key)) return
scene.textures.addBase64(asset.key, asset.data)
})
})
successCallback()
})
}
const createScene = async (scene: Phaser.Scene) => {
function createScene(scene: Phaser.Scene) {
/**
* Create sprite animations
* This is done here because phaser forces us to
*/
gameStore.assets.forEach((asset) => {
if (asset.group !== 'sprite_animations') return
scene.anims.create({
key: asset.key,
frameRate: 7,
frames: scene.anims.generateFrameNumbers(asset.key, { start: 0, end: asset.frameCount! - 1 }),
repeat: -1
assetManager.getAssetsByGroup('sprite_animations').then((assets) => {
assets.forEach((asset) => {
scene.anims.create({
key: asset.key,
frameRate: 7,
frames: scene.anims.generateFrameNumbers(asset.key, { start: 0, end: asset.frameCount! - 1 }),
repeat: -1
})
})
})
}
onBeforeUnmount(() => {
isLoaded.value = false
})
</script>
<style lang="scss">
canvas {
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-crisp-edges;
image-rendering: pixelated;
}
</style>

74
src/screens/Loading.vue Normal file
View File

@ -0,0 +1,74 @@
<template>
<div class="flex flex-col justify-center items-center h-dvh relative">
<div v-if="!isLoaded" class="w-20 h-20 rounded-full border-4 border-solid border-gray-300 border-t-transparent animate-spin"></div>
<button v-else @click="continueBtnClick" class="w-20 h-20 rounded-full bg-gray-500 flex items-center justify-center hover:bg-gray-600 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</button>
<div v-if="!isLoaded" class="text-center mt-6">
<h1 class="text-2xl font-bold">Loading...</h1>
<p class="text-gray-400">Please wait while we load the assets.</p>
</div>
</div>
</template>
<script setup lang="ts" async>
import { onMounted, ref } from 'vue'
import config from '@/config'
import type { AssetT as ServerAsset } from '@/types'
import { useAssetManager } from '@/utilities/assetManager'
import { useGameStore } from '@/stores/gameStore'
/**
* This component downloads all assets from the server and
* stores them in the asset manager.
*/
const gameStore = useGameStore()
const assetManager = useAssetManager
const isLoaded = ref(false)
async function getAssets() {
return fetch(config.server_endpoint + '/assets/')
.then((response) => {
if (!response.ok) throw new Error('Failed to fetch assets')
console.log(response)
return response.json()
})
.catch((error) => {
console.error('Error fetching assets:', error)
return false
})
}
async function loadAssetsIntoAssetManager(assets: ServerAsset[]): Promise<void> {
for (const asset of assets) {
// Check if the asset is already loaded
const existingAsset = await assetManager.getAsset(asset.key)
// Check if the asset needs to be updated
if (!existingAsset || new Date(asset.updatedAt) > new Date(existingAsset.updatedAt)) {
// Check if the asset is already loaded, if so, delete it
if (existingAsset) {
await assetManager.deleteAsset(asset.key)
}
// Add the asset to the asset manager
await assetManager.downloadAsset(asset.key, asset.url, asset.group, asset.updatedAt, asset.frameCount, asset.frameWidth, asset.frameHeight)
}
}
}
function continueBtnClick() {
gameStore.isAssetsLoaded = true
}
onMounted(async () => {
const assets = await getAssets()
if (assets) {
await loadAssetsIntoAssetManager(assets)
isLoaded.value = true
}
})
</script>

View File

@ -1,13 +1,13 @@
<template>
<div class="relative max-lg:h-dvh">
<div class="lg:bg-gradient-to-r bg-gradient-to-b from-gray-900 to-transparent w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute right-0 max-lg:bottom-0 lg:top-0 z-10"></div>
<div class="bg-[url('/assets/login/login-bg.png')] w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute right-0 max-lg:bottom-0 lg:top-0 bg-no-repeat bg-cover bg-center"></div>
<div class="relative max-lg:h-dvh flex flex-row-reverse">
<div class="lg:bg-gradient-to-l bg-gradient-to-b from-gray-900 to-transparent w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 z-10"></div>
<div class="bg-[url('/assets/login/login-bg.png')] w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 bg-no-repeat bg-cover bg-center"></div>
<div class="bg-gray-900 z-20 w-full lg:w-1/2 h-[65dvh] lg:h-dvh relative">
<div class="h-dvh flex items-center lg:justify-center flex-col px-8 max-lg:pt-20">
<img src="/assets/login/sq-logo-v1.svg" class="mb-10" />
<div class="relative">
<img src="/assets/ui-box-outer.svg" class="absolute w-full h-full" />
<img src="/assets/ui-box-inner.svg" class="absolute left-2 top-2 w-[calc(100%_-_16px)] h-[calc(100%_-_16px)] max-lg:hidden" />
<img src="/assets/ui-elements/ui-box-outer.svg" class="absolute w-full h-full" />
<img src="/assets/ui-elements/ui-box-inner.svg" class="absolute left-2 top-2 w-[calc(100%_-_16px)] h-[calc(100%_-_16px)] max-lg:hidden" />
<!-- Login Form -->
<form v-show="switchForm === 'login'" @submit.prevent="loginFunc" class="relative px-6 py-11">

View File

@ -1,8 +1,5 @@
<template>
<div class="flex justify-center items-center h-dvh relative">
<GmTools v-if="gameStore.character?.role === 'gm'" />
<GmPanel v-if="gameStore.character?.role === 'gm'" />
<Game :config="gameConfig" @create="createGame">
<Scene name="main" @preload="preloadScene" @create="createScene">
<ZoneEditor v-if="isLoaded" :key="JSON.stringify(`${zoneEditorStore.zone?.id}_${zoneEditorStore.zone?.createdAt}_${zoneEditorStore.zone?.updatedAt}`)" />
@ -18,10 +15,7 @@ import { ref, onBeforeUnmount } from 'vue'
import { Game, Scene } from 'phavuer'
import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import GmTools from '@/components/gameMaster/GmTools.vue'
import ZoneEditor from '@/components/gameMaster/zoneEditor/ZoneEditor.vue'
import GmPanel from '@/components/gameMaster/GmPanel.vue'
import { loadAssets } from '@/composables/zoneComposable'
const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore()
@ -97,13 +91,7 @@ const preloadScene = async (scene: Phaser.Scene) => {
scene.load.image('BLOCK', '/assets/zone/bt_tile.png')
scene.load.image('TELEPORT', '/assets/zone/tp_tile.png')
scene.load.image('blank_tile', '/assets/zone/blank_tile.png')
scene.load.image('blank_object', '/assets/zone/blank_tile.png')
scene.load.image('waypoint', '/assets/waypoint.png')
/**
* Load the assets into the Phaser scene
*/
await loadAssets(scene)
}
const createScene = async (scene: Phaser.Scene) => {
@ -127,11 +115,3 @@ onBeforeUnmount(() => {
isLoaded.value = false
})
</script>
<style lang="scss">
canvas {
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-crisp-edges;
image-rendering: pixelated;
}
</style>

View File

@ -1,9 +1,8 @@
import axios from 'axios'
import config from '@/config'
import { useGameStore } from '@/stores/gameStore'
import { useCookies } from '@vueuse/integrations/useCookies'
export async function register(username: string, password: string, gameStore = useGameStore()) {
export async function register(username: string, password: string) {
try {
const response = await axios.post(`${config.server_endpoint}/register`, { username, password })
useCookies().set('token', response.data.token as string)
@ -13,13 +12,13 @@ export async function register(username: string, password: string, gameStore = u
}
}
export async function login(username: string, password: string, gameStore = useGameStore()) {
export async function login(username: string, password: string) {
try {
const response = await axios.post(`${config.server_endpoint}/login`, { username, password })
useCookies().set('token', response.data.token as string, {
// for whole domain
// @TODO : #190
domain: window.location.hostname.split('.').slice(-2).join('.')
// domain: window.location.hostname.split('.').slice(-2).join('.')
})
return { success: true, token: response.data.token }
} catch (error: any) {

View File

@ -1,6 +1,6 @@
import { ref } from 'vue'
import { defineStore } from 'pinia'
import type { Tile, Object, Sprite } from '@/types'
import type { Tile, Object, Sprite, CharacterType } from '@/types'
export const useAssetManagerStore = defineStore('assetManager', () => {
const tileList = ref<Tile[]>([])
@ -12,6 +12,9 @@ export const useAssetManagerStore = defineStore('assetManager', () => {
const spriteList = ref<Sprite[]>([])
const selectedSprite = ref<Sprite | null>(null)
const characterTypeList = ref<CharacterType[]>([])
const selectedCharacterType = ref<CharacterType | null>(null)
function setTileList(tiles: Tile[]) {
tileList.value = tiles
}
@ -36,6 +39,14 @@ export const useAssetManagerStore = defineStore('assetManager', () => {
selectedSprite.value = sprite
}
function setCharacterTypeList(characterTypes: CharacterType[]) {
characterTypeList.value = characterTypes
}
function setSelectedCharacterType(characterType: CharacterType | null) {
selectedCharacterType.value = characterType
}
return {
tileList,
selectedTile,
@ -43,11 +54,15 @@ export const useAssetManagerStore = defineStore('assetManager', () => {
selectedObject,
spriteList,
selectedSprite,
characterTypeList,
selectedCharacterType,
setTileList,
setSelectedTile,
setObjectList,
setCharacterTypeList,
setSelectedObject,
setSpriteList,
setSelectedSprite
setSelectedSprite,
setSelectedCharacterType
}
})

View File

@ -7,9 +7,9 @@ import { useCookies } from '@vueuse/integrations/useCookies'
export const useGameStore = defineStore('game', {
state: () => {
return {
loginMessage: null as string | null,
notifications: [] as Notification[],
assets: [] as Asset[],
isAssetsLoaded: false,
loadedAssets: [] as string[],
token: '' as string | null,
connection: null as Socket | null,
user: null as User | null,
@ -26,17 +26,11 @@ export const useGameStore = defineStore('game', {
},
uiSettings: {
isChatOpen: false,
isUserPanelOpen: false,
isCharacterProfileOpen: false,
isGmPanelOpen: false
}
}
},
getters: {
getNotifications: (state: any) => state.notifications,
getAssetByKey: (state) => {
return (key: string) => state.assets.find((asset) => asset.key === key)
}
},
actions: {
addNotification(notification: Notification) {
if (!notification.id) {
@ -47,54 +41,6 @@ export const useGameStore = defineStore('game', {
removeNotification(id: string) {
this.notifications = this.notifications.filter((notification: Notification) => notification.id !== id)
},
setAssets(assets: Asset[]) {
this.assets = assets
},
addAsset(asset: Asset) {
this.assets.push(asset)
},
addAssets(assets: Asset[]) {
this.assets = this.assets.concat(assets)
},
async fetchSpriteAssets() {
return fetch(config.server_endpoint + '/assets/sprites')
.then((response) => response.json())
.then((assets) => {
// Only add the sprites that are not already in the store
this.addAssets(assets.filter((asset: Asset) => !this.getAssetByKey(asset.key)))
return true
})
.catch((error) => {
console.error('Error fetching assets:', error)
return false
})
},
async fetchZoneAssets(zoneId: number) {
return fetch(config.server_endpoint + '/assets/zone/' + zoneId)
.then((response) => response.json())
.then((assets) => {
// Only add the zones that are not already in the store
this.addAssets(assets.filter((asset: Asset) => !this.getAssetByKey(asset.key)))
return true
})
.catch((error) => {
console.error('Error fetching assets:', error)
return false
})
},
async fetchAllZoneAssets() {
return fetch(config.server_endpoint + '/assets/zone')
.then((response) => response.json())
.then((assets) => {
// Only add the zones that are not already in the store
this.addAssets(assets.filter((asset: Asset) => !this.getAssetByKey(asset.key)))
return true
})
.catch((error) => {
console.error('Error fetching assets:', error)
return false
})
},
setToken(token: string) {
this.token = token
},
@ -122,8 +68,8 @@ export const useGameStore = defineStore('game', {
toggleChat() {
this.uiSettings.isChatOpen = !this.uiSettings.isChatOpen
},
toggleUserPanel() {
this.uiSettings.isUserPanelOpen = !this.uiSettings.isUserPanelOpen
toggleCharacterProfile() {
this.uiSettings.isCharacterProfileOpen = !this.uiSettings.isCharacterProfileOpen
},
initConnection() {
this.connection = io(config.server_endpoint, {
@ -152,15 +98,15 @@ export const useGameStore = defineStore('game', {
})
},
disconnectSocket() {
if (this.connection) this.connection.disconnect()
this.connection?.disconnect()
useCookies().remove('token', {
// for whole domain
// @TODO : #190
domain: window.location.hostname.split('.').slice(-2).join('.')
// domain: window.location.hostname.split('.').slice(-2).join('.')
})
this.assets = []
this.isAssetsLoaded = false
this.connection = null
this.token = null
this.user = null
@ -169,7 +115,7 @@ export const useGameStore = defineStore('game', {
this.isPlayerDraggingCamera = false
this.gameSettings.isCameraFollowingCharacter = false
this.uiSettings.isChatOpen = false
this.uiSettings.isUserPanelOpen = false
this.uiSettings.isCharacterProfileOpen = false
this.world.date = new Date()
this.world.isRainEnabled = false

View File

@ -1,6 +1,6 @@
import { defineStore } from 'pinia'
import { useGameStore } from '@/stores/gameStore'
import type { Zone, Object, Tile, ZoneObject, ZoneEffects } from '@/types'
import type { Zone, Object, Tile, ZoneEffect } from '@/types'
export type TeleportSettings = {
toZoneId: number
@ -12,7 +12,7 @@ export type TeleportSettings = {
export const useZoneEditorStore = defineStore('zoneEditor', {
state: () => {
return {
active: true,
active: false,
zone: null as Zone | null,
tool: 'move',
drawMode: 'tile',
@ -33,7 +33,7 @@ export const useZoneEditorStore = defineStore('zoneEditor', {
width: 0,
height: 0,
pvp: false,
effects: [] as ZoneEffects[]
zoneEffects: [] as ZoneEffect[]
},
teleportSettings: {
toZoneId: 0,
@ -66,9 +66,9 @@ export const useZoneEditorStore = defineStore('zoneEditor', {
if (!this.zone) return
this.zone.pvp = pvp
},
setZoneEffects(zoneEffects: ZoneEffects) {
setZoneEffects(zoneEffects: ZoneEffect[]) {
if (!this.zone) return
this.zone.zoneEffects = zoneEffects
this.zoneSettings.zoneEffects = zoneEffects
},
setTool(tool: string) {
this.tool = tool

View File

@ -4,10 +4,11 @@ export type Notification = {
message?: string
}
export type Asset = {
export type AssetT = {
key: string
url: string
group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
updatedAt: Date
frameCount?: number
frameWidth?: number
frameHeight?: number
@ -53,7 +54,7 @@ export type Zone = {
height: number
tiles: any | null
pvp: boolean
zoneEffects: ZoneEffects
zoneEffects: ZoneEffect[]
zoneEventTiles: ZoneEventTile[]
zoneObjects: ZoneObject[]
characters: Character[]
@ -62,7 +63,7 @@ export type Zone = {
updatedAt: Date
}
export type ZoneEffects = {
export type ZoneEffect = {
id: string
zoneId: number
zone: Zone
@ -136,8 +137,8 @@ export type CharacterType = {
gender: CharacterGender
race: CharacterRace
characters: Character[]
spriteId: string
sprite: Sprite
spriteId?: string
sprite?: Sprite
createdAt: Date
updatedAt: Date
}

View File

@ -0,0 +1,72 @@
import config from '@/config'
import Dexie from 'dexie'
class AssetManager extends Dexie {
assets!: Dexie.Table<
{
key: string
data: Blob
group: string
updatedAt: Date
frameCount?: number
frameWidth?: number
frameHeight?: number
},
string
>
constructor() {
super('Assets')
this.version(1).stores({
assets: 'key, group'
})
}
async downloadAsset(key: string, url: string, group: string, updatedAt: Date, frameCount?: number, frameWidth?: number, frameHeight?: number) {
try {
const response = await fetch(config.server_endpoint + url)
const blob = await response.blob()
await this.assets.put({ key, data: blob, group, updatedAt, frameCount, frameWidth, frameHeight })
} catch (error) {
console.error(`Failed to add asset ${key}:`, error)
}
}
async getAsset(key: string) {
try {
const asset = await this.assets.get(key)
if (asset) {
return {
...asset,
data: URL.createObjectURL(asset.data)
}
}
} catch (error) {
console.error(`Failed to retrieve asset ${key}:`, error)
}
return null
}
async getAssetsByGroup(group: string) {
try {
const assets = await this.assets.where('group').equals(group).toArray()
return assets.map((asset) => ({
...asset,
data: URL.createObjectURL(asset.data)
}))
} catch (error) {
console.error(`Failed to retrieve assets for group ${group}:`, error)
return []
}
}
async deleteAsset(key: string) {
try {
await this.assets.delete(key)
} catch (error) {
console.error(`Failed to delete asset ${key}:`, error)
}
}
}
export const useAssetManager = new AssetManager()