Compare commits
67 Commits
feature/13
...
feature/#2
Author | SHA1 | Date | |
---|---|---|---|
9f50b062b0 | |||
347fc0e1e8 | |||
715fe5c318 | |||
df19c1094c | |||
279b9bc7a3 | |||
6adfc727c5 | |||
1aa07daa35 | |||
36901782ea | |||
62f2e1124c | |||
f6e1a54e74 | |||
6e2885cba6 | |||
252d9c87fd | |||
856829b605 | |||
a0f0b40ed3 | |||
68222ab511 | |||
fe804037d0 | |||
5d288772b5 | |||
3c9b92ccbd | |||
13cb46658f | |||
222614b856 | |||
9f10db142b | |||
860fe705c6 | |||
352ec3fad8 | |||
c7a3d74408 | |||
1e0da5f7cc | |||
7ce054191a | |||
34f547f0a6 | |||
14e07aa4a1 | |||
95dcf237cf | |||
5cc1821922 | |||
9d774bcb18 | |||
c6869f47b1 | |||
390b9517e0 | |||
2497da30b7 | |||
66e56d3626 | |||
d68ee120ab | |||
aff32c33c7 | |||
3c8744dc75 | |||
774871510e | |||
e61b705031 | |||
3902c611fa | |||
3765cfe5e9 | |||
2fad54fd26 | |||
be3cbf77bf | |||
f24a498246 | |||
9686381745 | |||
e42c530685 | |||
e56e078042 | |||
13be1a38fa | |||
27e857b9a6 | |||
2b2c290db0 | |||
245b50c1fd | |||
32dc7a2963 | |||
a9c2b209d9 | |||
a6c22df528 | |||
7dd2d70eca | |||
5fc3547d9c | |||
b5c222cc05 | |||
8b1efca7b8 | |||
1bdd2bc75a | |||
24dff8d920 | |||
8b98fc5c4e | |||
934ae50d8e | |||
7504e3719e | |||
34bd103ec2 | |||
295ce98e33 | |||
1cfdf1857e |
@ -27,5 +27,6 @@ RUN npm run build-ntc
|
|||||||
# Production stage
|
# Production stage
|
||||||
FROM nginx:1.26.1-alpine
|
FROM nginx:1.26.1-alpine
|
||||||
COPY --from=builder /usr/src/app/dist /usr/share/nginx/html
|
COPY --from=builder /usr/src/app/dist /usr/share/nginx/html
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
CMD ["nginx", "-g", "daemon off;"]
|
@ -3,8 +3,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/favicon.ico">
|
<link rel="icon" href="/favicon.ico">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<title>New Quest - Play</title>
|
<title>Sylvan Quest - Play</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
16
nginx.conf
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
739
package-lock.json
generated
@ -18,11 +18,12 @@
|
|||||||
"@vueuse/core": "^10.5.0",
|
"@vueuse/core": "^10.5.0",
|
||||||
"@vueuse/integrations": "^10.5.0",
|
"@vueuse/integrations": "^10.5.0",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"phaser": "^3.85.2",
|
"dexie": "^4.0.8",
|
||||||
|
"phaser": "^3.86.0",
|
||||||
"pinia": "^2.1.6",
|
"pinia": "^2.1.6",
|
||||||
"socket.io-client": "^4.8.0",
|
"socket.io-client": "^4.8.0",
|
||||||
"universal-cookie": "^6.1.3",
|
"universal-cookie": "^6.1.3",
|
||||||
"vue": "^3.5.10",
|
"vue": "^3.5.12",
|
||||||
"zod": "^3.22.2"
|
"zod": "^3.22.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -48,8 +49,8 @@
|
|||||||
"sass": "^1.79.4",
|
"sass": "^1.79.4",
|
||||||
"tailwindcss": "^3.4.13",
|
"tailwindcss": "^3.4.13",
|
||||||
"typescript": "~5.6.2",
|
"typescript": "~5.6.2",
|
||||||
"vite": "^5.4.8",
|
"vite": "^5.4.9",
|
||||||
"vite-plugin-vue-devtools": "^7.4.6",
|
"vite-plugin-vue-devtools": "^7.5.2",
|
||||||
"vitest": "^2.0.3",
|
"vitest": "^2.0.3",
|
||||||
"vue-tsc": "^1.6.5"
|
"vue-tsc": "^1.6.5"
|
||||||
}
|
}
|
||||||
|
20
public/assets/icons/chat-icon.svg
Normal file
After Width: | Height: | Size: 597 KiB |
Before Width: | Height: | Size: 1.4 KiB |
@ -1 +1,3 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" id="screenshot-7904a66d-0c2c-80b0-8004-7aa8e4dbc6e0" viewBox="5853.755 6642.086 23.788 23.788" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-7904a66d-0c2c-80b0-8004-7aa8e4dbc6e0"><g class="fills" id="fills-7904a66d-0c2c-80b0-8004-7aa8e4dbc6e0"><path d="M5867.837,6654.000L5877.045,6644.792C5877.199,6644.649,5877.322,6644.476,5877.408,6644.284C5877.493,6644.093,5877.539,6643.886,5877.543,6643.676C5877.546,6643.466,5877.508,6643.258,5877.429,6643.063C5877.350,6642.869,5877.234,6642.692,5877.085,6642.544C5876.937,6642.395,5876.760,6642.278,5876.565,6642.200C5876.371,6642.121,5876.163,6642.082,5875.953,6642.086C5875.743,6642.090,5875.536,6642.136,5875.344,6642.221C5875.153,6642.307,5874.980,6642.430,5874.837,6642.583L5865.629,6651.792L5856.420,6642.583C5856.124,6642.307,5855.732,6642.157,5855.328,6642.164C5854.923,6642.171,5854.537,6642.335,5854.250,6642.622C5853.964,6642.908,5853.800,6643.294,5853.793,6643.699C5853.786,6644.104,5853.936,6644.495,5854.212,6644.792L5863.420,6654.000L5854.212,6663.208C5853.919,6663.501,5853.755,6663.898,5853.755,6664.312C5853.755,6664.726,5853.919,6665.124,5854.212,6665.417C5854.505,6665.709,5854.902,6665.874,5855.316,6665.874C5855.730,6665.874,5856.127,6665.709,5856.420,6665.417L5865.629,6656.208L5874.837,6665.417C5875.130,6665.709,5875.527,6665.874,5875.941,6665.874C5876.355,6665.874,5876.753,6665.709,5877.045,6665.417C5877.338,6665.124,5877.502,6664.726,5877.502,6664.312C5877.502,6663.898,5877.338,6663.501,5877.045,6663.208L5867.837,6654.000ZZ" style="fill: rgb(255, 255, 255);"/></g></g></svg>
|
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7.91481 9.08504C7.71955 9.2803 7.40297 9.2803 7.20771 9.08504L5.55709 7.43442C5.36183 7.23916 5.04524 7.23916 4.84998 7.43442L2.99578 9.28862C2.80052 9.48389 2.48393 9.48389 2.28867 9.28863L1.00416 8.00412C0.808899 7.80885 0.808899 7.49227 1.00416 7.29701L2.85837 5.4428C3.05363 5.24754 3.05363 4.93096 2.85837 4.7357L0.914865 2.7922C0.719603 2.59693 0.719603 2.28035 0.914865 2.08509L2.07053 0.929423C2.26579 0.734161 2.58238 0.734162 2.77764 0.929424L4.72114 2.87293C4.9164 3.06819 5.23298 3.06819 5.42825 2.87293L7.297 1.00417C7.49226 0.808906 7.80885 0.808906 8.00411 1.00417L9.28862 2.28868C9.48388 2.48394 9.48388 2.80052 9.28862 2.99578L7.41986 4.86454C7.2246 5.0598 7.2246 5.37639 7.41986 5.57165L9.07048 7.22227C9.26574 7.41753 9.26574 7.73411 9.07048 7.92937L7.91481 9.08504Z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 915 B |
7
public/assets/icons/eye-closed.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg width="20" height="16" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g>
|
||||||
|
<path fill="none" d="M0 0h24v24H0z"/>
|
||||||
|
<path d="M17.882 19.297A10.949 10.949 0 0 1 12 21c-5.392 0-9.878-3.88-10.819-9a10.982 10.982 0 0 1 3.34-6.066L1.392 2.808l1.415-1.415 19.799 19.8-1.415 1.414-3.31-3.31zM5.935 7.35A8.965 8.965 0 0 0 3.223 12a9.005 9.005 0 0 0 13.201 5.838l-2.028-2.028A4.5 4.5 0 0 1 8.19 9.604L5.935 7.35zm6.979 6.978l-3.242-3.242a2.5 2.5 0 0 0 3.241 3.241zm7.893 2.264l-1.431-1.43A8.935 8.935 0 0 0 20.777 12 9.005 9.005 0 0 0 9.552 5.338L7.974 3.76C9.221 3.27 10.58 3 12 3c5.392 0 9.878 3.88 10.819 9a10.947 10.947 0 0 1-2.012 4.592zm-9.084-9.084a4.5 4.5 0 0 1 4.769 4.769l-4.77-4.769z" fill="#808080"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 788 B |
BIN
public/assets/icons/f1-icon.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
public/assets/icons/f2-icon.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
public/assets/icons/f3-icon.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
public/assets/icons/f4-icon.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
public/assets/icons/f5-icon.png
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
public/assets/icons/f6-icon.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
public/assets/icons/f7-icon.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
public/assets/icons/f8-icon.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
51
public/assets/icons/increase-size-option.svg
Normal 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 |
25
public/assets/icons/map-icon.svg
Normal file
After Width: | Height: | Size: 600 KiB |
10
public/assets/icons/menu-icon.svg
Normal file
After Width: | Height: | Size: 600 KiB |
10
public/assets/icons/minus-icon.svg
Normal file
After Width: | Height: | Size: 597 KiB |
Before Width: | Height: | Size: 325 B After Width: | Height: | Size: 597 KiB |
@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
<?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 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M21 15L15 21M21 8L8 21" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M21 15L15 21M21 8L8 21" stroke="#4d4d4d" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</svg>
|
</svg>
|
Before Width: | Height: | Size: 346 B After Width: | Height: | Size: 346 B |
25
public/assets/icons/socials-icon.svg
Normal file
After Width: | Height: | Size: 599 KiB |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 2.3 KiB |
15
public/assets/login/sq-logo-v1.svg
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
public/assets/music/click-btn.mp3
Normal file
BIN
public/assets/music/login.mp3
Normal file
@ -1 +0,0 @@
|
|||||||
<svg width="290" xmlns="http://www.w3.org/2000/svg" height="87" id="screenshot-e9942e24-155b-8096-8004-7eb5ea5d2669" viewBox="0 0 290 87" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-e9942e24-155b-8096-8004-7eb5ea5d2669"><g class="fills" id="fills-e9942e24-155b-8096-8004-7eb5ea5d2669"><path d="M286.515,78.278C282.911,83.543,276.856,87.000,270.000,87.000L20.000,87.000C8.962,87.000,0.000,78.038,0.000,67.000L0.000,20.000C0.000,8.962,8.962,0.000,20.000,0.000L270.000,0.000C276.834,0.000,282.872,3.435,286.480,8.671C268.843,10.411,255.000,25.352,255.000,43.500C255.000,61.610,268.784,76.525,286.515,78.278ZM290.000,20.000L290.000,67.000" style="fill: #fff; fill-opacity: 1;"/></g></g></svg>
|
|
Before Width: | Height: | Size: 726 B |
@ -1 +0,0 @@
|
|||||||
<svg width="290" xmlns="http://www.w3.org/2000/svg" height="87" id="screenshot-e38d8c7f-bba0-801b-8004-7d6eeffceb00" viewBox="4058.354 6110 290 87" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-e38d8c7f-bba0-801b-8004-7d6eeffceb00"><g class="fills" id="fills-e38d8c7f-bba0-801b-8004-7d6eeffceb00"><path d="M4061.840,6118.722C4065.444,6113.457,4071.499,6110.000,4078.354,6110.000L4328.354,6110.000C4339.393,6110.000,4348.354,6118.962,4348.354,6130.000L4348.354,6177.000C4348.354,6188.038,4339.393,6197.000,4328.354,6197.000L4078.354,6197.000C4071.521,6197.000,4065.483,6193.565,4061.875,6188.329C4079.512,6186.589,4093.354,6171.648,4093.354,6153.500C4093.354,6135.390,4079.571,6120.475,4061.840,6118.722ZM4058.354,6177.000L4058.354,6130.000" style="fill: #fff; fill-opacity: 1;"/></g></g></svg>
|
|
Before Width: | Height: | Size: 829 B |
22
public/assets/ui-border-4-corners-light.svg
Normal file
After Width: | Height: | Size: 598 KiB |
26
public/assets/ui-border-4-corners.svg
Normal file
After Width: | Height: | Size: 471 KiB |
Before Width: | Height: | Size: 301 KiB After Width: | Height: | Size: 301 KiB |
Before Width: | Height: | Size: 400 KiB After Width: | Height: | Size: 400 KiB |
23
public/assets/ui-rect-border-4-corners.svg
Normal file
After Width: | Height: | Size: 470 KiB |
BIN
public/assets/ui-texture.png
Normal file
After Width: | Height: | Size: 135 KiB |
37
src/App.vue
@ -1,36 +1,33 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="overflow-hidden">
|
|
||||||
<Notifications />
|
<Notifications />
|
||||||
<Login v-if="screen === 'login'" />
|
<GmTools v-if="gameStore.character?.role === 'gm'" />
|
||||||
<!-- <Register v-if="screen === 'register'" />-->
|
<GmPanel v-if="gameStore.character?.role === 'gm'" />
|
||||||
<Characters v-if="screen === 'characters'" />
|
|
||||||
<Game v-if="screen === 'game'" />
|
<component :is="currentScreen" />
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
import Notifications from '@/components/utilities/Notifications.vue'
|
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 Login from '@/screens/Login.vue'
|
||||||
// import Register from '@/screens/Register.vue'
|
|
||||||
import Characters from '@/screens/Characters.vue'
|
import Characters from '@/screens/Characters.vue'
|
||||||
import Game from '@/screens/Game.vue'
|
import Game from '@/screens/Game.vue'
|
||||||
|
// import Loading from '@/screens/Loading.vue'
|
||||||
|
import ZoneEditor from '@/screens/ZoneEditor.vue'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
|
|
||||||
const screen = computed(() => {
|
const currentScreen = computed(() => {
|
||||||
if (!gameStore.connection) {
|
// if (!gameStore.isAssetsLoaded) return Loading
|
||||||
return 'login'
|
if (!gameStore.connection) return Login
|
||||||
} else if (gameStore.token && gameStore.connection) {
|
if (!gameStore.token) return Login
|
||||||
if (gameStore.character) {
|
if (!gameStore.character) return Characters
|
||||||
return 'game'
|
if (zoneEditorStore.active) return ZoneEditor
|
||||||
}
|
return Game
|
||||||
return 'characters'
|
|
||||||
}
|
|
||||||
return 'login' // default fallback
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Disable right click
|
|
||||||
addEventListener('contextmenu', (event) => event.preventDefault())
|
|
||||||
</script>
|
</script>
|
||||||
|
BIN
src/assets/fonts/upheavtt.ttf
Normal file
@ -4,11 +4,15 @@
|
|||||||
|
|
||||||
// Fonts
|
// Fonts
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@300..700&family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@300..700&family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Upheaval';
|
||||||
|
src: url('../fonts/upheavtt.ttf');
|
||||||
|
}
|
||||||
|
|
||||||
//Globals
|
//Globals
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-black m-0 select-none overscroll-none overflow-hidden;
|
@apply bg-black m-0 select-none;
|
||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
|
|
||||||
@ -16,6 +20,9 @@ body {
|
|||||||
-webkit-user-select: none; /* Safari */
|
-webkit-user-select: none; /* Safari */
|
||||||
-ms-user-select: none; /* IE 10 and IE 11 */
|
-ms-user-select: none; /* IE 10 and IE 11 */
|
||||||
user-select: none; /* Standard syntax */
|
user-select: none; /* Standard syntax */
|
||||||
|
|
||||||
|
// Disable pinch zoom
|
||||||
|
touch-action: pan-x pan-y;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
@ -57,7 +64,7 @@ input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.input-field {
|
.input-field {
|
||||||
@apply px-4 py-3 text-base focus-visible:outline-none bg-gray border border-solid border-gray-500 rounded text-gray-300;
|
@apply px-4 py-2.5 text-base leading-5 focus-visible:outline-none bg-gray border border-solid border-gray-500 rounded text-gray-300;
|
||||||
&.inactive {
|
&.inactive {
|
||||||
@apply bg-gray-600/50 hover:cursor-not-allowed;
|
@apply bg-gray-600/50 hover:cursor-not-allowed;
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
@ -83,7 +90,7 @@ button {
|
|||||||
@apply text-center;
|
@apply text-center;
|
||||||
|
|
||||||
&.btn-cyan {
|
&.btn-cyan {
|
||||||
@apply bg-cyan text-gray-50 text-base rounded py-3;
|
@apply bg-cyan text-gray-50 text-base leading-5 rounded py-2.5;
|
||||||
|
|
||||||
&.active,
|
&.active,
|
||||||
&:hover {
|
&:hover {
|
||||||
@ -92,7 +99,7 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.btn-red {
|
&.btn-red {
|
||||||
@apply bg-red text-gray-50;
|
@apply bg-red text-gray-50 text-base leading-5 rounded py-2.5;
|
||||||
|
|
||||||
&.active,
|
&.active,
|
||||||
&:hover {
|
&:hover {
|
||||||
@ -100,11 +107,37 @@ button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.btn-empty {
|
||||||
|
@apply text-gray-50 border-2 border-solid border-gray-500 text-base leading-5 rounded py-2.5;
|
||||||
|
|
||||||
|
&.active,
|
||||||
|
&:hover {
|
||||||
|
@apply bg-gray-700 border-gray-700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@apply cursor-pointer;
|
@apply cursor-pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.eye-open {
|
||||||
|
@apply bg-[url('/assets/icons/eye-closed.svg')] w-5 h-4 right-2.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-pixel {
|
||||||
|
@apply text-white font-ui drop-shadow-pixel-black;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
@apply hidden;
|
@apply hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
image-rendering: -moz-crisp-edges;
|
||||||
|
image-rendering: -webkit-crisp-edges;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
@ -1,28 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<Scene name="effects" @preload="preloadScene" @create="createScene" @update="updateScene">
|
<Scene name="effects" @preload="preloadScene" @create="createScene" @update="updateScene"> </Scene>
|
||||||
</Scene>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Scene } from 'phavuer'
|
import { Scene } from 'phavuer'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useZoneStore } from '@/stores/zoneStore'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
import { onBeforeUnmount, ref, watch } from 'vue'
|
||||||
import { onBeforeMount, onBeforeUnmount, ref } from 'vue'
|
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const zoneStore = useZoneStore()
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
|
||||||
|
|
||||||
const sceneRef = ref<Phaser.Scene | null>(null)
|
const sceneRef = ref<Phaser.Scene | null>(null)
|
||||||
|
|
||||||
// Effect-related refs
|
// 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 rainEmitter = ref<Phaser.GameObjects.Particles.ParticleEmitter | null>(null)
|
||||||
const fogSprite = ref<Phaser.GameObjects.Sprite | 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) => {
|
const preloadScene = async (scene: Phaser.Scene) => {
|
||||||
scene.load.image('raindrop', 'assets/raindrop.png')
|
scene.load.image('raindrop', 'assets/raindrop.png')
|
||||||
scene.load.image('fog', 'assets/fog.png')
|
scene.load.image('fog', 'assets/fog.png')
|
||||||
@ -30,28 +23,18 @@ const preloadScene = async (scene: Phaser.Scene) => {
|
|||||||
|
|
||||||
const createScene = async (scene: Phaser.Scene) => {
|
const createScene = async (scene: Phaser.Scene) => {
|
||||||
sceneRef.value = scene
|
sceneRef.value = scene
|
||||||
createDayNightCycle(scene)
|
createLightEffect(scene)
|
||||||
createRainEffect(scene)
|
createRainEffect(scene)
|
||||||
createFogEffect(scene)
|
createFogEffect(scene)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateScene = (scene: Phaser.Scene, time: number) => {
|
const updateScene = () => {
|
||||||
updateDayNightCycle(time)
|
updateEffects()
|
||||||
updateFogEffect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const createDayNightCycle = (scene: Phaser.Scene) => {
|
const createLightEffect = (scene: Phaser.Scene) => {
|
||||||
dayNightCycle.value = scene.add.graphics()
|
lightEffect.value = scene.add.graphics()
|
||||||
dayNightCycle.value.setDepth(1000)
|
lightEffect.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 createRainEffect = (scene: Phaser.Scene) => {
|
const createRainEffect = (scene: Phaser.Scene) => {
|
||||||
@ -66,44 +49,62 @@ const createRainEffect = (scene: Phaser.Scene) => {
|
|||||||
blendMode: 'ADD'
|
blendMode: 'ADD'
|
||||||
})
|
})
|
||||||
rainEmitter.value.setDepth(900)
|
rainEmitter.value.setDepth(900)
|
||||||
toggleRain(true) // Start with rain off
|
rainEmitter.value.stop()
|
||||||
}
|
|
||||||
|
|
||||||
const toggleRain = (isRaining: boolean) => {
|
|
||||||
if (rainEmitter.value) {
|
|
||||||
rainEmitter.value.setVisible(isRaining)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const createFogEffect = (scene: Phaser.Scene) => {
|
const createFogEffect = (scene: Phaser.Scene) => {
|
||||||
fogSprite.value = scene.add.sprite(window.innerWidth / 2, window.innerHeight / 2, 'fog')
|
fogSprite.value = scene.add.sprite(window.innerWidth / 2, window.innerHeight / 2, 'fog')
|
||||||
fogSprite.value.setScale(2)
|
fogSprite.value.setScale(2)
|
||||||
fogSprite.value.setAlpha(0) // yeetdasasdasd
|
fogSprite.value.setAlpha(0)
|
||||||
fogSprite.value.setDepth(950) // yeetdasasdasd
|
fogSprite.value.setDepth(950)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateFogEffect = () => {
|
const updateEffects = () => {
|
||||||
if (fogSprite.value) {
|
const effects = zoneStore.zone?.zoneEffects || []
|
||||||
// Example: Oscillate fog opacity
|
|
||||||
const fogOpacity = (Math.sin(Date.now() / 5000) + 1) / 2 * 0.3
|
effects.forEach((effect) => {
|
||||||
fogSprite.value.setAlpha(fogOpacity)
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expose methods to control effects
|
const updateFogEffect = (strength: number) => {
|
||||||
const controlEffects = {
|
if (!fogSprite.value) return
|
||||||
toggleRain,
|
fogSprite.value.setAlpha(strength / 100)
|
||||||
setFogDensity: (density: number) => {
|
|
||||||
if (fogSprite.value) {
|
|
||||||
fogSprite.value.setAlpha(density)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make control methods available to parent components
|
watch(() => zoneStore.zone?.zoneEffects, updateEffects, { deep: true })
|
||||||
defineExpose(controlEffects)
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (sceneRef.value) sceneRef.value.scene.remove('effects')
|
if (sceneRef.value) sceneRef.value.scene.remove('effects')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// @TODO : Fix resize issue
|
||||||
</script>
|
</script>
|
@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-wrap items-center input-field gap-1">
|
<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">
|
<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>
|
<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>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal :isModalOpen="gameStore.uiSettings.isGmPanelOpen" @modal:close="() => gameStore.toggleGmPanel()" :modal-width="1000" :modal-height="650" :can-full-screen="true">
|
<Modal :isModalOpen="gameStore.uiSettings.isGmPanelOpen" @modal:close="() => gameStore.toggleGmPanel()" :modal-width="1000" :modal-height="650" :can-full-screen="true">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="m-0 font-medium shrink-0 text-gray-300">GM Panel</h3>
|
|
||||||
<div class="flex gap-1.5 flex-wrap">
|
<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">General</button>
|
||||||
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Users</button>
|
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Users</button>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal :isModalOpen="true" :closable="false" :is-resizable="false" :modal-width="modalWidth" :modal-height="modalHeight" :modal-position-x="posXY.x" :modal-position-y="posXY.y">
|
<Modal :isModalOpen="true" :closable="false" :is-resizable="false" :modal-width="modalWidth" :modal-height="modalHeight" :modal-position-x="posXY.x" :modal-position-y="posXY.y">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="m-0 font-medium shrink-0 text-gray-300">GM tools</h3>
|
<h3 class="m-0 font-medium shrink-0 text-white">GM tools</h3>
|
||||||
</template>
|
</template>
|
||||||
<template #modalBody>
|
<template #modalBody>
|
||||||
<div class="content flex flex-col gap-2.5 m-4 h-20">
|
<div class="content flex flex-col gap-2.5 m-4 h-20">
|
||||||
@ -20,7 +20,7 @@ import { onMounted, ref } from 'vue'
|
|||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const modalWidth = ref(200)
|
const modalWidth = ref(200)
|
||||||
const modalHeight = ref(180)
|
const modalHeight = ref(170)
|
||||||
|
|
||||||
let posXY = ref({ x: 0, y: 0 })
|
let posXY = ref({ x: 0, y: 0 })
|
||||||
|
|
||||||
|
@ -1,56 +1,70 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full w-full relative">
|
<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 -->
|
<!-- Asset Categories -->
|
||||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'tiles' }" @click="() => (selectedCategory = 'tiles')">
|
<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-cyan-200"></div>
|
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'objects' }" @click="() => (selectedCategory = 'objects')">
|
<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-cyan-200"></div>
|
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'sprites' }" @click="() => (selectedCategory = 'sprites')">
|
<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-cyan-200"></div>
|
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer">
|
<a class="relative p-2.5 hover:cursor-pointer">
|
||||||
<span>Items</span>
|
<span>Items</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
|
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer">
|
<a class="relative p-2.5 hover:cursor-pointer">
|
||||||
<span>NPC's</span>
|
<span>NPC's</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
|
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer">
|
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'shops' }" @click="() => (selectedCategory = 'shops')">
|
||||||
<span>Characters</span>
|
<span :class="{ 'text-white': selectedCategory === 'shops' }">Shops</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
|
<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>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer">
|
<a class="relative p-2.5 hover:cursor-pointer">
|
||||||
<span>Mounts</span>
|
<span>Mounts</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
|
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer">
|
<a class="relative p-2.5 hover:cursor-pointer">
|
||||||
<span>Pets</span>
|
<span>Pets</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
|
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute w-px bg-cyan-200 h-full top-0 left-1/6"></div>
|
<div class="absolute w-px bg-gray-500 h-full top-0 left-1/6"></div>
|
||||||
|
|
||||||
<!-- Assets list -->
|
<!-- Assets list -->
|
||||||
<div class="overflow-auto h-full w-4/12 flex flex-col relative">
|
<div class="overflow-auto h-full w-4/12 flex flex-col relative">
|
||||||
<TileList v-if="selectedCategory === 'tiles'" />
|
<TileList v-if="selectedCategory === 'tiles'" />
|
||||||
<ObjectList v-if="selectedCategory === 'objects'" />
|
<ObjectList v-if="selectedCategory === 'objects'" />
|
||||||
<SpriteList v-if="selectedCategory === 'sprites'" />
|
<SpriteList v-if="selectedCategory === 'sprites'" />
|
||||||
|
<CharacterTypeList v-if="selectedCategory === 'characterTypes'" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute w-px bg-cyan-200 h-full top-0 left-1/2"></div>
|
<div class="absolute w-px bg-gray-500 h-full top-0 left-1/2"></div>
|
||||||
|
|
||||||
<!-- Asset details -->
|
<!-- Asset details -->
|
||||||
<div class="flex w-1/2 after:hidden flex-col relative overflow-auto">
|
<div class="flex w-1/2 after:hidden flex-col relative overflow-auto">
|
||||||
<TileDetails v-if="selectedCategory === 'tiles' && assetManagerStore.selectedTile" />
|
<TileDetails v-if="selectedCategory === 'tiles' && assetManagerStore.selectedTile" />
|
||||||
<ObjectDetails v-if="selectedCategory === 'objects' && assetManagerStore.selectedObject" />
|
<ObjectDetails v-if="selectedCategory === 'objects' && assetManagerStore.selectedObject" />
|
||||||
<SpriteDetails v-if="selectedCategory === 'sprites' && assetManagerStore.selectedSprite" />
|
<SpriteDetails v-if="selectedCategory === 'sprites' && assetManagerStore.selectedSprite" />
|
||||||
|
<CharacterTypeDetails v-if="selectedCategory === 'characterTypes' && assetManagerStore.selectedCharacterType" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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 ObjectDetails from '@/components/gameMaster/assetManager/partials/object/ObjectDetails.vue'
|
||||||
import SpriteList from '@/components/gameMaster/assetManager/partials/sprite/SpriteList.vue'
|
import SpriteList from '@/components/gameMaster/assetManager/partials/sprite/SpriteList.vue'
|
||||||
import SpriteDetails from '@/components/gameMaster/assetManager/partials/sprite/SpriteDetails.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 assetManagerStore = useAssetManagerStore()
|
||||||
const selectedCategory = ref('tiles')
|
const selectedCategory = ref('tiles')
|
||||||
|
@ -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>
|
@ -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>
|
@ -4,7 +4,7 @@
|
|||||||
<div class="filler"></div>
|
<div class="filler"></div>
|
||||||
<img class="max-h-56" :src="`${config.server_endpoint}/assets/objects/${selectedObject?.id}.png`" :alt="'Object ' + selectedObject?.id" />
|
<img class="max-h-56" :src="`${config.server_endpoint}/assets/objects/${selectedObject?.id}.png`" :alt="'Object ' + selectedObject?.id" />
|
||||||
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeObject">Remove</button>
|
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeObject">Remove</button>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
|
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="m-2.5 p-2.5 block">
|
<div class="m-2.5 p-2.5 block">
|
||||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject">
|
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject">
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative p-2.5 flex items-center gap-x-2.5">
|
<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" />
|
<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" />
|
<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">
|
<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" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||||
</svg>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
|
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-bind="containerProps" class="overflow-y-auto relative" @scroll="onScroll">
|
<div v-bind="containerProps" class="overflow-y-auto relative" @scroll="onScroll">
|
||||||
<div v-bind="wrapperProps" ref="elementToScroll">
|
<div v-bind="wrapperProps" ref="elementToScroll">
|
||||||
@ -18,10 +18,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<span>{{ object.name }}</span>
|
<span>{{ object.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
|
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<button class="left-[calc(50%_-_60px)] fixed bottom-2.5 min-w-[unset] w-12 h-12 rounded-lg 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="" />
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<div class="w-full flex gap-2 mt-2 pb-4 relative">
|
<div class="w-full flex gap-2 mt-2 pb-4 relative">
|
||||||
<button class="btn-cyan px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="saveSprite">Save</button>
|
<button class="btn-cyan px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="saveSprite">Save</button>
|
||||||
<button class="btn-red px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="deleteSprite">Delete</button>
|
<button class="btn-red px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="deleteSprite">Delete</button>
|
||||||
<div class="w-[calc(100%_+_32px)] absolute left-[-15px] bottom-0 h-px bg-cyan-200"></div>
|
<div class="w-[calc(100%_+_32px)] absolute left-[-15px] bottom-0 h-px bg-gray-500"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative p-2.5 flex items-center gap-x-2.5">
|
<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" />
|
<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">
|
<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" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
|
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-bind="containerProps" class="overflow-y-auto relative" @scroll="onScroll">
|
<div v-bind="containerProps" class="overflow-y-auto relative" @scroll="onScroll">
|
||||||
<div v-bind="wrapperProps" ref="elementToScroll">
|
<div v-bind="wrapperProps" ref="elementToScroll">
|
||||||
@ -14,10 +14,10 @@
|
|||||||
<div class="flex items-center gap-2.5">
|
<div class="flex items-center gap-2.5">
|
||||||
<span>{{ sprite.name }}</span>
|
<span>{{ sprite.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
|
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<button class="left-[calc(50%_-_60px)] fixed bottom-2.5 min-w-[unset] w-12 h-12 rounded-lg 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="" />
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="filler"></div>
|
<div class="filler"></div>
|
||||||
<img class="max-h-72" :src="`${config.server_endpoint}/assets/tiles/${selectedTile?.id}.png`" :alt="'Tile ' + selectedTile?.id" />
|
<img class="max-h-72" :src="`${config.server_endpoint}/assets/tiles/${selectedTile?.id}.png`" :alt="'Tile ' + selectedTile?.id" />
|
||||||
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="deleteTile">Delete</button>
|
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="deleteTile">Delete</button>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
|
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="m-2.5 p-2.5 block">
|
<div class="m-2.5 p-2.5 block">
|
||||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveTile">
|
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveTile">
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative p-2.5 flex items-center gap-x-2.5">
|
<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" />
|
<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" />
|
<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">
|
<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" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||||
</svg>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
|
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-bind="containerProps" class="overflow-y-auto relative" @scroll="onScroll">
|
<div v-bind="containerProps" class="overflow-y-auto relative" @scroll="onScroll">
|
||||||
<div v-bind="wrapperProps" ref="elementToScroll">
|
<div v-bind="wrapperProps" ref="elementToScroll">
|
||||||
@ -18,10 +18,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<span>{{ tile.name }}</span>
|
<span>{{ tile.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
|
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<button class="left-[calc(50%_-_60px)] fixed bottom-2.5 min-w-[unset] w-12 h-12 rounded-lg 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="" />
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
118
src/components/gameMaster/zoneEditor/EventTiles.vue
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
<template>
|
||||||
|
<Image v-for="tile in zoneEditorStore.zone?.zoneEventTiles" v-bind="getImageProps(tile)" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
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'
|
||||||
|
|
||||||
|
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>
|
213
src/components/gameMaster/zoneEditor/Objects.vue
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
<template>
|
||||||
|
<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">
|
||||||
|
import { uuidv4 } from '@/utilities'
|
||||||
|
import { calculateIsometricDepth, getTile, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||||
|
import { Image, useScene } from 'phavuer'
|
||||||
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
|
import type { ZoneObject } from '@/types'
|
||||||
|
import SelectedZoneObject from '@/components/gameMaster/zoneEditor/partials/SelectedZoneObject.vue'
|
||||||
|
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 getImageProps(zoneObject: ZoneObject) {
|
||||||
|
return {
|
||||||
|
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 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 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 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
|
||||||
|
|
||||||
|
const newObject = {
|
||||||
|
id: uuidv4(),
|
||||||
|
zoneId: zoneEditorStore.zone.id,
|
||||||
|
zone: zoneEditorStore.zone,
|
||||||
|
objectId: zoneEditorStore.selectedObject.id,
|
||||||
|
object: zoneEditorStore.selectedObject,
|
||||||
|
depth: 0,
|
||||||
|
isRotated: false,
|
||||||
|
positionX: tile.x,
|
||||||
|
positionY: tile.y
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new object to zoneObjects
|
||||||
|
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, 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)
|
||||||
|
})
|
||||||
|
|
||||||
|
// watch zoneEditorStore.objectList and update originX and originY of objects in zoneObjects
|
||||||
|
watch(
|
||||||
|
() => zoneEditorStore.objectList,
|
||||||
|
(newObjects) => {
|
||||||
|
if (!zoneEditorStore.zone) return
|
||||||
|
|
||||||
|
const updatedZoneObjects = zoneEditorStore.zone.zoneObjects.map((zoneObject) => {
|
||||||
|
const updatedObject = newObjects.find((obj) => obj.id === zoneObject.object.id)
|
||||||
|
if (updatedObject) {
|
||||||
|
return {
|
||||||
|
...zoneObject,
|
||||||
|
object: {
|
||||||
|
...zoneObject.object,
|
||||||
|
originX: updatedObject.originX,
|
||||||
|
originY: updatedObject.originY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
if (updatedObject) {
|
||||||
|
zoneEditorStore.setSelectedObject({
|
||||||
|
...zoneEditorStore.selectedObject,
|
||||||
|
originX: updatedObject.originX,
|
||||||
|
originY: updatedObject.originY
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
</script>
|
146
src/components/gameMaster/zoneEditor/Tiles.vue
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
<template>
|
||||||
|
<Controls :layer="tiles" :depth="0" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import config from '@/config'
|
||||||
|
import { useScene } from 'phavuer'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
|
import { onBeforeMount, onBeforeUnmount } from 'vue'
|
||||||
|
import { createTileArray, getTile, placeTile, setLayerTiles } from '@/composables/zoneComposable'
|
||||||
|
import Controls from '@/components/utilities/Controls.vue'
|
||||||
|
|
||||||
|
const emit = defineEmits(['tilemap:create'])
|
||||||
|
|
||||||
|
const scene = useScene()
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
|
|
||||||
|
const zoneTilemap = createTilemap()
|
||||||
|
const tiles = createTileLayer()
|
||||||
|
|
||||||
|
function createTilemap() {
|
||||||
|
const zoneData = new Phaser.Tilemaps.MapData({
|
||||||
|
width: zoneEditorStore.zone?.width,
|
||||||
|
height: zoneEditorStore.zone?.height,
|
||||||
|
tileWidth: config.tile_size.x,
|
||||||
|
tileHeight: config.tile_size.y,
|
||||||
|
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
|
||||||
|
format: Phaser.Tilemaps.Formats.ARRAY_2D
|
||||||
|
})
|
||||||
|
const tilemap = new Phaser.Tilemaps.Tilemap(scene, zoneData)
|
||||||
|
emit('tilemap:create', tilemap)
|
||||||
|
return tilemap
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTileLayer() {
|
||||||
|
const tilesetImages = gameStore.assets.filter((asset) => asset.group === 'tiles').map((asset, index) => zoneTilemap.addTilesetImage(asset.key, asset.key, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y }))
|
||||||
|
tilesetImages.push(zoneTilemap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 1, 2, 0, { x: 0, y: -config.tile_size.y }))
|
||||||
|
|
||||||
|
const layer = zoneTilemap.createBlankLayer('tiles', tilesetImages as any, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
|
||||||
|
|
||||||
|
layer.setDepth(0)
|
||||||
|
layer.setCullPadding(2, 2)
|
||||||
|
|
||||||
|
return layer
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
setLayerTiles(zoneTilemap, tiles, zoneEditorStore.zone.tiles)
|
||||||
|
|
||||||
|
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, pencil)
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_DOWN, paint)
|
||||||
|
|
||||||
|
zoneTilemap.destroyLayer('tiles')
|
||||||
|
zoneTilemap.removeAllLayers()
|
||||||
|
zoneTilemap.destroy()
|
||||||
|
})
|
||||||
|
</script>
|
@ -1,215 +1,53 @@
|
|||||||
<template>
|
<template>
|
||||||
<Toolbar :layer="tiles" @eraser="eraser" @pencil="pencil" @paint="paint" @clear="clear" @save="save" />
|
<Tiles @tilemap:create="tileMap = $event" />
|
||||||
<ZoneList v-if="zoneEditorStore.isZoneListModalShown" />
|
<Objects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
||||||
|
<EventTiles v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
||||||
|
|
||||||
<template v-if="zoneEditorStore.zone">
|
<Toolbar @save="save" />
|
||||||
<Controls :layer="tiles as TilemapLayer" />
|
|
||||||
|
|
||||||
<Tiles />
|
<ZoneList />
|
||||||
<Objects />
|
<TileList />
|
||||||
|
<ObjectList />
|
||||||
|
|
||||||
<ZoneSettings />
|
<ZoneSettings />
|
||||||
<TeleportModal v-if="shouldShowTeleportModal" />
|
<TeleportModal />
|
||||||
|
|
||||||
<Container :depth="2">
|
|
||||||
<Image v-for="object in zoneObjects" :depth="calculateIsometricDepth(object.positionX, object.positionY, 0)" :key="object.id" v-bind="getObjectImageProps(object)" @pointerup="() => setSelectedZoneObject(object)" :flipX="object.isRotated" />
|
|
||||||
</Container>
|
|
||||||
|
|
||||||
<Container :depth="3">
|
|
||||||
<Image v-for="tile in zoneEventTiles" :key="tile.id" v-bind="getEventTileImageProps(tile)" />
|
|
||||||
</Container>
|
|
||||||
|
|
||||||
<SelectedZoneObject v-if="zoneEditorStore.selectedZoneObject" @update_depth="updateZoneObjectDepth" @delete="deleteZoneObject" @move="handleMove" @rotate="handleRotate" />
|
|
||||||
</template>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeMount, onMounted, onUnmounted, ref, watch } from 'vue'
|
import { onUnmounted, ref } from 'vue'
|
||||||
import { Container, Image, useScene } from 'phavuer'
|
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
import { calculateIsometricDepth, loadAssets, placeTile, setAllTiles, sortByIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
import { type Zone } from '@/types'
|
||||||
import { ZoneEventTileType, type ZoneObject, type ZoneEventTile, type Zone } from '@/types'
|
|
||||||
import { uuidv4 } from '@/utilities'
|
|
||||||
import config from '@/config'
|
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import Controls from '@/components/utilities/Controls.vue'
|
|
||||||
import Toolbar from '@/components/gameMaster/zoneEditor/partials/Toolbar.vue'
|
import Toolbar from '@/components/gameMaster/zoneEditor/partials/Toolbar.vue'
|
||||||
import Tiles from '@/components/gameMaster/zoneEditor/partials/TileList.vue'
|
import TileList from '@/components/gameMaster/zoneEditor/partials/TileList.vue'
|
||||||
import SelectedZoneObject from '@/components/gameMaster/zoneEditor/partials/SelectedZoneObject.vue'
|
import ObjectList from '@/components/gameMaster/zoneEditor/partials/ObjectList.vue'
|
||||||
import ZoneSettings from '@/components/gameMaster/zoneEditor/partials/ZoneSettings.vue'
|
import ZoneSettings from '@/components/gameMaster/zoneEditor/partials/ZoneSettings.vue'
|
||||||
import Objects from '@/components/gameMaster/zoneEditor/partials/ObjectList.vue'
|
|
||||||
import ZoneList from '@/components/gameMaster/zoneEditor/partials/ZoneList.vue'
|
import ZoneList from '@/components/gameMaster/zoneEditor/partials/ZoneList.vue'
|
||||||
import TeleportModal from '@/components/gameMaster/zoneEditor/partials/TeleportModal.vue'
|
import TeleportModal from '@/components/gameMaster/zoneEditor/partials/TeleportModal.vue'
|
||||||
import Tilemap = Phaser.Tilemaps.Tilemap
|
import Tiles from '@/components/gameMaster/zoneEditor/Tiles.vue'
|
||||||
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
|
import Objects from '@/components/gameMaster/zoneEditor/Objects.vue'
|
||||||
|
import EventTiles from '@/components/gameMaster/zoneEditor/EventTiles.vue'
|
||||||
|
|
||||||
/**
|
|
||||||
* @TODO:
|
|
||||||
* Clean all the code in this file
|
|
||||||
*/
|
|
||||||
|
|
||||||
const scene = useScene()
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
|
|
||||||
const { objectList, zone, selectedTile, selectedObject, selectedZoneObject, eraserMode, drawMode } = storeToRefs(zoneEditorStore)
|
const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null)
|
||||||
|
|
||||||
const zoneTilemap = createTilemap()
|
|
||||||
const tiles = createTileLayer()
|
|
||||||
const zoneObjects = ref<ZoneObject[]>([])
|
|
||||||
const zoneEventTiles = ref<ZoneEventTile[]>([])
|
|
||||||
let tileArray = createTileArray()
|
|
||||||
|
|
||||||
const shouldShowTeleportModal = computed(() => zoneEditorStore.tool === 'pencil' && drawMode.value === 'teleport')
|
|
||||||
|
|
||||||
function createTilemap() {
|
|
||||||
const zoneData = new Phaser.Tilemaps.MapData({
|
|
||||||
width: zone.value?.width ?? 10,
|
|
||||||
height: zone.value?.height ?? 10,
|
|
||||||
tileWidth: config.tile_size.x,
|
|
||||||
tileHeight: config.tile_size.y,
|
|
||||||
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
|
|
||||||
format: Phaser.Tilemaps.Formats.ARRAY_2D
|
|
||||||
})
|
|
||||||
const tilemap = new Phaser.Tilemaps.Tilemap(scene, zoneData)
|
|
||||||
return tilemap
|
|
||||||
}
|
|
||||||
|
|
||||||
function createTileLayer() {
|
|
||||||
const tilesetImages = gameStore.assets.filter((asset) => asset.group === 'tiles').map((asset, index) => zoneTilemap.addTilesetImage(asset.key, asset.key, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y }))
|
|
||||||
tilesetImages.push(zoneTilemap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 1, 2, 0, { x: 0, y: -config.tile_size.y }))
|
|
||||||
|
|
||||||
const layer = zoneTilemap.createBlankLayer('tiles', tilesetImages as any, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
|
|
||||||
|
|
||||||
layer.setDepth(0)
|
|
||||||
|
|
||||||
return layer
|
|
||||||
}
|
|
||||||
|
|
||||||
function createTileArray() {
|
|
||||||
return Array.from({ length: zoneTilemap.height || 0 }, () => Array.from({ length: zoneTilemap.width || 0 }, () => 'blank_tile'))
|
|
||||||
}
|
|
||||||
|
|
||||||
function getObjectImageProps(object: ZoneObject) {
|
|
||||||
return {
|
|
||||||
tint: selectedZoneObject.value?.id === object.id ? 0x00ff00 : 0xffffff,
|
|
||||||
x: tileToWorldX(zoneTilemap as any, object.positionX, object.positionY),
|
|
||||||
y: tileToWorldY(zoneTilemap as any, object.positionX, object.positionY),
|
|
||||||
texture: object.object.id,
|
|
||||||
originY: Number(object.object.originX),
|
|
||||||
originX: Number(object.object.originY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function eraser(tile: Phaser.Tilemaps.Tile) {
|
|
||||||
if (eraserMode.value === 'tile') {
|
|
||||||
placeTile(zoneTilemap as Tilemap, tiles as TilemapLayer, tile.x, tile.y, 'blank_tile')
|
|
||||||
tileArray[tile.y][tile.x] = 'blank_tile'
|
|
||||||
} else if (eraserMode.value === 'object') {
|
|
||||||
zoneObjects.value = zoneObjects.value.filter((object) => object.positionX !== tile.x || object.positionY !== tile.y)
|
|
||||||
} else if (eraserMode.value === 'blocking tile' || eraserMode.value === 'teleport') {
|
|
||||||
zoneEventTiles.value = zoneEventTiles.value.filter((eventTile) => eventTile.positionX !== tile.x || eventTile.positionY !== tile.y || (eraserMode.value === 'teleport' && eventTile.type !== ZoneEventTileType.TELEPORT))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function pencil(tile: Phaser.Tilemaps.Tile) {
|
|
||||||
if (drawMode.value === 'tile' && selectedTile.value) {
|
|
||||||
placeTile(zoneTilemap as Tilemap, tiles as TilemapLayer, tile.x, tile.y, selectedTile.value.id)
|
|
||||||
tileArray[tile.y][tile.x] = selectedTile.value.id
|
|
||||||
} else if (drawMode.value === 'object' && selectedObject.value) {
|
|
||||||
addZoneObject(tile)
|
|
||||||
} else if (drawMode.value === 'blocking tile' || drawMode.value === 'teleport') {
|
|
||||||
addZoneEventTile(tile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addZoneObject(tile: Phaser.Tilemaps.Tile) {
|
|
||||||
// Check if object already exists on position
|
|
||||||
const existingObject = zoneObjects.value.find((object) => object.positionX === tile.x && object.positionY === tile.y)
|
|
||||||
if (existingObject) return
|
|
||||||
|
|
||||||
const newObject = {
|
|
||||||
id: uuidv4(),
|
|
||||||
zoneId: zone.value!.id,
|
|
||||||
zone: zone.value!,
|
|
||||||
objectId: selectedObject.value!.id,
|
|
||||||
object: selectedObject.value!,
|
|
||||||
depth: 0,
|
|
||||||
isRotated: false,
|
|
||||||
positionX: tile.x,
|
|
||||||
positionY: tile.y
|
|
||||||
}
|
|
||||||
// Add new object to zoneObjects
|
|
||||||
zoneObjects.value = zoneObjects.value.concat(newObject)
|
|
||||||
}
|
|
||||||
|
|
||||||
function addZoneEventTile(tile: Phaser.Tilemaps.Tile) {
|
|
||||||
// Check if event tile already exists on position
|
|
||||||
const existingEventTile = zoneEventTiles.value.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y)
|
|
||||||
if (existingEventTile) return
|
|
||||||
|
|
||||||
const newEventTile = {
|
|
||||||
id: uuidv4(),
|
|
||||||
zoneId: zone.value!.id,
|
|
||||||
zone: zone.value!,
|
|
||||||
type: drawMode.value === 'blocking tile' ? ZoneEventTileType.BLOCK : ZoneEventTileType.TELEPORT,
|
|
||||||
positionX: tile.x,
|
|
||||||
positionY: tile.y,
|
|
||||||
teleport:
|
|
||||||
drawMode.value === 'teleport'
|
|
||||||
? {
|
|
||||||
toZoneId: zoneEditorStore.teleportSettings.toZoneId,
|
|
||||||
toPositionX: zoneEditorStore.teleportSettings.toPositionX,
|
|
||||||
toPositionY: zoneEditorStore.teleportSettings.toPositionY,
|
|
||||||
toRotation: zoneEditorStore.teleportSettings.toRotation
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
zoneEventTiles.value = zoneEventTiles.value.concat(newEventTile as any)
|
|
||||||
}
|
|
||||||
|
|
||||||
function paint() {
|
|
||||||
if (!selectedTile.value) return
|
|
||||||
|
|
||||||
// Ensure tileArray is initialized with correct dimensions
|
|
||||||
if (!tileArray || tileArray.length !== zoneTilemap.height) {
|
|
||||||
tileArray = Array.from({ length: zoneTilemap.height }, () => Array.from({ length: zoneTilemap.width }, () => 'blank_tile'))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set all tiles in the tilemap to the selected tile's id
|
|
||||||
for (let y = 0; y < zoneTilemap.height; y++) {
|
|
||||||
if (!tileArray[y]) {
|
|
||||||
tileArray[y] = Array(zoneTilemap.width).fill('blank_tile')
|
|
||||||
}
|
|
||||||
for (let x = 0; x < zoneTilemap.width; x++) {
|
|
||||||
placeTile(zoneTilemap as Tilemap, tiles as TilemapLayer, x, y, selectedTile.value.id)
|
|
||||||
tileArray[y][x] = selectedTile.value.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
if (!zone.value) return
|
if (!zoneEditorStore.zone) return
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
zoneId: zone.value.id,
|
zoneId: zoneEditorStore.zone.id,
|
||||||
name: zoneEditorStore.zoneSettings.name,
|
name: zoneEditorStore.zoneSettings.name,
|
||||||
width: zoneEditorStore.zoneSettings.width,
|
width: zoneEditorStore.zoneSettings.width,
|
||||||
height: zoneEditorStore.zoneSettings.height,
|
height: zoneEditorStore.zoneSettings.height,
|
||||||
tiles: tileArray,
|
tiles: zoneEditorStore.zone.tiles,
|
||||||
pvp: zone.value.pvp,
|
pvp: zoneEditorStore.zone.pvp,
|
||||||
zoneEventTiles: zoneEventTiles.value.map(({ id, zoneId, type, positionX, positionY, teleport }) => ({ id, zoneId, type, positionX, positionY, teleport })),
|
zoneEffects: zoneEditorStore.zone.zoneEffects.map(({ id, zoneId, effect, strength }) => ({ id, zoneId, effect, strength })),
|
||||||
zoneObjects: zoneObjects.value.map(({ id, zoneId, objectId, depth, isRotated, positionX, positionY }) => ({ id, zoneId, objectId, depth, isRotated, positionX, positionY }))
|
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) {
|
if (zoneEditorStore.isSettingsModalShown) {
|
||||||
@ -217,111 +55,11 @@ function save() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:zone_editor:zone:update', data, (response: Zone) => {
|
gameStore.connection?.emit('gm:zone_editor:zone:update', data, (response: Zone) => {
|
||||||
console.log('zone updated')
|
|
||||||
zoneEditorStore.setZone(response)
|
zoneEditorStore.setZone(response)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function clear() {
|
|
||||||
for (let y = 0; y < zoneTilemap.height; y++) {
|
|
||||||
if (!tileArray[y]) {
|
|
||||||
tileArray[y] = Array(zoneTilemap.width).fill('blank_tile')
|
|
||||||
}
|
|
||||||
for (let x = 0; x < zoneTilemap.width; x++) {
|
|
||||||
placeTile(zoneTilemap as Tilemap, tiles as TilemapLayer, x, y, 'blank_tile')
|
|
||||||
tileArray[y][x] = 'blank_tile'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
zoneEventTiles.value = []
|
|
||||||
zoneObjects.value = []
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateZoneObjectDepth(depth: number) {
|
|
||||||
zoneObjects.value = zoneObjects.value.map((object) => (object.id === selectedZoneObject.value?.id ? { ...object, depth } : object))
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteZoneObject(objectId: string) {
|
|
||||||
zoneObjects.value = zoneObjects.value.filter((object) => object.id !== objectId)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMove() {
|
|
||||||
console.log('move btn clicked')
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRotate(objectId: string) {
|
|
||||||
const object = zoneObjects.value.find((obj) => obj.id === objectId)
|
|
||||||
if (object) {
|
|
||||||
object.isRotated = !object.isRotated
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// watch zoneEditorStore.objectList and update originX and originY of objects in zoneObjects
|
|
||||||
watch(
|
|
||||||
objectList,
|
|
||||||
(newObjects) => {
|
|
||||||
zoneObjects.value = zoneObjects.value.map((zoneObject) => {
|
|
||||||
const updatedObject = newObjects.find((obj) => obj.id === zoneObject.objectId)
|
|
||||||
if (updatedObject) {
|
|
||||||
return {
|
|
||||||
...zoneObject,
|
|
||||||
object: {
|
|
||||||
...zoneObject.object,
|
|
||||||
originX: updatedObject.originX,
|
|
||||||
originY: updatedObject.originY
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return zoneObject
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update selectedObject if it exists
|
|
||||||
if (zoneEditorStore.selectedObject) {
|
|
||||||
const updatedObject = newObjects.find((obj) => obj.id === zoneEditorStore.selectedObject?.id)
|
|
||||||
if (updatedObject) {
|
|
||||||
zoneEditorStore.setSelectedObject({
|
|
||||||
...zoneEditorStore.selectedObject,
|
|
||||||
originX: updatedObject.originX,
|
|
||||||
originY: updatedObject.originY
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ deep: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
const setSelectedZoneObject = (zoneObject: ZoneObject | null) => {
|
|
||||||
if (!zoneObject) return
|
|
||||||
if (zoneEditorStore.tool !== 'move') return
|
|
||||||
zoneEditorStore.setSelectedZoneObject(zoneObject)
|
|
||||||
}
|
|
||||||
|
|
||||||
onBeforeMount(async () => {
|
|
||||||
await gameStore.fetchAllZoneAssets()
|
|
||||||
await loadAssets(scene)
|
|
||||||
|
|
||||||
tileArray.forEach((row, y) => row.forEach((_, x) => placeTile(zoneTilemap, tiles, x, y, 'blank_tile')))
|
|
||||||
|
|
||||||
if (zone.value?.tiles) {
|
|
||||||
setAllTiles(zoneTilemap, tiles, zone.value.tiles)
|
|
||||||
tileArray = zone.value.tiles.map((row) => row.map((tileId) => tileId || 'blank_tile'))
|
|
||||||
}
|
|
||||||
|
|
||||||
zoneEventTiles.value = zone.value?.zoneEventTiles ?? []
|
|
||||||
zoneObjects.value = sortByIsometricDepth(zone.value?.zoneObjects ?? [])
|
|
||||||
|
|
||||||
// Center camera
|
|
||||||
const centerY = (zoneTilemap.height * zoneTilemap.tileHeight) / 2
|
|
||||||
const centerX = (zoneTilemap.width * zoneTilemap.tileWidth) / 2
|
|
||||||
scene.cameras.main.centerOn(centerX, centerY)
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
zoneEventTiles.value = []
|
|
||||||
zoneObjects.value = []
|
|
||||||
tiles?.destroy()
|
|
||||||
zoneTilemap?.removeAllLayers()
|
|
||||||
zoneTilemap?.destroy()
|
|
||||||
zoneEditorStore.reset()
|
zoneEditorStore.reset()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal :isModalOpen="true" @modal:close="() => zoneEditorStore.toggleCreateZoneModal()" :modal-width="300" :modal-height="400" :is-resizable="false">
|
<Modal :isModalOpen="true" @modal:close="() => zoneEditorStore.toggleCreateZoneModal()" :modal-width="300" :modal-height="400" :is-resizable="false">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="m-0 font-medium shrink-0 text-gray-300">Create new zone</h3>
|
<h3 class="m-0 font-medium shrink-0 text-white">Create new zone</h3>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #modalBody>
|
<template #modalBody>
|
||||||
|
@ -1,22 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
|
||||||
<Modal :isModalOpen="zoneEditorStore.isObjectListModalShown" :modal-width="645" :modal-height="260" @modal:close="() => (zoneEditorStore.isObjectListModalShown = false)">
|
<Modal :isModalOpen="zoneEditorStore.isObjectListModalShown" :modal-width="645" :modal-height="260" @modal:close="() => (zoneEditorStore.isObjectListModalShown = false)">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="text-lg text-gray-300">Objects</h3>
|
<h3 class="text-lg text-white">Objects</h3>
|
||||||
<div class="flex">
|
</template>
|
||||||
|
<template #modalBody>
|
||||||
|
<div class="flex pt-4 pl-4">
|
||||||
<div class="w-full flex gap-1.5 flex-row">
|
<div class="w-full flex gap-1.5 flex-row">
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-1.5 font-titles hidden" for="search">Search...</label>
|
<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" />
|
<input @mousedown.stop class="input-field" type="text" name="search" placeholder="Search" v-model="searchQuery" />
|
||||||
</div>
|
</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>-->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
<template #modalBody>
|
|
||||||
<div class="flex flex-col h-full p-4">
|
<div class="flex flex-col h-full p-4">
|
||||||
<div class="mb-4 flex flex-wrap gap-2">
|
<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) }">
|
<button v-for="tag in uniqueTags" :key="tag" @click="toggleTag(tag)" class="btn-cyan" :class="{ 'opacity-50': !selectedTags.includes(tag) }">
|
||||||
@ -43,7 +38,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
</Teleport>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col items-center py-5 px-3 fixed bottom-14 right-0" v-if="zoneEditorStore.selectedZoneObject">
|
<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 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">
|
<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" />
|
<img src="/assets/icons/trashcan.svg" class="w-4 h-4" alt="Delete" />
|
||||||
</button>
|
</button>
|
||||||
@ -15,38 +11,23 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
import type { ZoneObject } from '@/types'
|
||||||
import { ref, computed, watch } from 'vue'
|
|
||||||
|
|
||||||
const emit = defineEmits(['update_depth', 'move', 'delete', 'rotate'])
|
const props = defineProps<{
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
zoneObject: ZoneObject
|
||||||
|
}>()
|
||||||
|
|
||||||
const objectDepth = ref(zoneEditorStore.objectDepth)
|
const emit = defineEmits(['move', 'rotate', 'delete'])
|
||||||
|
|
||||||
watch(
|
const handleMove = () => {
|
||||||
() => zoneEditorStore.selectedZoneObject,
|
emit('move', props.zoneObject.id)
|
||||||
(selectedZoneObject) => {
|
|
||||||
objectDepth.value = selectedZoneObject?.depth ?? 0
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleDepthInput = () => {
|
|
||||||
const depth = parseFloat(objectDepth.value.toString())
|
|
||||||
if (!isNaN(depth)) {
|
|
||||||
emit('update_depth', depth)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRotate = () => {
|
const handleRotate = () => {
|
||||||
emit('rotate', zoneEditorStore.selectedZoneObject?.id)
|
emit('rotate', props.zoneObject.id)
|
||||||
}
|
|
||||||
|
|
||||||
const handleMove = () => {
|
|
||||||
emit('move')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
emit('delete', zoneEditorStore.selectedZoneObject?.id)
|
emit('delete', props.zoneObject.id)
|
||||||
zoneEditorStore.setSelectedZoneObject(null)
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal :is-modal-open="true" @modal:close="() => zoneEditorStore.setTool('move')" :modal-width="300" :modal-height="350" :is-resizable="false">
|
<Modal :is-modal-open="showTeleportModal" @modal:close="() => zoneEditorStore.setTool('move')" :modal-width="300" :modal-height="350" :is-resizable="false">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="m-0 font-medium shrink-0 text-gray-300">Teleport settings</h3>
|
<h3 class="m-0 font-medium shrink-0 text-white">Teleport settings</h3>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #modalBody>
|
<template #modalBody>
|
||||||
<div class="m-4">
|
<div class="m-4">
|
||||||
<form method="post" @submit.prevent="" class="inline">
|
<form method="post" @submit.prevent="" class="inline">
|
||||||
@ -40,12 +39,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref, watch } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import type { Zone } from '@/types'
|
import type { Zone } from '@/types'
|
||||||
|
|
||||||
|
const showTeleportModal = computed(() => zoneEditorStore.tool === 'pencil' && zoneEditorStore.drawMode === 'teleport')
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
|
||||||
<Modal :isModalOpen="zoneEditorStore.isTileListModalShown" :modal-width="645" :modal-height="600" @modal:close="() => (zoneEditorStore.isTileListModalShown = false)">
|
<Modal :isModalOpen="zoneEditorStore.isTileListModalShown" :modal-width="645" :modal-height="600" @modal:close="() => (zoneEditorStore.isTileListModalShown = false)">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="text-lg text-gray-300">Tiles</h3>
|
<h3 class="text-lg text-white">Tiles</h3>
|
||||||
<div class="flex">
|
</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 class="w-full flex gap-1.5 flex-row">
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-1.5 font-titles hidden" for="search">Search...</label>
|
<label class="mb-1.5 font-titles hidden" for="search">Search...</label>
|
||||||
@ -11,8 +13,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
<template #modalBody>
|
|
||||||
<div class="flex flex-col h-full p-4">
|
<div class="flex flex-col h-full p-4">
|
||||||
<div class="mb-4 flex flex-wrap gap-2">
|
<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) }">
|
<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>
|
||||||
<div class="h-[calc(100%_-_60px)] flex-grow overflow-y-auto">
|
<div class="h-[calc(100%_-_60px)] flex-grow overflow-y-auto">
|
||||||
<div class="grid grid-cols-8 gap-2 justify-items-center">
|
<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
|
<img
|
||||||
class="max-w-full max-h-full border-2 border-solid"
|
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
|
||||||
:src="`${config.server_endpoint}/assets/tiles/${tile.id}.png`"
|
:src="`${config.server_endpoint}/assets/tiles/${group.parent.id}.png`"
|
||||||
alt="Tile"
|
:alt="group.parent.name"
|
||||||
@click="zoneEditorStore.setSelectedTile(tile)"
|
@click="openGroup(group)"
|
||||||
|
@load="() => processTile(group.parent)"
|
||||||
:class="{
|
:class="{
|
||||||
'cursor-pointer transition-all duration-300': true,
|
'border-cyan shadow-lg scale-105': isActiveTile(group.parent),
|
||||||
'border-cyan shadow-lg scale-105': zoneEditorStore.selectedTile?.id === tile.id,
|
'border-transparent hover:border-gray-300': !isActiveTile(group.parent)
|
||||||
'border-transparent hover:border-gray-300': zoneEditorStore.selectedTile?.id !== tile.id
|
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
|
<span class="text-xs mt-1">{{ getTileCategory(group.parent) }}</span>
|
||||||
|
<span v-if="group.children.length > 0" class="absolute top-0 right-0 bg-cyan text-white rounded-full w-5 h-5 flex items-center justify-center text-xs">
|
||||||
|
{{ group.children.length + 1 }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div 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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
</Teleport>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -55,20 +93,60 @@ const isModalOpen = ref(false)
|
|||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const selectedTags = ref<string[]>([])
|
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 uniqueTags = computed(() => {
|
||||||
const allTags = zoneEditorStore.tileList.flatMap((tile) => tile.tags || [])
|
const allTags = zoneEditorStore.tileList.flatMap((tile) => tile.tags || [])
|
||||||
return Array.from(new Set(allTags))
|
return Array.from(new Set(allTags))
|
||||||
})
|
})
|
||||||
|
|
||||||
const filteredTiles = computed(() => {
|
const groupedTiles = computed(() => {
|
||||||
return zoneEditorStore.tileList.filter((tile) => {
|
const groups: { parent: Tile; children: Tile[] }[] = []
|
||||||
|
const filteredTiles = zoneEditorStore.tileList.filter((tile) => {
|
||||||
const matchesSearch = !searchQuery.value || tile.name.toLowerCase().includes(searchQuery.value.toLowerCase())
|
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)))
|
const matchesTags = selectedTags.value.length === 0 || (tile.tags && selectedTags.value.some((tag) => tile.tags.includes(tag)))
|
||||||
return matchesSearch && matchesTags
|
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) => {
|
const toggleTag = (tag: string) => {
|
||||||
if (selectedTags.value.includes(tag)) {
|
if (selectedTags.value.includes(tag)) {
|
||||||
selectedTags.value = selectedTags.value.filter((t) => t !== tag)
|
selectedTags.value = selectedTags.value.filter((t) => t !== tag)
|
||||||
@ -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 () => {
|
onMounted(async () => {
|
||||||
isModalOpen.value = true
|
isModalOpen.value = true
|
||||||
gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => {
|
gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => {
|
||||||
zoneEditorStore.setTileList(response)
|
zoneEditorStore.setTileList(response)
|
||||||
|
response.forEach((tile) => processTile(tile))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,34 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex justify-center p-5">
|
<div class="flex justify-center p-5">
|
||||||
<div class="toolbar fixed bottom-0 left-0 m-3 rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10">
|
<div class="toolbar fixed bottom-0 left-0 m-3 rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10">
|
||||||
<div ref="clickOutsideElement" class="tools flex gap-2.5" v-if="zoneEditorStore.zone">
|
<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')">
|
<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>
|
</button>
|
||||||
|
|
||||||
<div class="w-px bg-cyan"></div>
|
<div class="w-px bg-cyan"></div>
|
||||||
|
|
||||||
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': zoneEditorStore.tool === 'pencil' }" @click="handleClick('pencil')">
|
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': 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" v-if="zoneEditorStore.tool === 'pencil'">
|
||||||
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectPencilOpen }">
|
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectPencilOpen }">
|
||||||
{{ zoneEditorStore.drawMode }}
|
{{ 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" />
|
<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>
|
||||||
<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'">
|
<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
|
Tile
|
||||||
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
|
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
|
||||||
</span>
|
</span>
|
||||||
<span class="py-2 px-2.5 relative hover:bg-cyan/50" @click="setDrawMode('object')">
|
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('object')">
|
||||||
Object
|
Object
|
||||||
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
|
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
|
||||||
</span>
|
</span>
|
||||||
<span class="py-2 px-2.5 relative hover:bg-cyan/50" @click="setDrawMode('teleport')">
|
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('teleport')">
|
||||||
Teleport
|
Teleport
|
||||||
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
|
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
|
||||||
</span>
|
</span>
|
||||||
<span class="py-2 px-2.5 relative hover:bg-cyan/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>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@ -36,26 +36,26 @@
|
|||||||
<div class="w-px bg-cyan"></div>
|
<div class="w-px bg-cyan"></div>
|
||||||
|
|
||||||
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': zoneEditorStore.tool === 'eraser' }" @click="handleClick('eraser')">
|
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': 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" v-if="zoneEditorStore.tool === 'eraser'">
|
||||||
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectEraserOpen }">
|
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectEraserOpen }">
|
||||||
{{ zoneEditorStore.eraserMode }}
|
{{ 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" />
|
<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>
|
||||||
<div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectEraserOpen">
|
<div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectEraserOpen">
|
||||||
<span class="py-2 px-2.5 relative hover:bg-cyan/50" @click="setEraserMode('tile')">
|
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('tile')">
|
||||||
Tile
|
Tile
|
||||||
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
|
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
|
||||||
</span>
|
</span>
|
||||||
<span class="py-2 px-2.5 relative hover:bg-cyan/50" @click="setEraserMode('object')">
|
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('object')">
|
||||||
Object
|
Object
|
||||||
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
|
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
|
||||||
</span>
|
</span>
|
||||||
<span class="py-2 px-2.5 relative hover:bg-cyan/50" @click="setEraserMode('teleport')">
|
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('teleport')">
|
||||||
Teleport
|
Teleport
|
||||||
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
|
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
|
||||||
</span>
|
</span>
|
||||||
<span class="py-2 px-2.5 relative hover:bg-cyan/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>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@ -63,12 +63,12 @@
|
|||||||
<div class="w-px bg-cyan"></div>
|
<div class="w-px bg-cyan"></div>
|
||||||
|
|
||||||
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': zoneEditorStore.tool === 'paint' }" @click="handleClick('paint')">
|
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': 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>
|
</button>
|
||||||
|
|
||||||
<div class="w-px bg-cyan"></div>
|
<div class="w-px bg-cyan"></div>
|
||||||
|
|
||||||
<button class="flex justify-center items-center min-w-10 p-0 relative" @click="handleClick('settings')" 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>
|
||||||
|
|
||||||
<div class="toolbar fixed bottom-0 right-0 m-3 rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10 space-x-2">
|
<div class="toolbar fixed bottom-0 right-0 m-3 rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10 space-x-2">
|
||||||
@ -83,22 +83,15 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
import { useScene } from 'phavuer'
|
|
||||||
import { getTile } from '@/composables/zoneComposable'
|
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
import { onClickOutside } from '@vueuse/core'
|
import { onClickOutside } from '@vueuse/core'
|
||||||
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
|
|
||||||
|
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
|
|
||||||
const props = defineProps({
|
const emit = defineEmits(['save', 'clear'])
|
||||||
layer: Phaser.Tilemaps.TilemapLayer
|
|
||||||
})
|
|
||||||
const scene = useScene()
|
|
||||||
const emit = defineEmits(['move', 'eraser', 'pencil', 'paint', 'save', 'clear'])
|
|
||||||
|
|
||||||
// track when clicked outside of toolbar items
|
// track when clicked outside of toolbar items
|
||||||
const clickOutsideElement = ref(null)
|
const toolbar = ref(null)
|
||||||
|
|
||||||
// track select state
|
// track select state
|
||||||
let selectPencilOpen = ref(false)
|
let selectPencilOpen = ref(false)
|
||||||
@ -119,47 +112,6 @@ function setEraserMode(value: string) {
|
|||||||
selectEraserOpen.value = false
|
selectEraserOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function clickTile(pointer: Phaser.Input.Pointer) {
|
|
||||||
if (zoneEditorStore.tool !== 'eraser' && zoneEditorStore.tool !== 'pencil' && zoneEditorStore.tool !== 'paint') return
|
|
||||||
if (pointer.event.shiftKey) return
|
|
||||||
|
|
||||||
const px = scene.cameras.main.worldView.x + pointer.x
|
|
||||||
const py = scene.cameras.main.worldView.y + pointer.y
|
|
||||||
|
|
||||||
const pointer_tile = getTile(px, py, props.layer as TilemapLayer) as Phaser.Tilemaps.Tile
|
|
||||||
if (!pointer_tile) return
|
|
||||||
|
|
||||||
if (zoneEditorStore.tool === 'eraser') {
|
|
||||||
emit('eraser', pointer_tile)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (zoneEditorStore.tool === 'pencil') {
|
|
||||||
emit('pencil', pointer_tile)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (zoneEditorStore.tool === 'paint') {
|
|
||||||
emit('paint', pointer_tile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawTiles(pointer: Phaser.Input.Pointer) {
|
|
||||||
if (!pointer.isDown) return
|
|
||||||
clickTile(pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_UP, clickTile)
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, drawTiles)
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
addEventListener('keydown', initKeyShortcuts)
|
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_UP, clickTile)
|
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_MOVE, drawTiles)
|
|
||||||
removeEventListener('keydown', initKeyShortcuts)
|
|
||||||
})
|
|
||||||
|
|
||||||
function handleClick(tool: string) {
|
function handleClick(tool: string) {
|
||||||
if (tool === 'settings') {
|
if (tool === 'settings') {
|
||||||
zoneEditorStore.toggleSettingsModal()
|
zoneEditorStore.toggleSettingsModal()
|
||||||
@ -172,21 +124,23 @@ function handleClick(tool: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function cycleToolMode(tool: 'pencil' | 'eraser') {
|
function cycleToolMode(tool: 'pencil' | 'eraser') {
|
||||||
const modes = ['tile', 'object', 'teleport', 'blocking tile'];
|
const modes = ['tile', 'object', 'teleport', 'blocking tile']
|
||||||
const currentMode = tool === 'pencil' ? zoneEditorStore.drawMode : zoneEditorStore.eraserMode;
|
const currentMode = tool === 'pencil' ? zoneEditorStore.drawMode : zoneEditorStore.eraserMode
|
||||||
const currentIndex = modes.indexOf(currentMode);
|
const currentIndex = modes.indexOf(currentMode)
|
||||||
const nextIndex = (currentIndex + 1) % modes.length;
|
const nextIndex = (currentIndex + 1) % modes.length
|
||||||
const nextMode = modes[nextIndex];
|
const nextMode = modes[nextIndex]
|
||||||
|
|
||||||
if (tool === 'pencil') {
|
if (tool === 'pencil') {
|
||||||
setDrawMode(nextMode);
|
setDrawMode(nextMode)
|
||||||
} else {
|
} else {
|
||||||
setEraserMode(nextMode);
|
setEraserMode(nextMode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initKeyShortcuts(event: KeyboardEvent) {
|
function initKeyShortcuts(event: KeyboardEvent) {
|
||||||
|
// Check if zone is set
|
||||||
if (!zoneEditorStore.zone) return
|
if (!zoneEditorStore.zone) return
|
||||||
|
|
||||||
// prevent if focused on composables
|
// prevent if focused on composables
|
||||||
if (document.activeElement?.tagName === 'INPUT') return
|
if (document.activeElement?.tagName === 'INPUT') return
|
||||||
|
|
||||||
@ -199,19 +153,26 @@ function initKeyShortcuts(event: KeyboardEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (keyActions.hasOwnProperty(event.key)) {
|
if (keyActions.hasOwnProperty(event.key)) {
|
||||||
const tool = keyActions[event.key];
|
const tool = keyActions[event.key]
|
||||||
if ((tool === 'pencil' || tool === 'eraser') && zoneEditorStore.tool === tool) {
|
if ((tool === 'pencil' || tool === 'eraser') && zoneEditorStore.tool === tool) {
|
||||||
cycleToolMode(tool);
|
cycleToolMode(tool)
|
||||||
} else {
|
} else {
|
||||||
handleClick(tool);
|
handleClick(tool)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickOutside(clickOutsideElement, handleClickOutside)
|
|
||||||
|
|
||||||
function handleClickOutside() {
|
function handleClickOutside() {
|
||||||
selectPencilOpen.value = false
|
selectPencilOpen.value = false
|
||||||
selectEraserOpen.value = false
|
selectEraserOpen.value = false
|
||||||
}
|
}
|
||||||
|
onClickOutside(toolbar, handleClickOutside)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
addEventListener('keydown', initKeyShortcuts)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
removeEventListener('keydown', initKeyShortcuts)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<CreateZone v-if="zoneEditorStore.isCreateZoneModalShown" />
|
<CreateZone v-if="zoneEditorStore.isCreateZoneModalShown" />
|
||||||
|
<Modal :is-modal-open="zoneEditorStore.isZoneListModalShown" @modal:close="() => zoneEditorStore.toggleZoneListModal()" :is-resizable="false" :modal-width="300" :modal-height="360">
|
||||||
<Teleport to="body">
|
|
||||||
<Modal @modal:close="() => zoneEditorStore.toggleZoneListModal()" :is-resizable="false" :is-modal-open="true" :modal-width="300" :modal-height="360">
|
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="text-lg text-gray-300">Zones</h3>
|
<h3 class="text-lg text-white">Zones</h3>
|
||||||
</template>
|
</template>
|
||||||
<template #modalBody>
|
<template #modalBody>
|
||||||
<div class="my-4 mx-auto">
|
<div class="my-4 mx-auto">
|
||||||
@ -13,19 +11,18 @@
|
|||||||
<button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="() => zoneEditorStore.toggleCreateZoneModal()">New</button>
|
<button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="() => zoneEditorStore.toggleCreateZoneModal()">New</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative p-2.5 cursor-pointer flex gap-y-2.5 gap-x-5 flex-wrap" v-for="(zone, index) in zoneEditorStore.zoneList" :key="zone.id">
|
<div class="relative p-2.5 cursor-pointer flex gap-y-2.5 gap-x-5 flex-wrap" v-for="(zone, index) in zoneEditorStore.zoneList" :key="zone.id">
|
||||||
<div class="absolute left-0 top-0 w-full h-px bg-cyan-200" v-if="index === 0"></div>
|
<div class="absolute left-0 top-0 w-full h-px bg-gray-500" v-if="index === 0"></div>
|
||||||
<div class="flex gap-3 items-center w-full" @click="() => loadZone(zone.id)">
|
<div class="flex gap-3 items-center w-full" @click="() => loadZone(zone.id)">
|
||||||
<span>{{ zone.name }}</span>
|
<span>{{ zone.name }}</span>
|
||||||
<span class="ml-auto gap-1 flex">
|
<span class="ml-auto gap-1 flex">
|
||||||
<button class="btn-red py-0.5 px-2.5 z-50" @click.stop="() => deleteZone(zone.id)">X</button>
|
<button class="btn-red w-11 h-11 z-50" @click.stop="() => deleteZone(zone.id)">X</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
|
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
</Teleport>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
@ -1,24 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal :is-modal-open="zoneEditorStore.isSettingsModalShown" @modal:close="() => zoneEditorStore.toggleSettingsModal()" :modal-width="600" :modal-height="350">
|
<Modal :is-modal-open="zoneEditorStore.isSettingsModalShown" @modal:close="() => zoneEditorStore.toggleSettingsModal()" :modal-width="600" :modal-height="350">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="m-0 font-medium shrink-0 text-gray-300">Zone settings</h3>
|
<h3 class="m-0 font-medium shrink-0 text-white">Zone settings</h3>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #modalBody>
|
<template #modalBody>
|
||||||
<div class="m-4">
|
<div class="m-4">
|
||||||
<form method="post" @submit.prevent="" class="inline">
|
<div class="space-x-2">
|
||||||
<div class="gap-2.5 flex flex-wrap">
|
<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">
|
<div class="form-field-full">
|
||||||
<label for="name">Name</label>
|
<label for="name">Name</label>
|
||||||
<input class="input-field" v-model="name" name="name" id="name" />
|
<input class="input-field" v-model="name" name="name" id="name" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-half">
|
<div class="form-field-half">
|
||||||
<label for="name">Width</label>
|
<label for="width">Width</label>
|
||||||
<input class="input-field" v-model="width" name="name" id="name" type="number" />
|
<input class="input-field" v-model="width" name="width" id="width" type="number" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-half">
|
<div class="form-field-half">
|
||||||
<label for="name">Height</label>
|
<label for="height">Height</label>
|
||||||
<input class="input-field" v-model="height" name="name" id="name" type="number" />
|
<input class="input-field" v-model="height" name="height" id="height" type="number" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="pvp">PVP enabled</label>
|
<label for="pvp">PVP enabled</label>
|
||||||
@ -29,6 +33,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
@ -40,16 +52,19 @@ import Modal from '@/components/utilities/Modal.vue'
|
|||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
|
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
|
const screen = ref('settings')
|
||||||
|
|
||||||
zoneEditorStore.setZoneName(zoneEditorStore.zone.name)
|
zoneEditorStore.setZoneName(zoneEditorStore.zone?.name)
|
||||||
zoneEditorStore.setZoneWidth(zoneEditorStore.zone.width)
|
zoneEditorStore.setZoneWidth(zoneEditorStore.zone?.width)
|
||||||
zoneEditorStore.setZoneHeight(zoneEditorStore.zone.height)
|
zoneEditorStore.setZoneHeight(zoneEditorStore.zone?.height)
|
||||||
zoneEditorStore.setZonePvp(zoneEditorStore.zone.pvp)
|
zoneEditorStore.setZonePvp(zoneEditorStore.zone?.pvp)
|
||||||
|
zoneEditorStore.setZoneEffects(zoneEditorStore.zone?.zoneEffects)
|
||||||
|
|
||||||
const name = ref(zoneEditorStore.zoneSettings.name)
|
const name = ref(zoneEditorStore.zoneSettings?.name)
|
||||||
const width = ref(zoneEditorStore.zoneSettings.width)
|
const width = ref(zoneEditorStore.zoneSettings?.width)
|
||||||
const height = ref(zoneEditorStore.zoneSettings.height)
|
const height = ref(zoneEditorStore.zoneSettings?.height)
|
||||||
const pvp = ref(zoneEditorStore.zoneSettings.pvp)
|
const pvp = ref(zoneEditorStore.zoneSettings?.pvp)
|
||||||
|
const zoneEffects = ref(zoneEditorStore.zoneSettings?.zoneEffects || [])
|
||||||
|
|
||||||
watch(name, (value) => {
|
watch(name, (value) => {
|
||||||
zoneEditorStore.setZoneName(value)
|
zoneEditorStore.setZoneName(value)
|
||||||
@ -66,4 +81,26 @@ watch(height, (value) => {
|
|||||||
watch(pvp, (value) => {
|
watch(pvp, (value) => {
|
||||||
zoneEditorStore.setZonePvp(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>
|
</script>
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full md:min-w-[350px] max-w-[750px] flex flex-col">
|
<div class="w-full md:min-w-[350px] max-w-[750px] flex flex-col absolute left-1/2 -translate-x-1/2 bottom-5">
|
||||||
<div ref="chatWindow" class="w-full overflow-auto h-32 mb-5 bg-gray-300/80 rounded-lg border-2 border-solid border-cyan-200" v-show="gameStore.uiSettings.isChatOpen">
|
<div ref="chatWindow" class="w-full overflow-auto h-32 mb-5 bg-gray rounded-md border-2 border-solid border-gray-500 text-gray-300" v-show="gameStore.uiSettings.isChatOpen">
|
||||||
<div v-for="message in chats" class="flex-col py-2 items-center p-3">
|
<div v-for="message in chats" class="flex-col py-2 items-center p-3">
|
||||||
<span class="text-ellipsis overflow-hidden whitespace-nowrap text-sm">{{ message.character.name }}</span>
|
<span class="text-ellipsis overflow-hidden whitespace-nowrap text-sm" :class="{ 'text-cyan-300': gameStore.character?.role == 'gm' }">{{ message.character.name }}</span>
|
||||||
<p class="text-gray-50 m-0">{{ message.message }}</p>
|
<p class="text-gray-50 m-0">{{ message.message }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full flex">
|
<div class="w-96 mx-auto relative">
|
||||||
|
<img src="/assets/icons/chat-icon.svg" class="absolute top-1/2 -translate-y-1/2 left-2.5 h-4 w-4 opacity-50" />
|
||||||
<input
|
<input
|
||||||
class="w-full h-12 rounded-lg text-lg px-4 py-0 bg-gray border-2 border-solid border-gray-500 text-gray-300 bg-[url('/assets/icons/submit-icon.svg')] bg-no-repeat bg-[right_25px_center] bg-[length:30px] focus:outline-none focus:ring-0 focus:border-cyan-800"
|
class="w-[332px] h-8 rounded-sm text-xs font-default pl-8 pr-4 py-0 bg-gray-600 border-2 border-solid border-gray-500 text-gray-300 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover focus:outline-none focus:ring-0 focus:border-cyan-800"
|
||||||
placeholder="Type something..."
|
placeholder="Type something..."
|
||||||
v-model="message"
|
v-model="message"
|
||||||
@keypress="handleKeyPress"
|
@keypress="handleKeyPress"
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
<template>
|
<template></template>
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
52
src/components/gui/Hotkeys.vue
Normal 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-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-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-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-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-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-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-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-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>
|
@ -1,5 +1,16 @@
|
|||||||
<template>
|
<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="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>
|
||||||
|
<span class="text-xs leading-3 text-pixel">{{ gameStore.character?.hitpoints }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-full flex items-center gap-2">
|
||||||
|
<label class="text-xs leading-3 text-pixel" for="sp">SP</label>
|
||||||
|
<progress class="h-2 rounded-sm w-full max-w-44 appearance-none accent-blue" id="sp" :value="gameStore.character?.mana" max="100">{{ gameStore.character?.mana }}%</progress>
|
||||||
|
<span class="text-xs leading-3 text-pixel">{{ gameStore.character?.mana }}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -9,41 +20,30 @@ const gameStore = useGameStore()
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.hud-wrapper {
|
#hp {
|
||||||
.hud-bg {
|
|
||||||
mask: url('/assets/shapes/hud-image-shape.svg') center/cover no-repeat;
|
|
||||||
}
|
|
||||||
#hp {
|
|
||||||
// Chrome, Safari, Edge, Opera
|
// Chrome, Safari, Edge, Opera
|
||||||
&::-webkit-progress-value {
|
&::-webkit-progress-value {
|
||||||
@apply bg-red rounded-lg;
|
@apply bg-gradient-to-r from-green from-75% to-green-200 rounded-sm;
|
||||||
}
|
}
|
||||||
&::-webkit-progress-bar {
|
&::-webkit-progress-bar {
|
||||||
@apply bg-white rounded-lg border-2 border-solid border-white;
|
@apply bg-white rounded-sm border border-solid border-black;
|
||||||
}
|
}
|
||||||
// Firefox
|
// Firefox
|
||||||
&::-moz-progress-bar {
|
&::-moz-progress-bar {
|
||||||
@apply bg-red rounded-lg border-2 border-solid border-white;
|
@apply bg-gradient-to-r from-green from-75% to-green-200 rounded-sm border border-solid border-black;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#mp {
|
#sp {
|
||||||
// Chrome, Safari, Edge, Opera
|
// Chrome, Safari, Edge, Opera
|
||||||
&::-webkit-progress-value {
|
&::-webkit-progress-value {
|
||||||
@apply bg-blue rounded-lg;
|
@apply bg-gradient-to-r from-blue from-75% to-blue-200 rounded-sm;
|
||||||
}
|
}
|
||||||
&::-webkit-progress-bar {
|
&::-webkit-progress-bar {
|
||||||
@apply bg-white rounded-lg border-2 border-solid border-white;
|
@apply bg-white rounded-sm border border-solid border-black;
|
||||||
}
|
}
|
||||||
// Firefox
|
// Firefox
|
||||||
&::-moz-progress-bar {
|
&::-moz-progress-bar {
|
||||||
@apply bg-blue rounded-lg border-2 border-solid border-white;
|
@apply bg-gradient-to-r from-blue from-75% to-blue-200 rounded-sm border border-solid border-black;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.other-player {
|
|
||||||
.hud-bg {
|
|
||||||
mask: url('/assets/shapes/hud-image-shape-flipped.svg') center/cover no-repeat;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
<template>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
</script>
|
|
@ -1,39 +1,49 @@
|
|||||||
<template>
|
<template>
|
||||||
<ul class="list-none flex gap-2.5 items-center m-0 max-md:pl-0">
|
<ul class="list-none flex flex-col gap-2.5 items-center m-0 pl-0 absolute left-4 top-4">
|
||||||
<li class="menu-item group relative" @click="gameStore.toggleChat">
|
|
||||||
<div class="group-hover:block absolute bottom-16 left-1/2 -translate-x-1/2 w-20 h-6 text-center bg-gray-300 border-2 border-solid border-cyan rounded-3xl hidden">
|
|
||||||
<p class="absolute w-full bottom-0 m-0 text-xs leading-6">Chat</p>
|
|
||||||
<div class="group-hover:block absolute -bottom-2.5 bg-cyan h-2 w-3.5 [clip-path:polygon(100%_0,_0_0,_50%_100%)] left-1/2 -translate-x-1/2 hidden"></div>
|
|
||||||
</div>
|
|
||||||
<a class="group-hover:bg-gray/70 group-hover:cursor-pointer p-1.5 bg-gray-300/70 border-2 border-solid border-cyan-200 hover:border-cyan rounded-lg block w-11 h-9">
|
|
||||||
<img class="group-hover:drop-shadow-default w-11 h-9 object-contain" draggable="false" src="/assets/icons/chat.png" />
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="menu-item group relative">
|
<li class="menu-item group relative">
|
||||||
<div class="group-hover:block absolute bottom-16 left-1/2 -translate-x-1/2 w-20 h-6 text-center bg-gray-300 border-2 border-solid border-cyan rounded-3xl hidden">
|
<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">World</p>
|
<p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">Open menu</p>
|
||||||
<div class="group-hover:block absolute -bottom-2.5 bg-cyan h-2 w-3.5 [clip-path:polygon(100%_0,_0_0,_50%_100%)] left-1/2 -translate-x-1/2 hidden"></div>
|
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
<a class="group-hover:bg-gray/70 group-hover:cursor-pointer p-1.5 bg-gray-300/70 border-2 border-solid border-cyan-200 hover:border-cyan rounded-lg block w-11 h-9">
|
<a class="group-hover:cursor-pointer bg-[url('/assets/ui-border-4-corners.svg')] bg-no-repeat block w-[42px] h-[42px] relative">
|
||||||
<img class="group-hover:drop-shadow-default w-11 h-9 object-contain" draggable="false" src="/assets/icons/world.png" />
|
<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">
|
|
||||||
<div class="group-hover:block absolute bottom-16 left-1/2 -translate-x-1/2 w-20 h-6 text-center bg-gray-300 border-2 border-solid border-cyan rounded-3xl hidden">
|
|
||||||
<p class="absolute w-full bottom-0 m-0 text-xs leading-6">Users</p>
|
|
||||||
<div class="group-hover:block absolute -bottom-2.5 bg-cyan h-2 w-3.5 [clip-path:polygon(100%_0,_0_0,_50%_100%)] left-1/2 -translate-x-1/2 hidden"></div>
|
|
||||||
</div>
|
|
||||||
<a class="group-hover:bg-gray/70 group-hover:cursor-pointer p-1.5 bg-gray-300/70 border-2 border-solid border-cyan-200 hover:border-cyan rounded-lg block w-11 h-9">
|
|
||||||
<img class="group-hover:drop-shadow-default w-11 h-9 object-contain" draggable="false" src="/assets/icons/users.png" />
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="menu-item group relative" @click="gameStore.toggleUserPanel">
|
<li class="menu-item group relative" @click="gameStore.toggleUserPanel">
|
||||||
<div class="group-hover:block absolute bottom-16 left-1/2 -translate-x-1/2 w-20 h-6 text-center bg-gray-300 border-2 border-solid border-cyan rounded-3xl hidden">
|
<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">Inventory</p>
|
<p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">User Profile</p>
|
||||||
<div class="group-hover:block absolute -bottom-2.5 bg-cyan h-2 w-3.5 [clip-path:polygon(100%_0,_0_0,_50%_100%)] left-1/2 -translate-x-1/2 hidden"></div>
|
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
<a class="group-hover:bg-gray/70 group-hover:cursor-pointer p-1.5 bg-gray-300/70 border-2 border-solid border-cyan-200 hover:border-cyan rounded-lg block w-11 h-9">
|
<a class="group-hover:cursor-pointer bg-[url('/assets/ui-border-4-corners.svg')] bg-no-repeat block w-[42px] h-[42px] relative">
|
||||||
<img class="group-hover:drop-shadow-default w-11 h-9 object-contain" draggable="false" src="/assets/icons/treasure-chest.png" />
|
<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>
|
||||||
|
</li>
|
||||||
|
<li class="menu-item group relative" @click="gameStore.toggleChat">
|
||||||
|
<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">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 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>
|
||||||
|
<li class="menu-item group relative">
|
||||||
|
<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">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 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>
|
||||||
|
<li class="menu-item group relative">
|
||||||
|
<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">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 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>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -43,4 +53,5 @@
|
|||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
let characterLevel = gameStore.character?.level.toString().padStart(2, '0')
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,5 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<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" />
|
||||||
|
</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" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<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="gameStore.uiSettings.isUserPanelOpen">
|
||||||
<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-300/80 border-solid border-2 border-cyan-200 rounded-lg z-50 flex flex-col backdrop-blur-sm shadow-lg">
|
<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-cyan-200">
|
<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>
|
<h3 class="m-0 font-medium shrink-0">Game menu</h3>
|
||||||
<div class="hidden sm:flex gap-1.5 flex-wrap">
|
<div class="hidden sm:flex gap-1.5 flex-wrap">
|
||||||
<button @click.stop="userPanelScreen = 'inventory'" :class="{ active: userPanelScreen === 'inventory' }" class="btn-cyan py-1.5 px-4 min-w-24">Inventory</button>
|
<button @click.stop="userPanelScreen = 'inventory'" :class="{ active: userPanelScreen === 'inventory' }" class="btn-cyan py-1.5 px-4 min-w-24">Inventory</button>
|
||||||
@ -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>
|
<button @click.stop="userPanelScreen = 'settings'" :class="{ active: userPanelScreen === 'settings' }" class="btn-cyan py-1.5 px-4 min-w-24">Settings</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2.5">
|
<div class="flex gap-2.5">
|
||||||
<button class="w-5 h-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" @click="gameStore.toggleUserPanel">
|
||||||
<img alt="close" draggable="false" src="/assets/icons/close-button-white.svg" class="w-full h-full" />
|
<img alt="close" draggable="false" src="/assets/icons/close-button-white.svg" class="w-full h-full" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute -left-5 -bottom-5 w-[calc(100%_+_40px)] my-px h-px bg-cyan-200"></div>
|
<div class="absolute -left-5 -bottom-5 w-[calc(100%_+_40px)] my-px h-px bg-gray-500"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="m-4">
|
<div class="m-4">
|
||||||
<h4 class="font-medium text-lg max-w-[375px]">Character stats</h4>
|
<h4 class="font-medium text-lg max-w-[375px]">Character stats</h4>
|
||||||
|
@ -13,54 +13,54 @@
|
|||||||
<div class="flex flex-col gap-3 mx-5 mt-2">
|
<div class="flex flex-col gap-3 mx-5 mt-2">
|
||||||
<div class="flex gap-3 justify-center">
|
<div class="flex gap-3 justify-center">
|
||||||
<!-- Helmet -->
|
<!-- Helmet -->
|
||||||
<div class="bg-gray-300/80 border-solid border-2 border-cyan-200 rounded-md aspect-square h-11 w-11 relative hover:bg-gray-200">
|
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square h-11 w-11 relative hover:bg-gray-200">
|
||||||
<img src="/assets/icons/inventory/helmet.svg" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" />
|
<img src="/assets/icons/inventory/helmet.svg" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Head charm -->
|
<!-- Head charm -->
|
||||||
<div class="bg-gray-300/80 border-solid border-2 border-cyan-200 rounded-md aspect-square h-11 w-11 relative hover:bg-gray-200">
|
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square h-11 w-11 relative hover:bg-gray-200">
|
||||||
<img src="/assets/icons/inventory/head_charm.svg" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" />
|
<img src="/assets/icons/inventory/head_charm.svg" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-3 justify-center">
|
<div class="flex gap-3 justify-center">
|
||||||
<!-- Bracers -->
|
<!-- Bracers -->
|
||||||
<div class="bg-gray-300/80 border-solid border-2 border-cyan-200 rounded-md w-11 h-[104px] relative hover:bg-gray-200">
|
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md w-11 h-[104px] relative hover:bg-gray-200">
|
||||||
<img src="/assets/icons/inventory/bracers.svg" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" />
|
<img src="/assets/icons/inventory/bracers.svg" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Chestplate -->
|
<!-- Chestplate -->
|
||||||
<div class="bg-gray-300/80 border-solid border-2 border-cyan-200 rounded-md aspect-square w-[104px] h-[104px] relative hover:bg-gray-200">
|
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square w-[104px] h-[104px] relative hover:bg-gray-200">
|
||||||
<img src="/assets/icons/inventory/chestplate.svg" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-10/12 opacity-20" />
|
<img src="/assets/icons/inventory/chestplate.svg" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-10/12 opacity-20" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Primary Weapon -->
|
<!-- Primary Weapon -->
|
||||||
<div class="bg-gray-300/80 border-solid border-2 border-cyan-200 rounded-md w-11 h-[104px] self-stretch justify-self-stretch relative hover:bg-gray-200">
|
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md w-11 h-[104px] self-stretch justify-self-stretch relative hover:bg-gray-200">
|
||||||
<img src="/assets/icons/inventory/primary_weapon.svg" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" />
|
<img src="/assets/icons/inventory/primary_weapon.svg" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-3 justify-center">
|
<div class="flex gap-3 justify-center">
|
||||||
<!-- Legs -->
|
<!-- Legs -->
|
||||||
<div class="bg-gray-300/80 border-solid border-2 border-cyan-200 rounded-md w-11 h-[104px] self-stretch justify-self-stretch relative hover:bg-gray-200">
|
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md w-11 h-[104px] self-stretch justify-self-stretch relative hover:bg-gray-200">
|
||||||
<img src="/assets/icons/inventory/legs.svg" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" />
|
<img src="/assets/icons/inventory/legs.svg" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<!-- Belt/pouch -->
|
<!-- Belt/pouch -->
|
||||||
<div class="bg-gray-300/80 border-solid border-2 border-cyan-200 rounded-md aspect-square h-11 w-11 self-stretch justify-self-stretch relative hover:bg-gray-200">
|
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square h-11 w-11 self-stretch justify-self-stretch relative hover:bg-gray-200">
|
||||||
<img src="/assets/icons/inventory/pouch.svg" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" />
|
<img src="/assets/icons/inventory/pouch.svg" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Boots -->
|
<!-- Boots -->
|
||||||
<div class="bg-gray-300/80 border-solid border-2 border-cyan-200 rounded-md aspect-square h-11 w-11 self-stretch justify-self-stretch relative hover:bg-gray-200">
|
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square h-11 w-11 self-stretch justify-self-stretch relative hover:bg-gray-200">
|
||||||
<img src="/assets/icons/inventory/boots.svg" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" />
|
<img src="/assets/icons/inventory/boots.svg" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute -left-5 -bottom-5 w-[calc(100%_+_40px)] my-px h-px bg-cyan-200"></div>
|
<div class="absolute -left-5 -bottom-5 w-[calc(100%_+_40px)] my-px h-px bg-gray-500"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="m-4">
|
<div class="m-4">
|
||||||
<h4 class="font-medium text-lg max-w-[375px]">Equipment Bonus</h4>
|
<h4 class="font-medium text-lg max-w-[375px]">Equipment Bonus</h4>
|
||||||
|
@ -3,14 +3,14 @@
|
|||||||
<div class="m-4 relative">
|
<div class="m-4 relative">
|
||||||
<h4 class="m-auto font-medium text-lg max-w-[375px]">Inventory</h4>
|
<h4 class="m-auto font-medium text-lg max-w-[375px]">Inventory</h4>
|
||||||
<div class="flex gap-3 mt-4 mx-auto flex-wrap max-w-[375px]">
|
<div class="flex gap-3 mt-4 mx-auto flex-wrap max-w-[375px]">
|
||||||
<div v-for="n in 24" class="bg-gray-300/80 border-solid border-2 border-cyan-200 w-12 h-12 rounded-md aspect-square shrink-0 justify-self-stretch hover:bg-gray-200"></div>
|
<div v-for="n in 24" class="bg-gray-300/80 border-solid border-2 border-gray-500 w-12 h-12 rounded-md aspect-square shrink-0 justify-self-stretch hover:bg-gray-200"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute -left-5 -bottom-5 w-[calc(100%_+_40px)] my-px h-px bg-cyan-200"></div>
|
<div class="absolute -left-5 -bottom-5 w-[calc(100%_+_40px)] my-px h-px bg-gray-500"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="m-4">
|
<div class="m-4">
|
||||||
<h4 class="m-auto font-medium text-lg max-w-[375px]">Chest items</h4>
|
<h4 class="m-auto font-medium text-lg max-w-[375px]">Chest items</h4>
|
||||||
<div class="flex gap-3 mt-4 mx-auto flex-wrap max-w-[375px]">
|
<div class="flex gap-3 mt-4 mx-auto flex-wrap max-w-[375px]">
|
||||||
<div v-for="n in 12" class="bg-gray-300/80 border-solid border-2 border-cyan-200 w-12 h-12 rounded-md aspect-square shrink-0 justify-self-stretch hover:bg-gray-200"></div>
|
<div v-for="n in 12" class="bg-gray-300/80 border-solid border-2 border-gray-500 w-12 h-12 rounded-md aspect-square shrink-0 justify-self-stretch hover:bg-gray-200"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,26 +4,26 @@
|
|||||||
<!-- Settings Categories -->
|
<!-- Settings Categories -->
|
||||||
<div class="relative p-2.5">
|
<div class="relative p-2.5">
|
||||||
<h3>Settings</h3>
|
<h3>Settings</h3>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
|
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||||
</div>
|
</div>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'character' }" @click.stop="settingCategory = 'character'">
|
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'character' }" @click.stop="settingCategory = 'character'">
|
||||||
<span>Character</span>
|
<span>Character</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
|
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'account' }" @click.stop="settingCategory = 'account'">
|
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'account' }" @click.stop="settingCategory = 'account'">
|
||||||
<span>Account</span>
|
<span>Account</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
|
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'audio' }" @click.stop="settingCategory = 'audio'">
|
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'audio' }" @click.stop="settingCategory = 'audio'">
|
||||||
<span>Audio</span>
|
<span>Audio</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
|
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'video' }" @click.stop="settingCategory = 'video'">
|
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'video' }" @click.stop="settingCategory = 'video'">
|
||||||
<span>Video</span>
|
<span>Video</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
|
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute w-px bg-cyan-200 h-full top-0 left-1/6"></div>
|
<div class="absolute w-px bg-gray-500 h-full top-0 left-1/6"></div>
|
||||||
|
|
||||||
<!-- Assets list -->
|
<!-- Assets list -->
|
||||||
<div class="overflow-auto h-full w-10/12 flex flex-col relative">
|
<div class="overflow-auto h-full w-10/12 flex flex-col relative">
|
||||||
|
@ -2,11 +2,11 @@
|
|||||||
<!-- Chat bubble -->
|
<!-- Chat bubble -->
|
||||||
<Container ref="charChatContainer" :depth="999" :x="currentX" :y="currentY">
|
<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" />
|
<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>
|
</Container>
|
||||||
<!-- Character name and health -->
|
<!-- Character name and health -->
|
||||||
<Container :depth="999" :x="currentX" :y="currentY">
|
<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="18.5" :fillColor="0xffffff" :width="74" :height="6" :radius="5" />
|
||||||
<RoundRectangle :origin-x="0.5" :origin-y="36.4" :fillColor="0x00b3b3" :width="70" :height="3" :radius="5" />
|
<RoundRectangle :origin-x="0.5" :origin-y="36.4" :fillColor="0x00b3b3" :width="70" :height="3" :radius="5" />
|
||||||
</Container>
|
</Container>
|
||||||
@ -113,7 +113,7 @@ const isFlippedX = computed(() => [6, 4].includes(props.character.rotation ?? 0)
|
|||||||
|
|
||||||
const charTexture = computed(() => {
|
const charTexture = computed(() => {
|
||||||
const { rotation, characterType, isMoving } = props.character
|
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 action = isMoving ? 'walk' : 'idle'
|
||||||
const direction = [0, 6].includes(rotation) ? 'left_up' : 'right_down'
|
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.setName(`${props.character.name}_chatText`)
|
||||||
text.setFontSize(13)
|
text.setFontSize(13)
|
||||||
text.setFontFamily('Arial')
|
text.setFontFamily('Arial')
|
||||||
|
text.setOrigin(0.5, 10.9)
|
||||||
|
|
||||||
// Fix text alignment on Windows and Android
|
// Fix text alignment on Windows and Android
|
||||||
if (game.device.os.windows || game.device.os.android) {
|
if (game.device.os.windows || game.device.os.android) {
|
||||||
text.setOrigin(0.5, 9.75)
|
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.setFontSize(13)
|
||||||
text.setFontFamily('Arial')
|
text.setFontFamily('Arial')
|
||||||
|
text.setOrigin(0.5, 9)
|
||||||
|
|
||||||
// Fix text alignment on Windows and Android
|
// Fix text alignment on Windows and Android
|
||||||
if (game.device.os.windows || game.device.os.android) {
|
if (game.device.os.windows || game.device.os.android) {
|
||||||
text.setOrigin(0.5, 8)
|
text.setOrigin(0.5, 8)
|
||||||
|
|
||||||
|
if (game.device.browser.firefox) {
|
||||||
|
text.setOrigin(0.5, 9)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,24 +38,20 @@ const modalOpened = ref(props.modalOpened)
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Modal :closable="false" :is-resizable="false" :isModalOpen="true" @modal:close="() => (modalOpened = !modalOpened)" :modal-width="300" :modal-height="190">
|
<Modal :closable="false" :is-resizable="false" :isModalOpen="true" @modal:close="() => (modalOpened = !modalOpened)" :modal-width="350" :modal-height="230">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<div class="text-gray-300">
|
|
||||||
<slot name="modalHeader"></slot>
|
<slot name="modalHeader"></slot>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<template #modalBody>
|
<template #modalBody>
|
||||||
<div class="text-gray-300 h-full">
|
<div class="h-[calc(100%_-_32px)] p-4">
|
||||||
<div class="flex h-full flex-col justify-between">
|
<div class="h-full flex flex-col justify-between">
|
||||||
<span class="p-2">
|
|
||||||
<slot name="modalBody"></slot>
|
<slot name="modalBody"></slot>
|
||||||
</span>
|
<div class="grid grid-flow-col justify-stretch gap-4">
|
||||||
<div class="flex justify-between p-2">
|
<button class="btn-empty py-1.5 px-4 min-w-24 inline-block" @click="props.cancelFunction()">
|
||||||
<button class="btn-cyan py-1.5 px-4 min-w-24 inline-block" @click="props.cancelFunction()">
|
|
||||||
{{ props.cancelButtonText }}
|
{{ props.cancelButtonText }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="btn-cyan py-1.5 px-4 min-w-24 inline-block" type="submit" @click="props.confirmFunction()">
|
<button class="btn-red py-1.5 px-4 min-w-24 inline-block" type="submit" @click="props.confirmFunction()">
|
||||||
{{ props.confirmButtonText }}
|
{{ props.confirmButtonText }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,23 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<div v-if="isModalOpenRef" class="fixed bg-gray border-solid border-2 border-gray-500 z-50 flex flex-col backdrop-blur-sm shadow-lg" :style="modalStyle">
|
<div v-if="isModalOpenRef" class="fixed border-solid border-2 border-gray-500 z-50 flex flex-col backdrop-blur-sm shadow-lg" :style="modalStyle">
|
||||||
<div @mousedown="startDrag" class="cursor-move p-2.5 flex justify-between items-center border-solid border-0 border-b border-gray-500">
|
<div @mousedown="startDrag" class="cursor-move p-2.5 flex justify-between items-center border-solid border-0 border-b border-gray-500 relative">
|
||||||
|
<div class="rounded-t-md absolute w-full h-full top-0 left-0 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-center bg-cover opacity-90"></div>
|
||||||
|
<div class="relative z-10">
|
||||||
<slot name="modalHeader" />
|
<slot name="modalHeader" />
|
||||||
|
</div>
|
||||||
<div class="flex gap-2.5">
|
<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">
|
<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-full h-full 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>
|
||||||
<button @click="close" v-if="closable" class="w-5 h-5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
|
<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" />
|
<img alt="close" draggable="false" src="/assets/icons/close-button-white.svg" class="w-full h-full" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-hidden grow">
|
<div class="overflow-hidden grow relative">
|
||||||
|
<div class="rounded-b-md absolute w-full h-full top-0 left-0 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center opacity-90"></div>
|
||||||
|
<div class="relative z-10 h-full">
|
||||||
<slot name="modalBody" />
|
<slot name="modalBody" />
|
||||||
<img v-if="isResizable && !isFullScreen" src="/assets/icons/resize-icon.svg" alt="resize" class="absolute bottom-0 right-0 w-5 h-5 cursor-nwse-resize invert-[60%]" @mousedown="startResize" />
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="$slots.modalFooter" class="px-5 min-h-12 flex justify-end gap-7.5 items-center border-solid border-t border-gray-500">
|
<img v-if="isResizable && !isFullScreen" src="/assets/icons/resize-icon.svg" alt="resize" class="absolute z-10 bottom-0 right-0 w-5 h-5 cursor-nwse-resize" @mousedown="startResize" />
|
||||||
<slot name="modalFooter" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
@ -84,7 +87,7 @@ let startHeight = 0
|
|||||||
let preFullScreenState = { x: 0, y: 0, width: 0, height: 0 }
|
let preFullScreenState = { x: 0, y: 0, width: 0, height: 0 }
|
||||||
|
|
||||||
const modalStyle = computed(() => ({
|
const modalStyle = computed(() => ({
|
||||||
borderRadius: isFullScreen.value ? '0' : '10px',
|
borderRadius: isFullScreen.value ? '0' : '6px',
|
||||||
top: isFullScreen.value ? '0' : `${y.value}px`,
|
top: isFullScreen.value ? '0' : `${y.value}px`,
|
||||||
left: isFullScreen.value ? '0' : `${x.value}px`,
|
left: isFullScreen.value ? '0' : `${x.value}px`,
|
||||||
width: isFullScreen.value ? '100vw' : `${width.value}px`,
|
width: isFullScreen.value ? '100vw' : `${width.value}px`,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<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">
|
<template #modalHeader v-if="notification.title">
|
||||||
<h3 class="m-0 font-medium shrink-0 text-gray-300">{{ notification.title }}</h3>
|
<h3 class="m-0 font-medium shrink-0 text-white">{{ notification.title }}</h3>
|
||||||
</template>
|
</template>
|
||||||
<template #modalBody v-if="notification.message">
|
<template #modalBody v-if="notification.message">
|
||||||
<p class="m-4">{{ notification.message }}</p>
|
<p class="m-4">{{ notification.message }}</p>
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Image v-for="object in zoneStore.zone?.zoneObjects" :depth="calculateIsometricDepth(object.positionX, object.positionY, object.object.frameWidth, object.object.frameHeight)" :key="object.id" v-bind="getObjectImageProps(object)" :flipX="object.isRotated" />
|
|
||||||
<!-- <Text v-for="object in zoneStore.zone?.zoneObjects" :key="object.id" :depth="99999" :text="Math.ceil(calculateIsometricDepth(object.positionX, object.positionY, object.object.frameWidth, object.object.frameHeight))" v-bind="getObjectProps(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 getObjectProps = (object: ZoneObject) => {
|
|
||||||
return {
|
|
||||||
x: tileToWorldX(props.tilemap as any, object.positionX, object.positionY),
|
|
||||||
y: tileToWorldY(props.tilemap as any, object.positionX, object.positionY),
|
|
||||||
originY: Number(object.object.originX),
|
|
||||||
originX: Number(object.object.originY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getObjectImageProps = (object: ZoneObject) => {
|
|
||||||
return {
|
|
||||||
x: tileToWorldX(props.tilemap as any, object.positionX, object.positionY),
|
|
||||||
y: tileToWorldY(props.tilemap as any, object.positionX, object.positionY),
|
|
||||||
texture: object.object.id,
|
|
||||||
originY: Number(object.object.originX),
|
|
||||||
originX: Number(object.object.originY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -1,23 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<Tiles :key="zoneStore.zone?.id ?? 0" @tilemap:create="tileMap = $event" />
|
<ZoneTiles :key="zoneStore.zone?.id ?? 0" @tilemap:create="tileMap = $event" />
|
||||||
<Objects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
<ZoneObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
||||||
<Characters v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
<Characters v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useScene } from 'phavuer'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useZoneStore } from '@/stores/zoneStore'
|
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 type { Character as CharacterT, Zone as ZoneT, ExtendedCharacter as ExtendedCharacterT } from '@/types'
|
||||||
import Tiles from '@/components/zone/Tiles.vue'
|
import ZoneTiles from '@/components/zone/ZoneTiles.vue'
|
||||||
import Objects from '@/components/zone/Objects.vue'
|
import ZoneObjects from '@/components/zone/ZoneObjects.vue'
|
||||||
import Characters from '@/components/zone/Characters.vue'
|
import Characters from '@/components/zone/Characters.vue'
|
||||||
import { loadAssets } from '@/composables/zoneComposable'
|
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const zoneStore = useZoneStore()
|
const zoneStore = useZoneStore()
|
||||||
const scene = useScene()
|
|
||||||
|
|
||||||
const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null)
|
const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null)
|
||||||
|
|
||||||
@ -28,13 +25,6 @@ type zoneLoadData = {
|
|||||||
|
|
||||||
// Event listeners
|
// Event listeners
|
||||||
gameStore.connection!.on('zone:character:teleport', async (data: zoneLoadData) => {
|
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
|
* @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)
|
zoneStore.updateCharacter(data)
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(async () => {
|
||||||
gameStore.connection!.emit('zone:character:join', async (response: zoneLoadData) => {
|
gameStore!.connection!.emit('zone:character:join', async (response: zoneLoadData) => {
|
||||||
// Fetch assets for new zone
|
|
||||||
await gameStore.fetchZoneAssets(response.zone.id)
|
|
||||||
await loadAssets(scene)
|
|
||||||
|
|
||||||
// Set zone and characters
|
// Set zone and characters
|
||||||
zoneStore.setZone(response.zone)
|
zoneStore.setZone(response.zone)
|
||||||
zoneStore.setCharacters(response.characters)
|
zoneStore.setCharacters(response.characters)
|
||||||
|
14
src/components/zone/ZoneObjects.vue
Normal 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>
|
@ -7,17 +7,15 @@ import config from '@/config'
|
|||||||
import { useScene } from 'phavuer'
|
import { useScene } from 'phavuer'
|
||||||
import { useZoneStore } from '@/stores/zoneStore'
|
import { useZoneStore } from '@/stores/zoneStore'
|
||||||
import { onBeforeMount, onBeforeUnmount } from 'vue'
|
import { onBeforeMount, onBeforeUnmount } from 'vue'
|
||||||
import { placeTile, setAllTiles } from '@/composables/zoneComposable'
|
import { setLayerTiles } from '@/composables/zoneComposable'
|
||||||
import Controls from '@/components/utilities/Controls.vue'
|
import Controls from '@/components/utilities/Controls.vue'
|
||||||
|
|
||||||
const emit = defineEmits(['tilemap:create'])
|
const emit = defineEmits(['tilemap:create'])
|
||||||
|
|
||||||
const zoneStore = useZoneStore()
|
const zoneStore = useZoneStore()
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
|
|
||||||
const zoneTilemap = createTilemap()
|
const zoneTilemap = createTilemap()
|
||||||
const tiles = createTileLayer()
|
const tiles = createTileLayer()
|
||||||
let tileArray = createTileArray()
|
|
||||||
|
|
||||||
function createTilemap() {
|
function createTilemap() {
|
||||||
const zoneData = new Phaser.Tilemaps.MapData({
|
const zoneData = new Phaser.Tilemaps.MapData({
|
||||||
@ -51,17 +49,11 @@ function createTileLayer() {
|
|||||||
return layer
|
return layer
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTileArray() {
|
|
||||||
return Array.from({ length: zoneStore.zone?.width ?? 0 }, () => Array.from({ length: zoneStore.zone?.height ?? 0 }, () => 'blank_tile'))
|
|
||||||
}
|
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
if (zoneStore.zone?.tiles) {
|
if (!zoneStore.zone?.tiles) {
|
||||||
setAllTiles(zoneTilemap, tiles, zoneStore.zone.tiles)
|
return
|
||||||
tileArray = zoneStore.zone.tiles.map((row) => row.map((tileId) => tileId || 'blank_tile'))
|
|
||||||
} else {
|
|
||||||
tileArray.forEach((row, y) => row.forEach((_, x) => placeTile(zoneTilemap, tiles, x, y, 'blank_tile')))
|
|
||||||
}
|
}
|
||||||
|
setLayerTiles(zoneTilemap, tiles, zoneStore.zone.tiles)
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
63
src/components/zone/partials/ZoneObject.vue
Normal 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>
|
0
src/composables/gameComposable.ts
Normal file
@ -9,7 +9,7 @@ export function useGamePointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilema
|
|||||||
const dragThreshold = 5 // pixels
|
const dragThreshold = 5 // pixels
|
||||||
|
|
||||||
function updateWaypoint(worldX: number, worldY: number) {
|
function updateWaypoint(worldX: number, worldY: number) {
|
||||||
const pointerTile = getTile(worldX, worldY, layer)
|
const pointerTile = getTile(layer, worldX, worldY)
|
||||||
if (pointerTile) {
|
if (pointerTile) {
|
||||||
const worldPoint = tileToWorldXY(layer, pointerTile.x, pointerTile.y)
|
const worldPoint = tileToWorldXY(layer, pointerTile.x, pointerTile.y)
|
||||||
waypoint.value = {
|
waypoint.value = {
|
||||||
@ -46,7 +46,7 @@ export function useGamePointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilema
|
|||||||
const distance = Phaser.Math.Distance.Between(pointerStartPosition.value.x, pointerStartPosition.value.y, pointer.x, pointer.y)
|
const distance = Phaser.Math.Distance.Between(pointerStartPosition.value.x, pointerStartPosition.value.y, pointer.x, pointer.y)
|
||||||
|
|
||||||
if (distance <= dragThreshold) {
|
if (distance <= dragThreshold) {
|
||||||
const pointerTile = getTile(pointer.worldX, pointer.worldY, layer)
|
const pointerTile = getTile(layer, pointer.worldX, pointer.worldY)
|
||||||
if (pointerTile) {
|
if (pointerTile) {
|
||||||
gameStore.connection?.emit('character:initMove', {
|
gameStore.connection?.emit('character:initMove', {
|
||||||
positionX: pointerTile.x,
|
positionX: pointerTile.x,
|
||||||
|
@ -11,7 +11,7 @@ export function useZoneEditorPointerHandlers(scene: Phaser.Scene, layer: Phaser.
|
|||||||
|
|
||||||
function updateWaypoint(pointer: Phaser.Input.Pointer) {
|
function updateWaypoint(pointer: Phaser.Input.Pointer) {
|
||||||
const { x: px, y: py } = camera.getWorldPoint(pointer.x, pointer.y)
|
const { x: px, y: py } = camera.getWorldPoint(pointer.x, pointer.y)
|
||||||
const pointerTile = getTile(px, py, layer)
|
const pointerTile = getTile(layer, px, py)
|
||||||
|
|
||||||
if (pointerTile) {
|
if (pointerTile) {
|
||||||
const worldPoint = tileToWorldXY(layer, pointerTile.x, pointerTile.y)
|
const worldPoint = tileToWorldXY(layer, pointerTile.x, pointerTile.y)
|
||||||
|
@ -2,84 +2,70 @@ import config from '@/config'
|
|||||||
import Tilemap = Phaser.Tilemaps.Tilemap
|
import Tilemap = Phaser.Tilemaps.Tilemap
|
||||||
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
|
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
|
||||||
import Tileset = Phaser.Tilemaps.Tileset
|
import Tileset = Phaser.Tilemaps.Tileset
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import Tile = Phaser.Tilemaps.Tile
|
||||||
|
|
||||||
export function getTile(x: number, y: number, layer: Phaser.Tilemaps.TilemapLayer): Phaser.Tilemaps.Tile | undefined {
|
export function getTile(layer: TilemapLayer | Tilemap, x: number, y: number): Tile | undefined {
|
||||||
const tile: Phaser.Tilemaps.Tile = layer.getTileAtWorldXY(x, y)
|
const tile = layer.getTileAtWorldXY(x, y)
|
||||||
if (!tile) return undefined
|
if (!tile) return undefined
|
||||||
return tile
|
return tile
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tileToWorldXY(layer: Phaser.Tilemaps.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)
|
const worldPoint = layer.tileToWorldXY(pos_x, pos_y)
|
||||||
|
if (!worldPoint) return { positionX: 0, positionY: 0 }
|
||||||
|
|
||||||
const positionX = worldPoint.x + config.tile_size.y
|
const positionX = worldPoint.x + config.tile_size.y
|
||||||
const positionY = worldPoint.y
|
const positionY = worldPoint.y
|
||||||
|
|
||||||
return { positionX, positionY }
|
return { positionX, positionY }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tileToWorldX(layer: Phaser.Tilemaps.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)
|
const worldPoint = layer.tileToWorldXY(pos_x, pos_y)
|
||||||
|
if (!worldPoint) return 0
|
||||||
|
|
||||||
return worldPoint.x + config.tile_size.x / 2
|
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)
|
const worldPoint = layer.tileToWorldXY(pos_x, pos_y)
|
||||||
|
if (!worldPoint) return 0
|
||||||
|
|
||||||
return worldPoint.y + config.tile_size.y * 1.5
|
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) {
|
export function placeTile(zone: Tilemap, layer: TilemapLayer, x: number, y: number, tileName: string) {
|
||||||
const tileImg = zone.getTileset(tileName) as Tileset
|
let tileImg = zone.getTileset(tileName) as Tileset
|
||||||
if (!tileImg) return
|
if (!tileImg) {
|
||||||
|
tileImg = zone.getTileset('blank_tile') as Tileset
|
||||||
|
}
|
||||||
layer.putTileAt(tileImg.firstgid, x, y)
|
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, y) => {
|
tiles.forEach((row: string[], y: number) => {
|
||||||
row.forEach((tile, x) => {
|
row.forEach((tile: string, x: number) => {
|
||||||
placeTile(zone, layer, x, y, tile)
|
placeTile(zone, layer, x, y, tile)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) => {
|
export const calculateIsometricDepth = (x: number, y: number, width: number = 0, height: number = 0, isCharacter: boolean = false) => {
|
||||||
const baseDepth = x + y
|
const baseDepth = x + y
|
||||||
if (isCharacter) {
|
if (isCharacter) {
|
||||||
return baseDepth // @TODO: Fix collision, this is a hack
|
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)
|
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 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()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
@ -5,7 +5,8 @@ import { createPinia } from 'pinia'
|
|||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
const pinia = createPinia()
|
||||||
|
|
||||||
app.use(createPinia())
|
app.use(pinia)
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
@ -5,14 +5,9 @@
|
|||||||
<div class="filler"></div>
|
<div class="filler"></div>
|
||||||
<div class="flex gap-14 w-full max-h-[650px] overflow-x-auto" v-if="!isLoading">
|
<div class="flex gap-14 w-full max-h-[650px] overflow-x-auto" v-if="!isLoading">
|
||||||
<!-- CHARACTER LIST -->
|
<!-- CHARACTER LIST -->
|
||||||
<div
|
<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 }">
|
||||||
v-for="character in characters"
|
<img src="/assets/ui-box-outer.svg" class="absolute w-full h-full" />
|
||||||
:key="character.id"
|
<img src="/assets/ui-box-inner.svg" class="absolute left-2 bottom-2 w-[calc(100%_-_16px)] h-[calc(100%_-_40px)]" />
|
||||||
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/login/login-box-outer.svg" class="absolute w-full h-full max-lg:hidden" />
|
|
||||||
<img src="/assets/login/login-box-inner.svg" class="absolute left-2 bottom-2 w-[calc(100%_-_16px)] h-[calc(100%_-_40px)] max-lg:hidden" />
|
|
||||||
<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" />
|
<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>
|
<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>
|
||||||
|
|
||||||
@ -66,21 +61,23 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- CREATE CHARACTER MODAL -->
|
<!-- CREATE CHARACTER MODAL -->
|
||||||
<Modal :isModalOpen="isModalOpen" @modal:close="isModalOpen = false">
|
<Modal :isModalOpen="isModalOpen" @modal:close="isModalOpen = false" :modal-width="430" :modal-height="275">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="m-0 font-medium text-gray-300">Create your character</h3>
|
<h3 class="m-0 font-medium text-white">Create your character</h3>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #modalBody>
|
<template #modalBody>
|
||||||
<div class="m-4 character-form">
|
<div class="p-4 h-[calc(100%_-_32px)]">
|
||||||
<form method="post" @submit.prevent="create" class="inline">
|
<form method="post" @submit.prevent="create" class="h-full flex flex-col justify-between">
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="name">Name</label>
|
<label for="name" class="text-white">Nickname</label>
|
||||||
<input class="input-field max-w-64" v-model="name" name="name" id="name" />
|
<input class="input-field" v-model="name" name="name" id="name" placeholder="Enter a nickname.." />
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-flow-col justify-stretch gap-4">
|
||||||
|
<button type="button" class="btn-empty py-1.5 px-4 inline-block" @click.prevent="isModalOpen = false">Cancel</button>
|
||||||
|
<button class="btn-cyan py-1.5 px-4 inline-block" type="submit">Create</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-cyan py-1.5 px-4 mr-5 min-w-24 inline-block" type="submit">CREATE</button>
|
|
||||||
</form>
|
</form>
|
||||||
<button class="btn-cyan py-1.5 px-4 min-w-24 inline-block" @click="isModalOpen = false">CANCEL</button>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
@ -88,11 +85,13 @@
|
|||||||
<!-- DELETE CHARACTER MODAL -->
|
<!-- DELETE CHARACTER MODAL -->
|
||||||
<ConfirmationModal v-if="deletingCharacter != null" :confirm-function="delete_character.bind(this, deletingCharacter.id)" :cancel-function="(() => (deletingCharacter = null)).bind(this)" confirm-button-text="Delete">
|
<ConfirmationModal v-if="deletingCharacter != null" :confirm-function="delete_character.bind(this, deletingCharacter.id)" :cancel-function="(() => (deletingCharacter = null)).bind(this)" confirm-button-text="Delete">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="m-0 font-medium text-gray-300">Deleting character</h3>
|
<h3 class="m-0 font-medium text-white">Delete character?</h3>
|
||||||
</template>
|
</template>
|
||||||
<template #modalBody>
|
<template #modalBody>
|
||||||
You are about to delete <span class="font-extrabold">{{ deletingCharacter.name }}</span
|
<p class="mt-0 mb-5 text-white text-lg">
|
||||||
>, are you sure about that?
|
Do you want to permanently delete <span class="font-extrabold text-white">{{ deletingCharacter.name }}</span
|
||||||
|
>?
|
||||||
|
</p>
|
||||||
</template>
|
</template>
|
||||||
</ConfirmationModal>
|
</ConfirmationModal>
|
||||||
</template>
|
</template>
|
||||||
@ -115,13 +114,7 @@ gameStore.connection?.on('character:list', (data: any) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
/**
|
// wait 0.75 sec
|
||||||
* 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
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
gameStore.connection?.emit('character:list')
|
gameStore.connection?.emit('character:list')
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
|
@ -1,67 +1,58 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex justify-center items-center h-dvh relative">
|
<div class="flex justify-center items-center h-dvh relative">
|
||||||
<GmTools v-if="gameStore.character?.role === 'gm'" />
|
|
||||||
<GmPanel v-if="gameStore.character?.role === 'gm'" />
|
|
||||||
|
|
||||||
<div v-if="!zoneEditorStore.active">
|
|
||||||
<Game :config="gameConfig" @create="createGame">
|
<Game :config="gameConfig" @create="createGame">
|
||||||
<Scene name="main" @preload="preloadScene" @create="createScene">
|
<Scene name="main" @preload="preloadScene" @create="createScene">
|
||||||
<div v-if="isLoaded">
|
|
||||||
<Menu />
|
<Menu />
|
||||||
<Hud />
|
<Hud />
|
||||||
<Keybindings />
|
<Hotkeys />
|
||||||
<Minimap />
|
<Minimap />
|
||||||
<Zone />
|
<Zone />
|
||||||
<Chat />
|
<Chat />
|
||||||
<Inventory />
|
|
||||||
<ExpBar />
|
<ExpBar />
|
||||||
|
|
||||||
|
<Inventory />
|
||||||
<Effects />
|
<Effects />
|
||||||
</div>
|
|
||||||
</Scene>
|
</Scene>
|
||||||
</Game>
|
</Game>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="zoneEditorStore.active">
|
|
||||||
<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}`)" />
|
|
||||||
</Scene>
|
|
||||||
</Game>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import config from '@/config'
|
||||||
import 'phaser'
|
import 'phaser'
|
||||||
import { ref, onBeforeUnmount } from 'vue'
|
import { onBeforeUnmount } from 'vue'
|
||||||
import { Game, Scene } from 'phavuer'
|
import { Game, Scene } from 'phavuer'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
|
||||||
import Menu from '@/components/gui/Menu.vue'
|
import Menu from '@/components/gui/Menu.vue'
|
||||||
import ExpBar from '@/components/gui/ExpBar.vue'
|
import ExpBar from '@/components/gui/ExpBar.vue'
|
||||||
import Hud from '@/components/gui/Hud.vue'
|
import Hud from '@/components/gui/Hud.vue'
|
||||||
import Zone from '@/components/zone/Zone.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 Chat from '@/components/gui/Chat.vue'
|
||||||
import GmTools from '@/components/gameMaster/GmTools.vue'
|
|
||||||
import ZoneEditor from '@/components/gameMaster/zoneEditor/ZoneEditor.vue'
|
|
||||||
import GmPanel from '@/components/gameMaster/GmPanel.vue'
|
|
||||||
import Inventory from '@/components/gui/UserPanel.vue'
|
import Inventory from '@/components/gui/UserPanel.vue'
|
||||||
import Effects from '@/components/Effects.vue'
|
import Effects from '@/components/Effects.vue'
|
||||||
import { loadAssets } from '@/composables/zoneComposable'
|
|
||||||
import Minimap from '@/components/gui/Minimap.vue'
|
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 gameStore = useGameStore()
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const assetManager = useAssetManager
|
||||||
const isLoaded = ref(false)
|
|
||||||
|
|
||||||
const gameConfig = {
|
const gameConfig = {
|
||||||
name: 'New Quest',
|
name: config.name,
|
||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
height: window.innerHeight,
|
height: window.innerHeight,
|
||||||
type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS
|
type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS
|
||||||
resolution: 5
|
resolution: 5,
|
||||||
|
plugins: {
|
||||||
|
global: [
|
||||||
|
{
|
||||||
|
key: 'rexAwaitLoader',
|
||||||
|
plugin: AwaitLoaderPlugin,
|
||||||
|
start: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createGame = (game: Phaser.Game) => {
|
const createGame = (game: Phaser.Game) => {
|
||||||
@ -82,67 +73,40 @@ const createGame = (game: Phaser.Game) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const preloadScene = async (scene: Phaser.Scene) => {
|
function preloadScene(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
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the base assets into the 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_tile', '/assets/zone/blank_tile.png')
|
||||||
scene.load.image('blank_object', '/assets/zone/blank_tile.png')
|
|
||||||
scene.load.image('waypoint', '/assets/waypoint.png')
|
scene.load.image('waypoint', '/assets/waypoint.png')
|
||||||
|
|
||||||
/**
|
scene.load.rexAwait(async function (successCallback) {
|
||||||
* Load the assets into the Phaser scene
|
await assetManager.getAssetsByGroup('tiles').then((assets) => {
|
||||||
*/
|
assets.forEach((asset) => {
|
||||||
await loadAssets(scene)
|
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
|
* Create sprite animations
|
||||||
* This is done here because phaser forces us to
|
* This is done here because phaser forces us to
|
||||||
*/
|
*/
|
||||||
gameStore.assets.forEach((asset) => {
|
assetManager.getAssetsByGroup('sprite_animations').then((assets) => {
|
||||||
if (asset.group !== 'sprite_animations') return
|
assets.forEach((asset) => {
|
||||||
|
|
||||||
scene.anims.create({
|
scene.anims.create({
|
||||||
key: asset.key,
|
key: asset.key,
|
||||||
frameRate: 7,
|
frameRate: 7,
|
||||||
@ -150,18 +114,8 @@ const createScene = async (scene: Phaser.Scene) => {
|
|||||||
repeat: -1
|
repeat: -1
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {})
|
||||||
isLoaded.value = false
|
|
||||||
gameStore.disconnectSocket()
|
|
||||||
})
|
|
||||||
</script>
|
</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
@ -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>
|
@ -1,26 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative max-lg:h-dvh">
|
<div class="relative max-lg:h-dvh flex flex-row-reverse">
|
||||||
<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="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 right-0 max-lg:bottom-0 lg:top-0 bg-no-repeat bg-cover bg-center"></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="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">
|
<div class="h-dvh flex items-center lg:justify-center flex-col px-8 max-lg:pt-20">
|
||||||
<img src="/assets/login/nq-logo-v1.png" class="mb-10" />
|
<img src="/assets/login/sq-logo-v1.svg" class="mb-10" />
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<img src="/assets/login/login-box-outer.svg" class="absolute w-full h-full max-lg:hidden" />
|
<img src="/assets/ui-box-outer.svg" class="absolute w-full h-full" />
|
||||||
<img src="/assets/login/login-box-inner.svg" class="absolute left-2 top-2 w-[calc(100%_-_16px)] h-[calc(100%_-_16px)] max-lg:hidden" />
|
<img src="/assets/ui-box-inner.svg" class="absolute left-2 top-2 w-[calc(100%_-_16px)] h-[calc(100%_-_16px)] max-lg:hidden" />
|
||||||
|
|
||||||
<!-- Login Form -->
|
<!-- Login Form -->
|
||||||
<form v-show="switchForm === 'login'" @submit.prevent="loginFunc" class="relative px-6 py-11">
|
<form v-show="switchForm === 'login'" @submit.prevent="loginFunc" class="relative px-6 py-11">
|
||||||
<div class="flex flex-col gap-5 p-2 mb-8 relative">
|
<div class="flex flex-col gap-5 p-2 mb-8 relative">
|
||||||
<div class="w-full grid gap-3 relative">
|
<div class="w-full grid gap-3 relative">
|
||||||
<input class="input-field xs:min-w-[350px] min-w-64" id="username" v-model="username" type="text" name="username" placeholder="Username" required autofocus />
|
<input class="input-field xs:min-w-[350px] min-w-64" id="username-login" v-model="username" type="text" name="username" placeholder="Username" required autofocus />
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<input class="input-field xs:min-w-[350px] min-w-64" id="password" v-model="password" type="password" name="password" placeholder="Password" required />
|
<input class="input-field xs:min-w-[350px] min-w-64" id="password-login" v-model="password" :type="showPassword ? 'text' : 'password'" name="password" placeholder="Password" required />
|
||||||
<button class="p-0 absolute right-3 w-4 h-3 top-1/2 -translate-y-1/2 bg-[url('/assets/icons/eye.svg')] bg-no-repeat"></button>
|
<button type="button" @click.prevent="showPassword = !showPassword" :class="{ 'eye-open': showPassword }" class="bg-[url('/assets/icons/eye.svg')] p-0 absolute right-3 w-4 h-3 top-1/2 -translate-y-1/2 bg-no-repeat"></button>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="loginError" class="text-red-200 text-xs absolute top-full mt-1">{{ loginError }}</span>
|
<span v-if="loginError" class="text-red-200 text-xs absolute top-full mt-1">{{ loginError }}</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="text-right text-cyan-300 text-base">Forgot password?</button>
|
<button class="inline-flex self-end p-0 text-cyan-300 text-base">Forgot password?</button>
|
||||||
<button class="btn-cyan px-0 xs:w-full" type="submit">Play now</button>
|
<button class="btn-cyan px-0 xs:w-full" type="submit">Play now</button>
|
||||||
|
|
||||||
<!-- Divider shape -->
|
<!-- Divider shape -->
|
||||||
@ -39,10 +39,10 @@
|
|||||||
<form v-show="switchForm === 'register'" @submit.prevent="registerFunc" class="relative px-6 py-11">
|
<form v-show="switchForm === 'register'" @submit.prevent="registerFunc" class="relative px-6 py-11">
|
||||||
<div class="flex flex-col gap-5 p-2 mb-8 relative">
|
<div class="flex flex-col gap-5 p-2 mb-8 relative">
|
||||||
<div class="w-full grid gap-3 relative">
|
<div class="w-full grid gap-3 relative">
|
||||||
<input class="input-field xs:min-w-[350px] min-w-64" id="username" v-model="username" type="text" name="username" placeholder="Username" required autofocus />
|
<input class="input-field xs:min-w-[350px] min-w-64" id="username-register" v-model="username" type="text" name="username" placeholder="Username" required autofocus />
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<input class="input-field xs:min-w-[350px] min-w-64" id="password" v-model="password" type="password" name="password" placeholder="Password" required />
|
<input class="input-field xs:min-w-[350px] min-w-64" id="password-register" v-model="password" :type="showPassword ? 'text' : 'password'" name="password" placeholder="Password" required />
|
||||||
<button class="p-0 absolute right-3 w-4 h-3 top-1/2 -translate-y-1/2 bg-[url('/assets/icons/eye.svg')] bg-no-repeat"></button>
|
<button type="button" @click.prevent="showPassword = !showPassword" :class="{ 'eye-open': showPassword }" class="bg-[url('/assets/icons/eye.svg')] p-0 absolute right-3 w-4 h-3 top-1/2 -translate-y-1/2 bg-no-repeat"></button>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="loginError" class="text-red-200 text-xs absolute top-full mt-1">{{ loginError }}</span>
|
<span v-if="loginError" class="text-red-200 text-xs absolute top-full mt-1">{{ loginError }}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -76,6 +76,8 @@ const username = ref('')
|
|||||||
const password = ref('')
|
const password = ref('')
|
||||||
const switchForm = ref('login')
|
const switchForm = ref('login')
|
||||||
const loginError = ref('')
|
const loginError = ref('')
|
||||||
|
const showPassword = ref(false)
|
||||||
|
|
||||||
// automatic login because of development
|
// automatic login because of development
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const token = useCookies().get('token')
|
const token = useCookies().get('token')
|
||||||
|
@ -1 +0,0 @@
|
|||||||
<template></template>
|
|
117
src/screens/ZoneEditor.vue
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex justify-center items-center h-dvh relative">
|
||||||
|
<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}`)" />
|
||||||
|
</Scene>
|
||||||
|
</Game>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<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 { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
|
import ZoneEditor from '@/components/gameMaster/zoneEditor/ZoneEditor.vue'
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
|
const isLoaded = ref(false)
|
||||||
|
|
||||||
|
const gameConfig = {
|
||||||
|
name: config.name,
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight,
|
||||||
|
type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS
|
||||||
|
resolution: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
const createGame = (game: Phaser.Game) => {
|
||||||
|
/**
|
||||||
|
* Resize the game when the window is resized
|
||||||
|
*/
|
||||||
|
addEventListener('resize', () => {
|
||||||
|
game.scale.resize(window.innerWidth, window.innerHeight)
|
||||||
|
})
|
||||||
|
|
||||||
|
// We don't support canvas mode, only WebGL
|
||||||
|
if (game.renderer.type === Phaser.CANVAS) {
|
||||||
|
gameStore.addNotification({
|
||||||
|
title: 'Warning',
|
||||||
|
message: 'Your browser does not support WebGL. Please use a modern browser like Chrome, Firefox, or Edge.'
|
||||||
|
})
|
||||||
|
gameStore.disconnectSocket()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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('waypoint', '/assets/waypoint.png')
|
||||||
|
}
|
||||||
|
|
||||||
|
const createScene = async (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
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
isLoaded.value = false
|
||||||
|
})
|
||||||
|
</script>
|
@ -1,9 +1,8 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import config from '@/config'
|
import config from '@/config'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
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 {
|
try {
|
||||||
const response = await axios.post(`${config.server_endpoint}/register`, { username, password })
|
const response = await axios.post(`${config.server_endpoint}/register`, { username, password })
|
||||||
useCookies().set('token', response.data.token as string)
|
useCookies().set('token', response.data.token as string)
|
||||||
@ -13,12 +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 {
|
try {
|
||||||
const response = await axios.post(`${config.server_endpoint}/login`, { username, password })
|
const response = await axios.post(`${config.server_endpoint}/login`, { username, password })
|
||||||
useCookies().set('token', response.data.token as string, {
|
useCookies().set('token', response.data.token as string, {
|
||||||
// for whole domain
|
// for whole domain
|
||||||
domain: window.location.hostname.split('.').slice(-2).join('.')
|
// @TODO : #190
|
||||||
|
// domain: window.location.hostname.split('.').slice(-2).join('.')
|
||||||
})
|
})
|
||||||
return { success: true, token: response.data.token }
|
return { success: true, token: response.data.token }
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import type { Tile, Object, Sprite } from '@/types'
|
import type { Tile, Object, Sprite, CharacterType } from '@/types'
|
||||||
|
|
||||||
export const useAssetManagerStore = defineStore('assetManager', () => {
|
export const useAssetManagerStore = defineStore('assetManager', () => {
|
||||||
const tileList = ref<Tile[]>([])
|
const tileList = ref<Tile[]>([])
|
||||||
@ -12,6 +12,9 @@ export const useAssetManagerStore = defineStore('assetManager', () => {
|
|||||||
const spriteList = ref<Sprite[]>([])
|
const spriteList = ref<Sprite[]>([])
|
||||||
const selectedSprite = ref<Sprite | null>(null)
|
const selectedSprite = ref<Sprite | null>(null)
|
||||||
|
|
||||||
|
const characterTypeList = ref<CharacterType[]>([])
|
||||||
|
const selectedCharacterType = ref<CharacterType | null>(null)
|
||||||
|
|
||||||
function setTileList(tiles: Tile[]) {
|
function setTileList(tiles: Tile[]) {
|
||||||
tileList.value = tiles
|
tileList.value = tiles
|
||||||
}
|
}
|
||||||
@ -36,6 +39,14 @@ export const useAssetManagerStore = defineStore('assetManager', () => {
|
|||||||
selectedSprite.value = sprite
|
selectedSprite.value = sprite
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setCharacterTypeList(characterTypes: CharacterType[]) {
|
||||||
|
characterTypeList.value = characterTypes
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSelectedCharacterType(characterType: CharacterType | null) {
|
||||||
|
selectedCharacterType.value = characterType
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tileList,
|
tileList,
|
||||||
selectedTile,
|
selectedTile,
|
||||||
@ -43,11 +54,15 @@ export const useAssetManagerStore = defineStore('assetManager', () => {
|
|||||||
selectedObject,
|
selectedObject,
|
||||||
spriteList,
|
spriteList,
|
||||||
selectedSprite,
|
selectedSprite,
|
||||||
|
characterTypeList,
|
||||||
|
selectedCharacterType,
|
||||||
setTileList,
|
setTileList,
|
||||||
setSelectedTile,
|
setSelectedTile,
|
||||||
setObjectList,
|
setObjectList,
|
||||||
|
setCharacterTypeList,
|
||||||
setSelectedObject,
|
setSelectedObject,
|
||||||
setSpriteList,
|
setSpriteList,
|
||||||
setSelectedSprite
|
setSelectedSprite,
|
||||||
|
setSelectedCharacterType
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,20 +1,26 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { io, Socket } from 'socket.io-client'
|
import { io, Socket } from 'socket.io-client'
|
||||||
import type { Asset, Character, Notification, User } from '@/types'
|
import type { Asset, Character, Notification, User, WorldSettings } from '@/types'
|
||||||
import config from '@/config'
|
import config from '@/config'
|
||||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||||
|
|
||||||
export const useGameStore = defineStore('game', {
|
export const useGameStore = defineStore('game', {
|
||||||
state: () => {
|
state: () => {
|
||||||
return {
|
return {
|
||||||
loginMessage: null as string | null,
|
|
||||||
notifications: [] as Notification[],
|
notifications: [] as Notification[],
|
||||||
assets: [] as Asset[],
|
isAssetsLoaded: false,
|
||||||
|
loadedAssets: [] as string[],
|
||||||
token: '' as string | null,
|
token: '' as string | null,
|
||||||
connection: null as Socket | null,
|
connection: null as Socket | null,
|
||||||
user: null as User | null,
|
user: null as User | null,
|
||||||
character: null as Character | null,
|
character: null as Character | null,
|
||||||
isPlayerDraggingCamera: false,
|
isPlayerDraggingCamera: false,
|
||||||
|
world: {
|
||||||
|
date: new Date(),
|
||||||
|
isRainEnabled: false,
|
||||||
|
isFogEnabled: false,
|
||||||
|
fogDensity: 0.5
|
||||||
|
} as WorldSettings,
|
||||||
gameSettings: {
|
gameSettings: {
|
||||||
isCameraFollowingCharacter: false
|
isCameraFollowingCharacter: false
|
||||||
},
|
},
|
||||||
@ -25,12 +31,6 @@ export const useGameStore = defineStore('game', {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getters: {
|
|
||||||
getNotifications: (state: any) => state.notifications,
|
|
||||||
getAssetByKey: (state) => {
|
|
||||||
return (key: string) => state.assets.find((asset) => asset.key === key)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions: {
|
actions: {
|
||||||
addNotification(notification: Notification) {
|
addNotification(notification: Notification) {
|
||||||
if (!notification.id) {
|
if (!notification.id) {
|
||||||
@ -41,54 +41,6 @@ export const useGameStore = defineStore('game', {
|
|||||||
removeNotification(id: string) {
|
removeNotification(id: string) {
|
||||||
this.notifications = this.notifications.filter((notification: Notification) => notification.id !== id)
|
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) {
|
setToken(token: string) {
|
||||||
this.token = token
|
this.token = token
|
||||||
},
|
},
|
||||||
@ -146,14 +98,15 @@ export const useGameStore = defineStore('game', {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
disconnectSocket() {
|
disconnectSocket() {
|
||||||
if (this.connection) this.connection.disconnect()
|
this.connection?.disconnect()
|
||||||
|
|
||||||
useCookies().remove('token', {
|
useCookies().remove('token', {
|
||||||
// for whole domain
|
// for whole domain
|
||||||
domain: window.location.hostname.split('.').slice(-2).join('.')
|
// @TODO : #190
|
||||||
|
// domain: window.location.hostname.split('.').slice(-2).join('.')
|
||||||
})
|
})
|
||||||
|
|
||||||
this.assets = []
|
this.isAssetsLoaded = false
|
||||||
this.connection = null
|
this.connection = null
|
||||||
this.token = null
|
this.token = null
|
||||||
this.user = null
|
this.user = null
|
||||||
@ -163,6 +116,11 @@ export const useGameStore = defineStore('game', {
|
|||||||
this.gameSettings.isCameraFollowingCharacter = false
|
this.gameSettings.isCameraFollowingCharacter = false
|
||||||
this.uiSettings.isChatOpen = false
|
this.uiSettings.isChatOpen = false
|
||||||
this.uiSettings.isUserPanelOpen = false
|
this.uiSettings.isUserPanelOpen = false
|
||||||
|
|
||||||
|
this.world.date = new Date()
|
||||||
|
this.world.isRainEnabled = false
|
||||||
|
this.world.isFogEnabled = false
|
||||||
|
this.world.fogDensity = 0.5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|