Compare commits
95 Commits
feature/15
...
feature/#1
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
3c7e96ea7f | |||
1cfdf1857e | |||
c86fd2e564 | |||
f2e439831a | |||
474de8b14a | |||
b264ab3e40 | |||
4293ec63b6 | |||
34393a31ac | |||
53daa758a8 | |||
86f2510f3a | |||
e610e866c7 | |||
15442764c2 | |||
8c3a488e7d | |||
f51cb839bf | |||
376f8653d6 | |||
9b474909b3 | |||
95d322a63c | |||
16a8435f7b | |||
334ceaa8f3 | |||
f6b6b4b8ea | |||
7a50385420 | |||
e5213cf5e6 | |||
cc07d132e8 | |||
61b2717fb5 | |||
99d66a7c42 | |||
eb4ae4a625 | |||
586600da7c | |||
0ffec44038 | |||
1b86b25bc2 | |||
cd9316a384 | |||
80bb38a6f7 | |||
0a37a09ed4 | |||
1f46b94441 | |||
df3b9db45d | |||
d214bd37ad | |||
c31dada1f9 | |||
9cf872b7e2 | |||
5b31729f64 | |||
fda5224806 | |||
fd599907e5 | |||
c2ae271306 | |||
494576b284 |
@ -27,5 +27,6 @@ RUN npm run build-ntc
|
||||
# Production stage
|
||||
FROM nginx:1.26.1-alpine
|
||||
COPY --from=builder /usr/src/app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
@ -3,8 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
|
||||
<title>New Quest - Play</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Sylvan Quest - Play</title>
|
||||
</head>
|
||||
<body>
|
||||
<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;
|
||||
}
|
||||
}
|
1214
package-lock.json
generated
32
package.json
@ -15,15 +15,15 @@
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^10.11.0",
|
||||
"@vueuse/integrations": "^10.11.0",
|
||||
"axios": "^1.7.2",
|
||||
"phaser": "^3.80.1",
|
||||
"pinia": "^2.1.7",
|
||||
"socket.io-client": "^4.7.5",
|
||||
"@vueuse/core": "^10.5.0",
|
||||
"@vueuse/integrations": "^10.5.0",
|
||||
"axios": "^1.7.7",
|
||||
"phaser": "^3.86.0",
|
||||
"pinia": "^2.1.6",
|
||||
"socket.io-client": "^4.8.0",
|
||||
"universal-cookie": "^6.1.3",
|
||||
"vue": "^3.4.33",
|
||||
"zod": "^3.23.8"
|
||||
"vue": "^3.5.12",
|
||||
"zod": "^3.22.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.10.3",
|
||||
@ -40,16 +40,16 @@
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-vue": "^9.27.0",
|
||||
"jsdom": "^24.1.1",
|
||||
"npm-run-all2": "^6.2.2",
|
||||
"phaser3-rex-plugins": "^1.80.5",
|
||||
"npm-run-all2": "^6.2.3",
|
||||
"phaser3-rex-plugins": "^1.80.8",
|
||||
"phavuer": "^0.16.1",
|
||||
"postcss": "^8.4.39",
|
||||
"postcss": "^8.4.47",
|
||||
"prettier": "^3.3.3",
|
||||
"sass": "^1.77.8",
|
||||
"tailwindcss": "^3.4.6",
|
||||
"typescript": "~5.5.3",
|
||||
"vite": "^5.3.4",
|
||||
"vite-plugin-vue-devtools": "^7.3.6",
|
||||
"sass": "^1.79.4",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^5.4.9",
|
||||
"vite-plugin-vue-devtools": "^7.5.2",
|
||||
"vitest": "^2.0.3",
|
||||
"vue-tsc": "^1.6.5"
|
||||
}
|
||||
|
BIN
public/assets/fog.png
Normal file
After Width: | Height: | Size: 432 KiB |
BIN
public/assets/fog.webp
Normal file
After Width: | Height: | Size: 6.1 KiB |
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 |
3
public/assets/icons/eye.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="12" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.00004 0C11.5948 0 14.5854 2.58651 15.2124 6C14.5854 9.41347 11.5948 12 8.00004 12C4.40525 12 1.4146 9.41347 0.787598 6C1.4146 2.58651 4.40525 0 8.00004 0ZM8.00004 10.6667C10.8238 10.6667 13.24 8.70133 13.8516 6C13.24 3.29869 10.8238 1.33333 8.00004 1.33333C5.17624 1.33333 2.75998 3.29869 2.14836 6C2.75998 8.70133 5.17624 10.6667 8.00004 10.6667ZM8.00004 9C6.34316 9 5.00001 7.65687 5.00001 6C5.00001 4.34315 6.34316 3 8.00004 3C9.65684 3 11 4.34315 11 6C11 7.65687 9.65684 9 8.00004 9ZM8.00004 7.66667C8.92051 7.66667 9.66671 6.92047 9.66671 6C9.66671 5.07953 8.92051 4.33333 8.00004 4.33333C7.07957 4.33333 6.33334 5.07953 6.33334 6C6.33334 6.92047 7.07957 7.66667 8.00004 7.66667Z" fill="#808080"/>
|
||||
</svg>
|
After Width: | Height: | Size: 818 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 -->
|
||||
<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>
|
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 |
BIN
public/assets/login/login-bg.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
public/assets/login/nq-logo-v1.png
Normal file
After Width: | Height: | Size: 20 KiB |
15
public/assets/login/sq-logo-v1.svg
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
public/assets/raindrop.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
public/assets/raindrop.webp
Normal file
After Width: | Height: | Size: 274 B |
@ -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 |
@ -1 +1 @@
|
||||
<svg width="290" xmlns="http://www.w3.org/2000/svg" height="87" id="screenshot-e9942e24-155b-8096-8004-7eaff9882cd6" 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-7eaff9882cd6"><g class="fills" id="fills-e9942e24-155b-8096-8004-7eaff9882cd6"><path d="M3.485,8.722C7.089,3.457,13.144,0.000,20.000,0.000L270.000,0.000C281.038,0.000,290.000,8.962,290.000,20.000L290.000,67.000C290.000,78.038,281.038,87.000,270.000,87.000L20.000,87.000C13.166,87.000,7.128,83.565,3.520,78.329C21.157,76.589,35.000,61.648,35.000,43.500C35.000,25.390,21.216,10.475,3.485,8.722ZM0.000,67.000L0.000,20.000"/></g><g id="strokes-e9942e24-155b-8096-8004-7eaff9882cd6" class="strokes"><g class="inner-stroke-shape"><defs><clipPath id="inner-stroke-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0"><use href="#stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0"/></clipPath><path d="M3.485,8.722C7.089,3.457,13.144,0.000,20.000,0.000L270.000,0.000C281.038,0.000,290.000,8.962,290.000,20.000L290.000,67.000C290.000,78.038,281.038,87.000,270.000,87.000L20.000,87.000C13.166,87.000,7.128,83.565,3.520,78.329C21.157,76.589,35.000,61.648,35.000,43.500C35.000,25.390,21.216,10.475,3.485,8.722ZM0.000,67.000L0.000,20.000" id="stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0" style="fill: none; stroke-width: 6; stroke: rgb(255, 255, 255); stroke-opacity: 1;"/></defs><use href="#stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0" clip-path="url('#inner-stroke-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0')"/></g></g></g></svg>
|
||||
<svg width="290" xmlns="http://www.w3.org/2000/svg" height="87" id="screenshot-e9942e24-155b-8096-8004-7eaff9882cd6" 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-7eaff9882cd6"><g class="fills" id="fills-e9942e24-155b-8096-8004-7eaff9882cd6"><path d="M3.485,8.722C7.089,3.457,13.144,0.000,20.000,0.000L270.000,0.000C281.038,0.000,290.000,8.962,290.000,20.000L290.000,67.000C290.000,78.038,281.038,87.000,270.000,87.000L20.000,87.000C13.166,87.000,7.128,83.565,3.520,78.329C21.157,76.589,35.000,61.648,35.000,43.500C35.000,25.390,21.216,10.475,3.485,8.722ZM0.000,67.000L0.000,20.000"/></g><g id="strokes-e9942e24-155b-8096-8004-7eaff9882cd6" class="strokes"><g class="inner-stroke-shape"><defs><clipPath id="inner-stroke-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0"><use href="#stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0"/></clipPath><path d="M3.485,8.722C7.089,3.457,13.144,0.000,20.000,0.000L270.000,0.000C281.038,0.000,290.000,8.962,290.000,20.000L290.000,67.000C290.000,78.038,281.038,87.000,270.000,87.000L20.000,87.000C13.166,87.000,7.128,83.565,3.520,78.329C21.157,76.589,35.000,61.648,35.000,43.500C35.000,25.390,21.216,10.475,3.485,8.722ZM0.000,67.000L0.000,20.000" id="stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0" style="fill: none; stroke-width: 6; stroke: rgb(77 77 77); stroke-opacity: 1;"/></defs><use href="#stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0" clip-path="url('#inner-stroke-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0')"/></g></g></g></svg>
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
@ -1 +1 @@
|
||||
<svg width="1508.086" xmlns="http://www.w3.org/2000/svg" height="1511.251" id="screenshot-0d120e2a-8725-8061-8004-79728483f7ea" viewBox="-201.784 -208.012 1508.086 1511.251" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-0d120e2a-8725-8061-8004-79728483f7ea" width="800px" height="800px" rx="0" ry="0" style="opacity: 0.3; fill: rgb(0, 0, 0);"><g id="shape-0d120e2a-8725-8061-8004-79728484b3fe"><g class="fills" id="fills-0d120e2a-8725-8061-8004-79728484b3fe"><path d="M1190.359,745.690L1043.367,575.945L1133.504,630.603C1099.180,585.722,1047.978,532.622,975.722,469.519L780.783,401.898L896.468,404.538C851.234,371.382,797.068,339.090,738.130,311.073L601.350,337.338L689.349,289.039C627.425,263.088,562.143,241.922,497.713,229.160C430.172,215.674,363.453,211.534,303.512,221.641L314.753,151.382C271.664,177.012,239.130,209.992,214.226,251.017L204.390,177.710C166.181,212.950,148.095,250.172,143.131,301.343C69.092,307.974,-2.300,327.925,-73.861,347.005L-63.628,384.898C361.675,238.903,753.109,407.667,987.467,615.054L960.305,643.506C749.259,458.743,490.332,358.712,193.406,380.541C209.110,415.226,228.858,447.126,251.288,474.785L371.998,430.671L277.417,504.449C294.635,521.771,312.591,536.670,331.111,548.765L396.081,470.998L358.366,564.253C377.625,573.924,397.183,580.217,416.674,582.837C534.164,599.232,652.310,618.566,782.785,703.535L773.618,601.955L831.163,737.792C852.261,754.489,876.370,771.191,897.168,782.701L861.169,684.190L960.409,811.519C976.589,817.512,992.991,822.477,1008.953,826.486C1066.083,840.287,1120.015,842.594,1157.256,819.002C1175.975,807.393,1189.205,786.963,1190.791,762.853C1191.080,756.933,1190.907,750.762,1190.359,745.690ZZ" style="fill: rgb(54, 143, 139);"/></g></g></g></svg>
|
||||
<svg width="1508.086" xmlns="http://www.w3.org/2000/svg" height="1511.251" id="screenshot-0d120e2a-8725-8061-8004-79728483f7ea" viewBox="-201.784 -208.012 1508.086 1511.251" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-0d120e2a-8725-8061-8004-79728483f7ea" width="800px" height="800px" rx="0" ry="0" style="opacity: 0.3; fill: rgb(0, 0, 0);"><g id="shape-0d120e2a-8725-8061-8004-79728484b3fe"><g class="fills" id="fills-0d120e2a-8725-8061-8004-79728484b3fe"><path d="M1190.359,745.690L1043.367,575.945L1133.504,630.603C1099.180,585.722,1047.978,532.622,975.722,469.519L780.783,401.898L896.468,404.538C851.234,371.382,797.068,339.090,738.130,311.073L601.350,337.338L689.349,289.039C627.425,263.088,562.143,241.922,497.713,229.160C430.172,215.674,363.453,211.534,303.512,221.641L314.753,151.382C271.664,177.012,239.130,209.992,214.226,251.017L204.390,177.710C166.181,212.950,148.095,250.172,143.131,301.343C69.092,307.974,-2.300,327.925,-73.861,347.005L-63.628,384.898C361.675,238.903,753.109,407.667,987.467,615.054L960.305,643.506C749.259,458.743,490.332,358.712,193.406,380.541C209.110,415.226,228.858,447.126,251.288,474.785L371.998,430.671L277.417,504.449C294.635,521.771,312.591,536.670,331.111,548.765L396.081,470.998L358.366,564.253C377.625,573.924,397.183,580.217,416.674,582.837C534.164,599.232,652.310,618.566,782.785,703.535L773.618,601.955L831.163,737.792C852.261,754.489,876.370,771.191,897.168,782.701L861.169,684.190L960.409,811.519C976.589,817.512,992.991,822.477,1008.953,826.486C1066.083,840.287,1120.015,842.594,1157.256,819.002C1175.975,807.393,1189.205,786.963,1190.791,762.853C1191.080,756.933,1190.907,750.762,1190.359,745.690ZZ" style="fill: rgb(13 109 105);"/></g></g></g></svg>
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
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 |
23
public/assets/ui-box-inner.svg
Normal file
After Width: | Height: | Size: 301 KiB |
23
public/assets/ui-box-outer.svg
Normal file
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 |
Before Width: | Height: | Size: 372 B After Width: | Height: | Size: 250 B |
Before Width: | Height: | Size: 135 B After Width: | Height: | Size: 109 B |
Before Width: | Height: | Size: 920 B After Width: | Height: | Size: 696 B |
Before Width: | Height: | Size: 834 B After Width: | Height: | Size: 708 B |
37
src/App.vue
@ -1,33 +1,34 @@
|
||||
<template>
|
||||
<div class="overflow-hidden">
|
||||
<Notifications />
|
||||
<Login v-if="screen === 'login'" />
|
||||
<!-- <Register v-if="screen === 'register'" />-->
|
||||
<Characters v-if="screen === 'characters'" />
|
||||
<Game v-if="screen === 'game'" />
|
||||
</div>
|
||||
<Notifications />
|
||||
<GmTools v-if="gameStore.character?.role === 'gm'" />
|
||||
<GmPanel v-if="gameStore.character?.role === 'gm'" />
|
||||
|
||||
<component :is="currentScreen" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
import Notifications from '@/components/utilities/Notifications.vue'
|
||||
import GmTools from '@/components/gameMaster/GmTools.vue'
|
||||
import GmPanel from '@/components/gameMaster/GmPanel.vue'
|
||||
import Login from '@/screens/Login.vue'
|
||||
// import Register from '@/screens/Register.vue'
|
||||
import Characters from '@/screens/Characters.vue'
|
||||
import Game from '@/screens/Game.vue'
|
||||
import ZoneEditor from '@/screens/ZoneEditor.vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const zoneEditorStore = useZoneEditorStore()
|
||||
|
||||
const screen = computed(() => {
|
||||
if (!gameStore.connection) {
|
||||
return 'login'
|
||||
} else if (gameStore.token && gameStore.connection) {
|
||||
if (gameStore.character) {
|
||||
return 'game'
|
||||
}
|
||||
return 'characters'
|
||||
}
|
||||
return 'login' // default fallback
|
||||
const currentScreen = computed(() => {
|
||||
if (!gameStore.connection) return Login
|
||||
if (!gameStore.token) return Login
|
||||
if (!gameStore.character) return Characters
|
||||
if (zoneEditorStore.active) return ZoneEditor
|
||||
return Game
|
||||
})
|
||||
|
||||
// Disable right click
|
||||
addEventListener('contextmenu', (event) => event.preventDefault())
|
||||
</script>
|
||||
|
BIN
src/assets/fonts/upheavtt.ttf
Normal file
@ -3,12 +3,16 @@
|
||||
@tailwind utilities;
|
||||
|
||||
// Fonts
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&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
|
||||
|
||||
body {
|
||||
@apply bg-black m-0 select-none overscroll-none overflow-hidden;
|
||||
@apply bg-black m-0 select-none;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
@ -26,14 +30,14 @@ h5,
|
||||
h6,
|
||||
button,
|
||||
a {
|
||||
@apply font-titles text-white font-medium m-0;
|
||||
@apply font-default text-gray-200 font-medium m-0;
|
||||
}
|
||||
|
||||
p,
|
||||
span,
|
||||
li,
|
||||
label {
|
||||
@apply font-default text-white;
|
||||
@apply font-default text-gray-200;
|
||||
}
|
||||
|
||||
button,
|
||||
@ -56,12 +60,8 @@ input {
|
||||
}
|
||||
}
|
||||
|
||||
.input-cyan {
|
||||
@apply py-2 px-2.5 font-titles border border-solid border-cyan bg-white/70 rounded;
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
@apply outline-2 outline-cyan;
|
||||
}
|
||||
.input-field {
|
||||
@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 {
|
||||
@apply bg-gray-600/50 hover:cursor-not-allowed;
|
||||
&::placeholder {
|
||||
@ -87,28 +87,54 @@ button {
|
||||
@apply text-center;
|
||||
|
||||
&.btn-cyan {
|
||||
@apply bg-cyan/50 border border-solid border-white/25 rounded drop-shadow-20;
|
||||
@apply bg-cyan text-gray-50 text-base leading-5 rounded py-2.5;
|
||||
|
||||
&.active,
|
||||
&:hover {
|
||||
@apply bg-cyan;
|
||||
@apply bg-cyan-800;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-bordeaux {
|
||||
@apply bg-bordeaux/50 border border-solid border-white/25 rounded drop-shadow-20;
|
||||
&.btn-red {
|
||||
@apply bg-red text-gray-50 text-base leading-5 rounded py-2.5;
|
||||
|
||||
&.active,
|
||||
&:hover {
|
||||
@apply bg-bordeaux;
|
||||
@apply bg-red-300;
|
||||
}
|
||||
}
|
||||
|
||||
&.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 {
|
||||
@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 {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
canvas {
|
||||
image-rendering: -moz-crisp-edges;
|
||||
image-rendering: -webkit-crisp-edges;
|
||||
image-rendering: pixelated;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
110
src/components/Effects.vue
Normal file
@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<Scene name="effects" @preload="preloadScene" @create="createScene" @update="updateScene"> </Scene>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Scene } from 'phavuer'
|
||||
import { useZoneStore } from '@/stores/zoneStore'
|
||||
import { onBeforeUnmount, ref, watch } from 'vue'
|
||||
|
||||
const zoneStore = useZoneStore()
|
||||
|
||||
const sceneRef = ref<Phaser.Scene | null>(null)
|
||||
|
||||
// Effect-related refs
|
||||
const lightEffect = ref<Phaser.GameObjects.Graphics | null>(null)
|
||||
const rainEmitter = ref<Phaser.GameObjects.Particles.ParticleEmitter | null>(null)
|
||||
const fogSprite = ref<Phaser.GameObjects.Sprite | null>(null)
|
||||
|
||||
const preloadScene = async (scene: Phaser.Scene) => {
|
||||
scene.load.image('raindrop', 'assets/raindrop.png')
|
||||
scene.load.image('fog', 'assets/fog.png')
|
||||
}
|
||||
|
||||
const createScene = async (scene: Phaser.Scene) => {
|
||||
sceneRef.value = scene
|
||||
createLightEffect(scene)
|
||||
createRainEffect(scene)
|
||||
createFogEffect(scene)
|
||||
}
|
||||
|
||||
const updateScene = () => {
|
||||
updateEffects()
|
||||
}
|
||||
|
||||
const createLightEffect = (scene: Phaser.Scene) => {
|
||||
lightEffect.value = scene.add.graphics()
|
||||
lightEffect.value.setDepth(1000)
|
||||
}
|
||||
|
||||
const createRainEffect = (scene: Phaser.Scene) => {
|
||||
rainEmitter.value = scene.add.particles(0, 0, 'raindrop', {
|
||||
x: { min: 0, max: window.innerWidth },
|
||||
y: -50,
|
||||
quantity: 5,
|
||||
lifespan: 2000,
|
||||
speedY: { min: 300, max: 500 },
|
||||
scale: { start: 0.005, end: 0.005 },
|
||||
alpha: { start: 0.5, end: 0 },
|
||||
blendMode: 'ADD'
|
||||
})
|
||||
rainEmitter.value.setDepth(900)
|
||||
rainEmitter.value.stop()
|
||||
}
|
||||
|
||||
const createFogEffect = (scene: Phaser.Scene) => {
|
||||
fogSprite.value = scene.add.sprite(window.innerWidth / 2, window.innerHeight / 2, 'fog')
|
||||
fogSprite.value.setScale(2)
|
||||
fogSprite.value.setAlpha(0)
|
||||
fogSprite.value.setDepth(950)
|
||||
}
|
||||
|
||||
const updateEffects = () => {
|
||||
const effects = zoneStore.zone?.zoneEffects || []
|
||||
|
||||
effects.forEach((effect) => {
|
||||
switch (effect.effect) {
|
||||
case 'light':
|
||||
updateLightEffect(effect.strength)
|
||||
break
|
||||
case 'rain':
|
||||
updateRainEffect(effect.strength)
|
||||
break
|
||||
case 'fog':
|
||||
updateFogEffect(effect.strength)
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const updateLightEffect = (strength: number) => {
|
||||
if (!lightEffect.value) return
|
||||
const darkness = 1 - strength / 100
|
||||
lightEffect.value.clear()
|
||||
lightEffect.value.fillStyle(0x000000, darkness)
|
||||
lightEffect.value.fillRect(0, 0, window.innerWidth, window.innerHeight)
|
||||
}
|
||||
|
||||
const updateRainEffect = (strength: number) => {
|
||||
if (!rainEmitter.value) return
|
||||
if (strength > 0) {
|
||||
rainEmitter.value.start()
|
||||
rainEmitter.value.setQuantity(Math.floor((strength / 100) * 10))
|
||||
} else {
|
||||
rainEmitter.value.stop()
|
||||
}
|
||||
}
|
||||
|
||||
const updateFogEffect = (strength: number) => {
|
||||
if (!fogSprite.value) return
|
||||
fogSprite.value.setAlpha(strength / 100)
|
||||
}
|
||||
|
||||
watch(() => zoneStore.zone?.zoneEffects, updateEffects, { deep: true })
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (sceneRef.value) sceneRef.value.scene.remove('effects')
|
||||
})
|
||||
|
||||
// @TODO : Fix resize issue
|
||||
</script>
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex flex-wrap items-center input-cyan 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">
|
||||
<span class="text-xs">{{ 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>
|
||||
|
@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<Modal :isModalOpen="gameStore.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>
|
||||
<h3 class="m-0 font-medium shrink-0">GM Panel</h3>
|
||||
<div class="flex gap-1.5 flex-wrap">
|
||||
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">General</button>
|
||||
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Users</button>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<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">
|
||||
<template #modalHeader>
|
||||
<h3 class="m-0 font-medium shrink-0">GM tools</h3>
|
||||
<h3 class="m-0 font-medium shrink-0 text-white">GM tools</h3>
|
||||
</template>
|
||||
<template #modalBody>
|
||||
<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 gameStore = useGameStore()
|
||||
const modalWidth = ref(200)
|
||||
const modalHeight = ref(160)
|
||||
const modalHeight = ref(170)
|
||||
|
||||
let posXY = ref({ x: 0, y: 0 })
|
||||
|
||||
|
@ -4,53 +4,67 @@
|
||||
<!-- Asset Categories -->
|
||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'tiles' }" @click="() => (selectedCategory = 'tiles')">
|
||||
<span>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 class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'objects' }" @click="() => (selectedCategory = 'objects')">
|
||||
<span>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 class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'sprites' }" @click="() => (selectedCategory = 'sprites')">
|
||||
<span>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 class="relative p-2.5 hover:cursor-pointer">
|
||||
<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 class="relative p-2.5 hover:cursor-pointer">
|
||||
<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 class="relative p-2.5 hover:cursor-pointer">
|
||||
<span>Characters</span>
|
||||
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
|
||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'shops' }" @click="() => (selectedCategory = 'shops')">
|
||||
<span>Shops</span>
|
||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||
</a>
|
||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'characterTypes' }" @click="() => (selectedCategory = 'characterTypes')">
|
||||
<span>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>Character hair</span>
|
||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||
</a>
|
||||
<a class="relative p-2.5 hover:cursor-pointer">
|
||||
<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 class="relative p-2.5 hover:cursor-pointer">
|
||||
<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>
|
||||
</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 -->
|
||||
<div class="overflow-auto h-full w-4/12 flex flex-col relative">
|
||||
<TileList v-if="selectedCategory === 'tiles'" />
|
||||
<ObjectList v-if="selectedCategory === 'objects'" />
|
||||
<SpriteList v-if="selectedCategory === 'sprites'" />
|
||||
<CharacterTypeList v-if="selectedCategory === 'characterTypes'" />
|
||||
</div>
|
||||
|
||||
<div class="absolute w-px bg-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 -->
|
||||
<div class="flex w-1/2 after:hidden flex-col relative overflow-auto">
|
||||
<TileDetails v-if="selectedCategory === 'tiles' && assetManagerStore.selectedTile" />
|
||||
<ObjectDetails v-if="selectedCategory === 'objects' && assetManagerStore.selectedObject" />
|
||||
<SpriteDetails v-if="selectedCategory === 'sprites' && assetManagerStore.selectedSprite" />
|
||||
<CharacterTypeDetails v-if="selectedCategory === 'characterTypes' && assetManagerStore.selectedCharacterType" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -64,6 +78,8 @@ import ObjectList from '@/components/gameMaster/assetManager/partials/object/Obj
|
||||
import ObjectDetails from '@/components/gameMaster/assetManager/partials/object/ObjectDetails.vue'
|
||||
import SpriteList from '@/components/gameMaster/assetManager/partials/sprite/SpriteList.vue'
|
||||
import SpriteDetails from '@/components/gameMaster/assetManager/partials/sprite/SpriteDetails.vue'
|
||||
import CharacterTypeList from '@/components/gameMaster/assetManager/partials/characterType/CharacterTypeList.vue'
|
||||
import CharacterTypeDetails from '@/components/gameMaster/assetManager/partials/characterType/CharacterTypeDetails.vue'
|
||||
|
||||
const assetManagerStore = useAssetManagerStore()
|
||||
const selectedCategory = ref('tiles')
|
||||
|
@ -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/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 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="currentColor">
|
||||
<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/50 p-0 hover:bg-cyan" 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>
|
@ -3,22 +3,22 @@
|
||||
<div class="relative p-2.5 flex flex-col items-center justify-between h-72">
|
||||
<div class="filler"></div>
|
||||
<img class="max-h-56" :src="`${config.server_endpoint}/assets/objects/${selectedObject?.id}.png`" :alt="'Object ' + selectedObject?.id" />
|
||||
<button class="btn-bordeaux 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>
|
||||
<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-gray-500"></div>
|
||||
</div>
|
||||
<div class="m-2.5 p-2.5 block">
|
||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject">
|
||||
<div class="form-field-full">
|
||||
<label for="name">Name</label>
|
||||
<input v-model="objectName" class="input-cyan" type="text" name="name" placeholder="Wall #1" />
|
||||
<input v-model="objectName" class="input-field" type="text" name="name" placeholder="Wall #1" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="origin-x">Origin X</label>
|
||||
<input v-model="objectOriginX" class="input-cyan" type="number" step="any" name="origin-x" placeholder="Origin X" />
|
||||
<input v-model="objectOriginX" class="input-field" type="number" step="any" name="origin-x" placeholder="Origin X" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="origin-y">Origin Y</label>
|
||||
<input v-model="objectOriginY" class="input-cyan" type="number" step="any" name="origin-y" placeholder="Origin Y" />
|
||||
<input v-model="objectOriginY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="origin-x">Tags</label>
|
||||
@ -26,22 +26,22 @@
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="origin-x">Is animated</label>
|
||||
<select v-model="objectIsAnimated" class="input-cyan" name="is-animated">
|
||||
<select v-model="objectIsAnimated" class="input-field" name="is-animated">
|
||||
<option :value="false">No</option>
|
||||
<option :value="true">Yes</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="frame-speed">Frame speed</label>
|
||||
<input v-model="objectFrameSpeed" class="input-cyan" type="number" step="any" name="frame-speed" placeholder="Frame speed" />
|
||||
<input v-model="objectFrameSpeed" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame speed" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="frame-width">Frame width</label>
|
||||
<input v-model="objectFrameWidth" class="input-cyan" type="number" step="any" name="frame-width" placeholder="Frame width" />
|
||||
<input v-model="objectFrameWidth" class="input-field" type="number" step="any" name="frame-width" placeholder="Frame width" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="frame-height">Frame height</label>
|
||||
<input v-model="objectFrameHeight" class="input-cyan" type="number" step="any" name="frame-height" placeholder="Frame height" />
|
||||
<input v-model="objectFrameHeight" class="input-field" type="number" step="any" name="frame-height" placeholder="Frame height" />
|
||||
</div>
|
||||
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
||||
</form>
|
||||
|
@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div class="relative p-2.5 flex items-center gap-x-2.5">
|
||||
<input v-model="searchQuery" class="input-cyan 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">
|
||||
<input class="hidden" id="upload-asset" ref="objectUploadField" type="file" accept="image/png" multiple @change="handleFileUpload" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</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 v-bind="containerProps" class="overflow-y-auto relative" @scroll="onScroll">
|
||||
<div v-bind="wrapperProps" ref="elementToScroll">
|
||||
@ -18,10 +18,10 @@
|
||||
</div>
|
||||
<span>{{ object.name }}</span>
|
||||
</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>
|
||||
</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/50 p-0 hover:bg-cyan" 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>
|
||||
|
@ -4,13 +4,13 @@
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div class="w-full flex flex-col">
|
||||
<label class="mb-1.5 font-titles" for="name">Name</label>
|
||||
<input v-model="spriteName" class="input-cyan" type="text" name="name" placeholder="New sprite" />
|
||||
<input v-model="spriteName" class="input-field" type="text" name="name" placeholder="New sprite" />
|
||||
</div>
|
||||
|
||||
<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-bordeaux 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>
|
||||
<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-gray-500"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -19,40 +19,40 @@
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
{{ action.action }}
|
||||
<button class="btn-bordeaux px-4 py-1.5 min-w-24" type="button" @click.prevent="() => spriteActions.splice(spriteActions.indexOf(action), 1)">Delete</button>
|
||||
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="() => spriteActions.splice(spriteActions.indexOf(action), 1)">Delete</button>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveSprite">
|
||||
<div class="form-field-full">
|
||||
<label for="action">Action</label>
|
||||
<input v-model="action.action" class="input-cyan" type="text" name="action" placeholder="Action" />
|
||||
<input v-model="action.action" class="input-field" type="text" name="action" placeholder="Action" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="origin-x">Origin X</label>
|
||||
<input v-model.number="action.originX" class="input-cyan" type="number" step="any" name="origin-x" placeholder="Origin X" />
|
||||
<input v-model.number="action.originX" class="input-field" type="number" step="any" name="origin-x" placeholder="Origin X" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="origin-y">Origin Y</label>
|
||||
<input v-model.number="action.originY" class="input-cyan" type="number" step="any" name="origin-y" placeholder="Origin Y" />
|
||||
<input v-model.number="action.originY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="is-animated">Is animated</label>
|
||||
<select v-model="action.isAnimated" class="input-cyan" name="is-animated">
|
||||
<select v-model="action.isAnimated" class="input-field" name="is-animated">
|
||||
<option :value="false">No</option>
|
||||
<option :value="true">Yes</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field-half" v-if="action.isAnimated">
|
||||
<label for="is-looping">Is looping</label>
|
||||
<select v-model="action.isLooping" class="input-cyan" name="is-looping">
|
||||
<select v-model="action.isLooping" class="input-field" name="is-looping">
|
||||
<option :value="false">No</option>
|
||||
<option :value="true">Yes</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field-full" v-if="action.isAnimated">
|
||||
<label for="frame-speed">Frame speed</label>
|
||||
<input v-model.number="action.frameSpeed" class="input-cyan" type="number" step="any" name="frame-speed" placeholder="Frame speed" />
|
||||
<input v-model.number="action.frameSpeed" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame speed" />
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<SpriteActionsInput v-model="action.sprites" />
|
||||
|
@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="relative p-2.5 flex items-center gap-x-2.5">
|
||||
<input v-model="searchQuery" class="input-cyan 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">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</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 v-bind="containerProps" class="overflow-y-auto relative" @scroll="onScroll">
|
||||
<div v-bind="wrapperProps" ref="elementToScroll">
|
||||
@ -14,10 +14,10 @@
|
||||
<div class="flex items-center gap-2.5">
|
||||
<span>{{ sprite.name }}</span>
|
||||
</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>
|
||||
</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/50 p-0 hover:bg-cyan" 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>
|
||||
|
@ -3,14 +3,14 @@
|
||||
<div class="relative p-2.5 flex flex-col items-center justify-between h-72">
|
||||
<div class="filler"></div>
|
||||
<img class="max-h-72" :src="`${config.server_endpoint}/assets/tiles/${selectedTile?.id}.png`" :alt="'Tile ' + selectedTile?.id" />
|
||||
<button class="btn-bordeaux 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>
|
||||
<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-gray-500"></div>
|
||||
</div>
|
||||
<div class="m-2.5 p-2.5 block">
|
||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveTile">
|
||||
<div class="form-field-full">
|
||||
<label for="name">Name</label>
|
||||
<input v-model="tileName" class="input-cyan" type="text" name="name" placeholder="Tile #1" />
|
||||
<input v-model="tileName" class="input-field" type="text" name="name" placeholder="Tile #1" />
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="origin-x">Tags</label>
|
||||
|
@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div class="relative p-2.5 flex items-center gap-x-2.5">
|
||||
<input v-model="searchQuery" class="input-cyan 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">
|
||||
<input class="hidden" id="upload-asset" ref="tileUploadField" type="file" accept="image/png" multiple @change="handleFileUpload" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</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 v-bind="containerProps" class="overflow-y-auto relative" @scroll="onScroll">
|
||||
<div v-bind="wrapperProps" ref="elementToScroll">
|
||||
@ -18,10 +18,10 @@
|
||||
</div>
|
||||
<span>{{ tile.name }}</span>
|
||||
</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>
|
||||
</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/50 p-0 hover:bg-cyan" 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>
|
||||
|
112
src/components/gameMaster/zoneEditor/EventTiles.vue
Normal file
@ -0,0 +1,112 @@
|
||||
<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 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 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>
|
206
src/components/gameMaster/zoneEditor/Objects.vue
Normal file
@ -0,0 +1,206 @@
|
||||
<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 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,
|
||||
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 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>
|
140
src/components/gameMaster/zoneEditor/Tiles.vue
Normal file
@ -0,0 +1,140 @@
|
||||
<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, setAllTiles } 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 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 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
|
||||
setAllTiles(zoneTilemap, tiles, createTileArray(zoneTilemap.width, zoneTilemap.height, zoneEditorStore.selectedTile.id))
|
||||
|
||||
// Adjust zoneEditorStore.zone.tiles
|
||||
zoneEditorStore.zone.tiles = createTileArray(zoneTilemap.width, zoneTilemap.height, zoneEditorStore.selectedTile.id)
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
if (!zoneEditorStore.zone?.tiles) {
|
||||
return
|
||||
}
|
||||
setAllTiles(zoneTilemap, tiles, zoneEditorStore.zone.tiles)
|
||||
|
||||
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,214 +1,56 @@
|
||||
<template>
|
||||
<Toolbar :layer="tiles" @eraser="eraser" @pencil="pencil" @paint="paint" @clear="clear" @save="save" />
|
||||
<ZoneList v-if="zoneEditorStore.isZoneListModalShown" />
|
||||
<Tiles @tilemap:create="tileMap = $event" />
|
||||
<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">
|
||||
<Controls :layer="tiles as TilemapLayer" />
|
||||
<Toolbar @save="save" />
|
||||
|
||||
<Tiles />
|
||||
<Objects />
|
||||
<ZoneList />
|
||||
<TileList />
|
||||
<ObjectList />
|
||||
|
||||
<ZoneSettings />
|
||||
<TeleportModal v-if="shouldShowTeleportModal" />
|
||||
|
||||
<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)" />
|
||||
</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" />
|
||||
</template>
|
||||
<ZoneSettings />
|
||||
<TeleportModal />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeMount, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { Container, Image, useScene } from 'phavuer'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onBeforeMount, onUnmounted, ref } from 'vue'
|
||||
import { useScene } from 'phavuer'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
import { calculateIsometricDepth, loadAssets, placeTile, setAllTiles, sortByIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||
import { ZoneEventTileType, type ZoneObject, type ZoneEventTile, type Zone } from '@/types'
|
||||
import { uuidv4 } from '@/utilities'
|
||||
import config from '@/config'
|
||||
import { loadAssets } from '@/composables/zoneComposable'
|
||||
import { type Zone } from '@/types'
|
||||
|
||||
// Components
|
||||
import Controls from '@/components/utilities/Controls.vue'
|
||||
import Toolbar from '@/components/gameMaster/zoneEditor/partials/Toolbar.vue'
|
||||
import Tiles from '@/components/gameMaster/zoneEditor/partials/TileList.vue'
|
||||
import SelectedZoneObject from '@/components/gameMaster/zoneEditor/partials/SelectedZoneObject.vue'
|
||||
import TileList from '@/components/gameMaster/zoneEditor/partials/TileList.vue'
|
||||
import ObjectList from '@/components/gameMaster/zoneEditor/partials/ObjectList.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 TeleportModal from '@/components/gameMaster/zoneEditor/partials/TeleportModal.vue'
|
||||
import Tilemap = Phaser.Tilemaps.Tilemap
|
||||
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
|
||||
|
||||
/**
|
||||
* @TODO:
|
||||
* Clean all the code in this file
|
||||
*/
|
||||
import Tiles from '@/components/gameMaster/zoneEditor/Tiles.vue'
|
||||
import Objects from '@/components/gameMaster/zoneEditor/Objects.vue'
|
||||
import EventTiles from '@/components/gameMaster/zoneEditor/EventTiles.vue'
|
||||
|
||||
const scene = useScene()
|
||||
const gameStore = useGameStore()
|
||||
const zoneEditorStore = useZoneEditorStore()
|
||||
|
||||
const { objectList, zone, selectedTile, selectedObject, selectedZoneObject, eraserMode, drawMode } = storeToRefs(zoneEditorStore)
|
||||
|
||||
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, 0, 0, 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, 0, 0, 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,
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null)
|
||||
|
||||
function save() {
|
||||
if (!zone.value) return
|
||||
if (!zoneEditorStore.zone) return
|
||||
|
||||
const data = {
|
||||
zoneId: zone.value.id,
|
||||
zoneId: zoneEditorStore.zone.id,
|
||||
name: zoneEditorStore.zoneSettings.name,
|
||||
width: zoneEditorStore.zoneSettings.width,
|
||||
height: zoneEditorStore.zoneSettings.height,
|
||||
tiles: tileArray,
|
||||
pvp: zone.value.pvp,
|
||||
zoneEventTiles: zoneEventTiles.value.map(({ id, zoneId, type, positionX, positionY, teleport }) => ({ id, zoneId, type, positionX, positionY, teleport })),
|
||||
zoneObjects: zoneObjects.value.map(({ id, zoneId, objectId, depth, positionX, positionY }) => ({ id, zoneId, objectId, depth, positionX, positionY }))
|
||||
tiles: zoneEditorStore.zone.tiles,
|
||||
pvp: zoneEditorStore.zone.pvp,
|
||||
zoneEffects: zoneEditorStore.zone.zoneEffects.map(({ id, zoneId, effect, strength }) => ({ id, zoneId, effect, strength })),
|
||||
zoneEventTiles: zoneEditorStore.zone.zoneEventTiles.map(({ id, zoneId, type, positionX, positionY, teleport }) => ({ id, zoneId, type, positionX, positionY, teleport })),
|
||||
zoneObjects: zoneEditorStore.zone.zoneObjects.map(({ id, zoneId, objectId, depth, isRotated, positionX, positionY }) => ({ id, zoneId, objectId, depth, isRotated, positionX, positionY }))
|
||||
}
|
||||
|
||||
if (zoneEditorStore.isSettingsModalShown) {
|
||||
@ -216,104 +58,16 @@ function save() {
|
||||
}
|
||||
|
||||
gameStore.connection?.emit('gm:zone_editor:zone:update', data, (response: Zone) => {
|
||||
console.log('zone updated')
|
||||
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')
|
||||
}
|
||||
|
||||
// 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(() => {
|
||||
zoneEventTiles.value = []
|
||||
zoneObjects.value = []
|
||||
tiles?.destroy()
|
||||
zoneTilemap?.removeAllLayers()
|
||||
zoneTilemap?.destroy()
|
||||
zoneEditorStore.reset()
|
||||
})
|
||||
</script>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<Modal :isModalOpen="true" @modal:close="() => zoneEditorStore.toggleCreateZoneModal()" :modal-width="300" :modal-height="400" :is-resizable="false">
|
||||
<template #modalHeader>
|
||||
<h3 class="m-0 font-medium shrink-0">Create new zone</h3>
|
||||
<h3 class="m-0 font-medium shrink-0 text-white">Create new zone</h3>
|
||||
</template>
|
||||
|
||||
<template #modalBody>
|
||||
@ -10,19 +10,19 @@
|
||||
<div class="gap-2.5 flex flex-wrap">
|
||||
<div class="form-field-full">
|
||||
<label for="name">Name</label>
|
||||
<input class="input-cyan max-w-64" v-model="name" name="name" id="name" />
|
||||
<input class="input-field max-w-64" v-model="name" name="name" id="name" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="name">Width</label>
|
||||
<input class="input-cyan max-w-64" v-model="width" name="name" id="name" type="number" />
|
||||
<input class="input-field max-w-64" v-model="width" name="name" id="name" type="number" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="name">Height</label>
|
||||
<input class="input-cyan max-w-64" v-model="height" name="name" id="name" type="number" />
|
||||
<input class="input-field max-w-64" v-model="height" name="name" id="name" type="number" />
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="name">PVP enabled</label>
|
||||
<select class="input-cyan" name="pvp" id="pvp">
|
||||
<select class="input-field" name="pvp" id="pvp">
|
||||
<option :value="false">No</option>
|
||||
<option :value="true">Yes</option>
|
||||
</select>
|
||||
|
@ -2,21 +2,17 @@
|
||||
<Teleport to="body">
|
||||
<Modal :isModalOpen="zoneEditorStore.isObjectListModalShown" :modal-width="645" :modal-height="260" @modal:close="() => (zoneEditorStore.isObjectListModalShown = false)">
|
||||
<template #modalHeader>
|
||||
<h3 class="text-lg">Objects</h3>
|
||||
<div class="flex">
|
||||
<h3 class="text-lg text-white">Objects</h3>
|
||||
</template>
|
||||
<template #modalBody>
|
||||
<div class="flex pt-4 pl-4">
|
||||
<div class="w-full flex gap-1.5 flex-row">
|
||||
<div>
|
||||
<label class="mb-1.5 font-titles hidden" for="search">Search...</label>
|
||||
<input @mousedown.stop class="input-cyan" 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>-->
|
||||
<!-- <label class="mb-1.5 font-titles hidden" for="depth">Depth</label>-->
|
||||
<!-- <input v-model="objectDepth" @mousedown.stop class="input-cyan" type="number" name="depth" placeholder="Depth" />-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #modalBody>
|
||||
<div class="flex flex-col h-full p-4">
|
||||
<div class="mb-4 flex flex-wrap gap-2">
|
||||
<button v-for="tag in uniqueTags" :key="tag" @click="toggleTag(tag)" class="btn-cyan" :class="{ 'opacity-50': !selectedTags.includes(tag) }">
|
||||
|
@ -1,50 +1,33 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center py-5 px-3 fixed bottom-14 right-0">
|
||||
<div class="self-end mt-2 flex gap-2">
|
||||
<div>
|
||||
<label class="mb-1.5 font-titles block text-sm text-gray-700 hidden" for="depth">Depth</label>
|
||||
<input v-model="objectDepth" @mousedown.stop @input="handleDepthInput" class="input-cyan max-w-24 px-2 py-1 border rounded" type="number" name="depth" placeholder="Depth" :disabled="!isObjectSelected" />
|
||||
</div>
|
||||
<button @mousedown.stop @click="handleDelete" class="btn-bordeaux py-1.5 px-4" :disabled="!isObjectSelected">
|
||||
<button @mousedown.stop @click="handleDelete" class="btn-red py-1.5 px-4">
|
||||
<img src="/assets/icons/trashcan.svg" class="w-4 h-4" alt="Delete" />
|
||||
</button>
|
||||
<button @mousedown.stop @click="zoneEditorStore.setSelectedObject(zoneEditorStore.selectedZoneObject?.object)" class="btn-cyan py-1.5 px-4" :disabled="!isObjectSelected">S</button>
|
||||
<button @mousedown.stop @click="handleMove" class="btn-cyan py-1.5 px-4 min-w-24" :disabled="!isObjectSelected">Move</button>
|
||||
<button @mousedown.stop @click="handleRotate" class="btn-cyan py-1.5 px-4">Rotate</button>
|
||||
<button @mousedown.stop @click="handleMove" class="btn-cyan py-1.5 px-4 min-w-24">Move</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import type { ZoneObject } from '@/types'
|
||||
|
||||
const emit = defineEmits(['update_depth', 'move', 'delete'])
|
||||
const zoneEditorStore = useZoneEditorStore()
|
||||
const props = defineProps<{
|
||||
zoneObject: ZoneObject
|
||||
}>()
|
||||
|
||||
const objectDepth = ref(zoneEditorStore.objectDepth)
|
||||
|
||||
const isObjectSelected = computed(() => !!zoneEditorStore.selectedZoneObject)
|
||||
|
||||
watch(
|
||||
() => zoneEditorStore.selectedZoneObject,
|
||||
(selectedZoneObject) => {
|
||||
objectDepth.value = selectedZoneObject?.depth ?? 0
|
||||
}
|
||||
)
|
||||
|
||||
const handleDepthInput = () => {
|
||||
const depth = parseFloat(objectDepth.value.toString())
|
||||
if (!isNaN(depth)) {
|
||||
emit('update_depth', depth)
|
||||
}
|
||||
}
|
||||
const emit = defineEmits(['move', 'rotate', 'delete'])
|
||||
|
||||
const handleMove = () => {
|
||||
emit('move')
|
||||
emit('move', props.zoneObject.id)
|
||||
}
|
||||
|
||||
const handleRotate = () => {
|
||||
emit('rotate', props.zoneObject.id)
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
emit('delete', zoneEditorStore.selectedZoneObject?.id)
|
||||
zoneEditorStore.setSelectedZoneObject(null)
|
||||
emit('delete', props.zoneObject.id)
|
||||
}
|
||||
</script>
|
||||
|
@ -1,24 +1,23 @@
|
||||
<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>
|
||||
<h3 class="m-0 font-medium shrink-0">Teleport settings</h3>
|
||||
<h3 class="m-0 font-medium shrink-0 text-white">Teleport settings</h3>
|
||||
</template>
|
||||
|
||||
<template #modalBody>
|
||||
<div class="m-4">
|
||||
<form method="post" @submit.prevent="" class="inline">
|
||||
<div class="gap-2.5 flex flex-wrap">
|
||||
<div class="form-field-half">
|
||||
<label for="positionX">Position X</label>
|
||||
<input class="input-cyan" v-model="toPositionX" name="positionX" id="positionX" type="number" />
|
||||
<input class="input-field" v-model="toPositionX" name="positionX" id="positionX" type="number" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="positionY">Position Y</label>
|
||||
<input class="input-cyan" v-model="toPositionY" name="positionY" id="positionY" type="number" />
|
||||
<input class="input-field" v-model="toPositionY" name="positionY" id="positionY" type="number" />
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="rotation">Rotation</label>
|
||||
<select v-model="toRotation" class="input-cyan" name="rotation" id="rotation">
|
||||
<select v-model="toRotation" class="input-field" name="rotation" id="rotation">
|
||||
<option :value="0">North</option>
|
||||
<option :value="2">East</option>
|
||||
<option :value="4">South</option>
|
||||
@ -27,7 +26,7 @@
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="toZoneId">Zone to teleport to</label>
|
||||
<select v-model="toZoneId" class="input-cyan" name="toZoneId" id="toZoneId">
|
||||
<select v-model="toZoneId" class="input-field" name="toZoneId" id="toZoneId">
|
||||
<option :value="0">Select zone</option>
|
||||
<option v-for="zone in zoneEditorStore.zoneList" :key="zone.id" :value="zone.id">{{ zone.name }}</option>
|
||||
</select>
|
||||
@ -40,12 +39,13 @@
|
||||
</template>
|
||||
|
||||
<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 { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import type { Zone } from '@/types'
|
||||
|
||||
const showTeleportModal = computed(() => zoneEditorStore.tool === 'pencil' && zoneEditorStore.drawMode === 'teleport')
|
||||
const zoneEditorStore = useZoneEditorStore()
|
||||
const gameStore = useGameStore()
|
||||
|
||||
|
@ -2,17 +2,17 @@
|
||||
<Teleport to="body">
|
||||
<Modal :isModalOpen="zoneEditorStore.isTileListModalShown" :modal-width="645" :modal-height="600" @modal:close="() => (zoneEditorStore.isTileListModalShown = false)">
|
||||
<template #modalHeader>
|
||||
<h3 class="text-lg">Tiles</h3>
|
||||
<div class="flex">
|
||||
<h3 class="text-lg text-white">Tiles</h3>
|
||||
</template>
|
||||
<template #modalBody>
|
||||
<div class="flex pt-4 pl-4">
|
||||
<div class="w-full flex gap-1.5 flex-row">
|
||||
<div>
|
||||
<label class="mb-1.5 font-titles hidden" for="search">Search...</label>
|
||||
<input @mousedown.stop class="input-cyan" 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>
|
||||
</template>
|
||||
<template #modalBody>
|
||||
<div class="flex flex-col h-full p-4">
|
||||
<div class="mb-4 flex flex-wrap gap-2">
|
||||
<button v-for="tag in uniqueTags" :key="tag" @click="toggleTag(tag)" class="btn-cyan" :class="{ 'opacity-50': !selectedTags.includes(tag) }">
|
||||
|
@ -1,21 +1,21 @@
|
||||
<template>
|
||||
<div class="flex justify-center p-5">
|
||||
<div class="toolbar fixed bottom-0 m-3 rounded left-0 right-0 flex bg-gray-300/80 solid border-solid border-2 border-cyan ext-gray-50 p-1.5 px-3 p min-w-11/12 h-10">
|
||||
<div ref="clickOutsideElement" 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-50 gap-2.5': zoneEditorStore.tool === 'move' }" @click="handleClick('move')">
|
||||
<div class="toolbar fixed bottom-0 left-0 m-3 rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10">
|
||||
<div ref="toolbar" class="tools flex gap-2.5" v-if="zoneEditorStore.zone">
|
||||
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': zoneEditorStore.tool === 'move' }" @click="handleClick('move')">
|
||||
<img class="invert w-5 h-5" src="/assets/icons/zoneEditor/move.svg" alt="Move camera" /> <span :class="{ 'ml-2.5': zoneEditorStore.tool !== 'move' }">(M)</span>
|
||||
</button>
|
||||
|
||||
<div class="w-px bg-cyan"></div>
|
||||
|
||||
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan-50 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>
|
||||
<div class="select" v-if="zoneEditorStore.tool === 'pencil'">
|
||||
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectPencilOpen }">
|
||||
{{ zoneEditorStore.drawMode }}
|
||||
<img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/zoneEditor/chevron.svg" />
|
||||
</div>
|
||||
<div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray-300/80 rounded min-w-28 border border-cyan 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')">
|
||||
Tile
|
||||
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
|
||||
@ -35,14 +35,14 @@
|
||||
|
||||
<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-50 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>
|
||||
<div class="select" v-if="zoneEditorStore.tool === 'eraser'">
|
||||
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectEraserOpen }">
|
||||
{{ zoneEditorStore.eraserMode }}
|
||||
<img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/zoneEditor/chevron.svg" />
|
||||
</div>
|
||||
<div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray-300/80 rounded min-w-28 border border-cyan 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')">
|
||||
Tile
|
||||
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
|
||||
@ -62,7 +62,7 @@
|
||||
|
||||
<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-50 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>
|
||||
</button>
|
||||
|
||||
@ -71,7 +71,7 @@
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2.5 ml-auto">
|
||||
<div class="toolbar fixed bottom-0 right-0 m-3 rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10 space-x-2">
|
||||
<button class="btn-cyan px-3.5" @click="() => zoneEditorStore.toggleZoneListModal()">Load</button>
|
||||
<button class="btn-cyan px-3.5" @click="() => emit('save')" v-if="zoneEditorStore.zone">Save</button>
|
||||
<button class="btn-cyan px-3.5" @click="() => emit('clear')" v-if="zoneEditorStore.zone">Clear</button>
|
||||
@ -83,22 +83,15 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { useScene } from 'phavuer'
|
||||
import { getTile } from '@/composables/zoneComposable'
|
||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
import { onClickOutside } from '@vueuse/core'
|
||||
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
|
||||
|
||||
const zoneEditorStore = useZoneEditorStore()
|
||||
|
||||
const props = defineProps({
|
||||
layer: Phaser.Tilemaps.TilemapLayer
|
||||
})
|
||||
const scene = useScene()
|
||||
const emit = defineEmits(['move', 'eraser', 'pencil', 'paint', 'save', 'clear'])
|
||||
const emit = defineEmits(['save', 'clear'])
|
||||
|
||||
// track when clicked outside of toolbar items
|
||||
const clickOutsideElement = ref(null)
|
||||
const toolbar = ref(null)
|
||||
|
||||
// track select state
|
||||
let selectPencilOpen = ref(false)
|
||||
@ -119,47 +112,6 @@ function setEraserMode(value: string) {
|
||||
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) {
|
||||
if (tool === 'settings') {
|
||||
zoneEditorStore.toggleSettingsModal()
|
||||
@ -171,13 +123,28 @@ function handleClick(tool: string) {
|
||||
selectEraserOpen.value = tool === 'eraser' ? !selectEraserOpen.value : false
|
||||
}
|
||||
|
||||
// Key bindings
|
||||
function cycleToolMode(tool: 'pencil' | 'eraser') {
|
||||
const modes = ['tile', 'object', 'teleport', 'blocking tile']
|
||||
const currentMode = tool === 'pencil' ? zoneEditorStore.drawMode : zoneEditorStore.eraserMode
|
||||
const currentIndex = modes.indexOf(currentMode)
|
||||
const nextIndex = (currentIndex + 1) % modes.length
|
||||
const nextMode = modes[nextIndex]
|
||||
|
||||
if (tool === 'pencil') {
|
||||
setDrawMode(nextMode)
|
||||
} else {
|
||||
setEraserMode(nextMode)
|
||||
}
|
||||
}
|
||||
|
||||
function initKeyShortcuts(event: KeyboardEvent) {
|
||||
// Check if zone is set
|
||||
if (!zoneEditorStore.zone) return
|
||||
|
||||
// prevent if focused on composables
|
||||
if (document.activeElement?.tagName === 'INPUT') return
|
||||
|
||||
const keyActions: any = {
|
||||
const keyActions: { [key: string]: string } = {
|
||||
m: 'move',
|
||||
p: 'pencil',
|
||||
e: 'eraser',
|
||||
@ -186,14 +153,26 @@ function initKeyShortcuts(event: KeyboardEvent) {
|
||||
}
|
||||
|
||||
if (keyActions.hasOwnProperty(event.key)) {
|
||||
handleClick(keyActions[event.key])
|
||||
const tool = keyActions[event.key]
|
||||
if ((tool === 'pencil' || tool === 'eraser') && zoneEditorStore.tool === tool) {
|
||||
cycleToolMode(tool)
|
||||
} else {
|
||||
handleClick(tool)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClickOutside(clickOutsideElement, handleClickOutside)
|
||||
|
||||
function handleClickOutside() {
|
||||
selectPencilOpen.value = false
|
||||
selectEraserOpen.value = false
|
||||
}
|
||||
onClickOutside(toolbar, handleClickOutside)
|
||||
|
||||
onMounted(() => {
|
||||
addEventListener('keydown', initKeyShortcuts)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
removeEventListener('keydown', initKeyShortcuts)
|
||||
})
|
||||
</script>
|
||||
|
@ -1,31 +1,28 @@
|
||||
<template>
|
||||
<CreateZone v-if="zoneEditorStore.isCreateZoneModalShown" />
|
||||
|
||||
<Teleport to="body">
|
||||
<Modal @modal:close="() => zoneEditorStore.toggleZoneListModal()" :is-resizable="false" :is-modal-open="true" :modal-width="300" :modal-height="360">
|
||||
<template #modalHeader>
|
||||
<h3 class="text-lg">Zones</h3>
|
||||
</template>
|
||||
<template #modalBody>
|
||||
<div class="my-4 mx-auto">
|
||||
<div class="text-center mb-4 px-2 flex gap-2.5">
|
||||
<button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="fetchZones">Refresh</button>
|
||||
<button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="() => zoneEditorStore.toggleCreateZoneModal()">New</button>
|
||||
</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="absolute left-0 top-0 w-full h-px bg-cyan-200" v-if="index === 0"></div>
|
||||
<div class="flex gap-3 items-center w-full" @click="() => loadZone(zone.id)">
|
||||
<span>{{ zone.name }}</span>
|
||||
<span class="ml-auto gap-1 flex">
|
||||
<button class="btn-bordeaux py-0.5 px-2.5 z-50" @click.stop="() => deleteZone(zone.id)">X</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
|
||||
</div>
|
||||
<Modal :is-modal-open="zoneEditorStore.isZoneListModalShown" @modal:close="() => zoneEditorStore.toggleZoneListModal()" :is-resizable="false" :modal-width="300" :modal-height="360">
|
||||
<template #modalHeader>
|
||||
<h3 class="text-lg text-white">Zones</h3>
|
||||
</template>
|
||||
<template #modalBody>
|
||||
<div class="my-4 mx-auto">
|
||||
<div class="text-center mb-4 px-2 flex gap-2.5">
|
||||
<button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="fetchZones">Refresh</button>
|
||||
<button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="() => zoneEditorStore.toggleCreateZoneModal()">New</button>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</Teleport>
|
||||
<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-gray-500" v-if="index === 0"></div>
|
||||
<div class="flex gap-3 items-center w-full" @click="() => loadZone(zone.id)">
|
||||
<span>{{ zone.name }}</span>
|
||||
<span class="ml-auto gap-1 flex">
|
||||
<button class="btn-red w-11 h-11 z-50" @click.stop="() => deleteZone(zone.id)">X</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
@ -1,34 +1,46 @@
|
||||
<template>
|
||||
<Modal :is-modal-open="zoneEditorStore.isSettingsModalShown" @modal:close="() => zoneEditorStore.toggleSettingsModal()" :modal-width="300" :modal-height="350" :is-resizable="false">
|
||||
<Modal :is-modal-open="zoneEditorStore.isSettingsModalShown" @modal:close="() => zoneEditorStore.toggleSettingsModal()" :modal-width="600" :modal-height="350">
|
||||
<template #modalHeader>
|
||||
<h3 class="m-0 font-medium shrink-0">Zone settings</h3>
|
||||
<h3 class="m-0 font-medium shrink-0 text-white">Zone settings</h3>
|
||||
</template>
|
||||
|
||||
<template #modalBody>
|
||||
<div class="m-4">
|
||||
<form method="post" @submit.prevent="" class="inline">
|
||||
<div class="gap-2.5 flex flex-wrap">
|
||||
<div class="space-x-2">
|
||||
<button class="btn-cyan py-1.5 px-4" type="button" @click.prevent="screen = 'settings'">Settings</button>
|
||||
<button class="btn-cyan py-1.5 px-4" type="button" @click.prevent="screen = 'effects'">Effects</button>
|
||||
</div>
|
||||
<form method="post" @submit.prevent="" class="inline" v-if="screen === 'settings'">
|
||||
<div class="gap-2.5 flex flex-wrap mt-4">
|
||||
<div class="form-field-full">
|
||||
<label for="name">Name</label>
|
||||
<input class="input-cyan" v-model="name" name="name" id="name" />
|
||||
<input class="input-field" v-model="name" name="name" id="name" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="name">Width</label>
|
||||
<input class="input-cyan" v-model="width" name="name" id="name" type="number" />
|
||||
<label for="width">Width</label>
|
||||
<input class="input-field" v-model="width" name="width" id="width" type="number" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="name">Height</label>
|
||||
<input class="input-cyan" v-model="height" name="name" id="name" type="number" />
|
||||
<label for="height">Height</label>
|
||||
<input class="input-field" v-model="height" name="height" id="height" type="number" />
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="pvp">PVP enabled</label>
|
||||
<select v-model="pvp" class="input-cyan" name="pvp" id="pvp">
|
||||
<select v-model="pvp" class="input-field" name="pvp" id="pvp">
|
||||
<option :value="false">No</option>
|
||||
<option :value="true">Yes</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<form method="post" @submit.prevent="" class="inline" v-if="screen === 'effects'">
|
||||
<div v-for="(effect, index) in zoneEffects" :key="effect.id" class="mb-2 flex items-center space-x-2 mt-4">
|
||||
<input class="input-field flex-grow" v-model="effect.effect" placeholder="Effect name" />
|
||||
<input class="input-field w-20" v-model.number="effect.strength" type="number" placeholder="Strength" />
|
||||
<button class="btn-red py-1 px-2" type="button" @click="removeEffect(index)">Delete</button>
|
||||
</div>
|
||||
<button class="btn-green py-1 px-2 mt-2" type="button" @click="addEffect">Add Effect</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
@ -40,16 +52,19 @@ import Modal from '@/components/utilities/Modal.vue'
|
||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
|
||||
const zoneEditorStore = useZoneEditorStore()
|
||||
const screen = ref('settings')
|
||||
|
||||
zoneEditorStore.setZoneName(zoneEditorStore.zone.name)
|
||||
zoneEditorStore.setZoneWidth(zoneEditorStore.zone.width)
|
||||
zoneEditorStore.setZoneHeight(zoneEditorStore.zone.height)
|
||||
zoneEditorStore.setZonePvp(zoneEditorStore.zone.pvp)
|
||||
zoneEditorStore.setZoneName(zoneEditorStore.zone?.name)
|
||||
zoneEditorStore.setZoneWidth(zoneEditorStore.zone?.width)
|
||||
zoneEditorStore.setZoneHeight(zoneEditorStore.zone?.height)
|
||||
zoneEditorStore.setZonePvp(zoneEditorStore.zone?.pvp)
|
||||
zoneEditorStore.setZoneEffects(zoneEditorStore.zone?.zoneEffects)
|
||||
|
||||
const name = ref(zoneEditorStore.zoneSettings.name)
|
||||
const width = ref(zoneEditorStore.zoneSettings.width)
|
||||
const height = ref(zoneEditorStore.zoneSettings.height)
|
||||
const pvp = ref(zoneEditorStore.zoneSettings.pvp)
|
||||
const name = ref(zoneEditorStore.zoneSettings?.name)
|
||||
const width = ref(zoneEditorStore.zoneSettings?.width)
|
||||
const height = ref(zoneEditorStore.zoneSettings?.height)
|
||||
const pvp = ref(zoneEditorStore.zoneSettings?.pvp)
|
||||
const zoneEffects = ref(zoneEditorStore.zoneSettings?.zoneEffects || [])
|
||||
|
||||
watch(name, (value) => {
|
||||
zoneEditorStore.setZoneName(value)
|
||||
@ -66,4 +81,26 @@ watch(height, (value) => {
|
||||
watch(pvp, (value) => {
|
||||
zoneEditorStore.setZonePvp(value)
|
||||
})
|
||||
|
||||
watch(
|
||||
zoneEffects,
|
||||
(value) => {
|
||||
zoneEditorStore.setZoneEffects(value)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const addEffect = () => {
|
||||
zoneEffects.value.push({
|
||||
id: Date.now().toString(), // Simple unique id generation
|
||||
zoneId: zoneEditorStore.zone?.id,
|
||||
zone: zoneEditorStore.zone,
|
||||
effect: '',
|
||||
strength: 1
|
||||
})
|
||||
}
|
||||
|
||||
const removeEffect = (index) => {
|
||||
zoneEffects.value.splice(index, 1)
|
||||
}
|
||||
</script>
|
||||
|
@ -1,14 +1,15 @@
|
||||
<template>
|
||||
<div class="w-full md:min-w-[350px] max-w-[750px] flex flex-col">
|
||||
<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.isChatOpen">
|
||||
<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 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">
|
||||
<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>
|
||||
</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
|
||||
class="w-full h-12 rounded-lg text-lg px-4 py-0 bg-gray-300/80 border-2 border-solid border-cyan-200 text-gray-50 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..."
|
||||
v-model="message"
|
||||
@keypress="handleKeyPress"
|
||||
|
7
src/components/gui/ExpBar.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template></template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
</script>
|
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,46 +1,16 @@
|
||||
<template>
|
||||
<div class="hud-wrapper relative left-0 w-[310px] h-[84px]">
|
||||
<div class="absolute w-14 h-14 bg-white/80 rounded-full border-3 border-solid border-white top-1/2 -translate-y-1/2 left-0 z-20">
|
||||
<img class="w-7 absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" draggable="false" src="/assets/avatar/default/head.png" />
|
||||
<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="hud-bg absolute top-0 left-8 w-[280px] h-[84px] z-10 bg-[url('/assets/bg-hud-2.png')] bg-top bg-[length:cover] bg-no-repeat mask-[url('/assets/shapes/hud-image-shape.svg')] mask-center mask-[length:cover] mask-no-repeat"></div>
|
||||
<div class="absolute top-0 left-8 w-[280px] h-[84px] z-10 bg-[url('/assets/shapes/hud-shape-empty.svg')] bg-center bg-[length:cover] bg-no-repeat">
|
||||
<div class="h-16 flex flex-col items-end py-2.5 pl-12 pr-5">
|
||||
<div class="w-full flex items-center justify-between mb-1.5">
|
||||
<span class="text-ellipsis overflow-hidden whitespace-nowrap max-w-32 text-sm">{{ gameStore.character.name }}</span>
|
||||
<span class="text-sm">lvl. {{ gameStore.character.level }}</span>
|
||||
</div>
|
||||
<div class="w-full flex items-center justify-between">
|
||||
<label class="text-sm" for="hp">HP</label>
|
||||
<progress class="h-2 rounded-lg w-full max-w-44 appearance-none accent-red" id="hp" :value="gameStore.character.hitpoints" max="100">{{ gameStore.character.hitpoints }}%</progress>
|
||||
</div>
|
||||
<div class="w-full flex items-center justify-between">
|
||||
<label class="text-sm" for="mp">MP</label>
|
||||
<progress class="h-2 rounded-lg w-full max-w-44 appearance-none accent-blue" id="mp" :value="gameStore.character.mana" max="100">{{ gameStore.character.mana }}%</progress>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- TODO: Replace gameStore.character with other (selected) player's -->
|
||||
<!-- <div class="hud-wrapper other-player relative right-0 w-[310px] h-[84px]">-->
|
||||
<!-- <div class="absolute w-14 h-14 bg-white/80 rounded-full border-3 border-solid border-white top-1/2 -translate-y-1/2 right-0 z-20">-->
|
||||
<!-- <img class="w-7 absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 -scale-x-100" draggable="false" src="/assets/avatar/default/head.png" />-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="hud-bg absolute top-0 right-8 w-[280px] h-[84px] z-10 bg-[url('/assets/bg-hud-2.png')] bg-center bg-[length:cover] bg-no-repeat mask-[url('/assets/shapes/hud-image-shape.svg')] mask-center mask-[length:cover] mask-no-repeat"></div>-->
|
||||
<!-- <div class="absolute top-0 right-8 w-[280px] h-[84px] z-10 -scale-x-100 bg-[url('/assets/shapes/hud-shape-empty.svg')] bg-center bg-[length:cover] bg-no-repeat">-->
|
||||
<!-- <div class="h-16 flex flex-col items-end -scale-x-100 py-2.5 pr-12 pl-5">-->
|
||||
<!-- <div class="w-full flex items-center justify-between mb-1.5">-->
|
||||
<!-- <span class="text-ellipsis overflow-hidden whitespace-nowrap max-w-32 text-sm">{{ gameStore.character.name }}</span>-->
|
||||
<!-- <span class="text-sm">lvl. {{ gameStore.character.level }}</span>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="w-full flex items-center justify-between">-->
|
||||
<!-- <label class="text-sm" for="hp">HP</label>-->
|
||||
<!-- <progress class="h-2 rounded-lg w-full max-w-44 appearance-none accent-red" id="hp" :value="gameStore.character.hitpoints" max="100">{{ gameStore.character.hitpoints }}%</progress>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@ -50,41 +20,30 @@ const gameStore = useGameStore()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.hud-wrapper {
|
||||
.hud-bg {
|
||||
mask: url('/assets/shapes/hud-image-shape.svg') center/cover no-repeat;
|
||||
#hp {
|
||||
// Chrome, Safari, Edge, Opera
|
||||
&::-webkit-progress-value {
|
||||
@apply bg-gradient-to-r from-green from-75% to-green-200 rounded-sm;
|
||||
}
|
||||
#hp {
|
||||
// Chrome, Safari, Edge, Opera
|
||||
&::-webkit-progress-value {
|
||||
@apply bg-red rounded-lg;
|
||||
}
|
||||
&::-webkit-progress-bar {
|
||||
@apply bg-white rounded-lg border-2 border-solid border-white;
|
||||
}
|
||||
// Firefox
|
||||
&::-moz-progress-bar {
|
||||
@apply bg-red rounded-lg border-2 border-solid border-white;
|
||||
}
|
||||
&::-webkit-progress-bar {
|
||||
@apply bg-white rounded-sm border border-solid border-black;
|
||||
}
|
||||
#mp {
|
||||
// Chrome, Safari, Edge, Opera
|
||||
&::-webkit-progress-value {
|
||||
@apply bg-blue rounded-lg;
|
||||
}
|
||||
&::-webkit-progress-bar {
|
||||
@apply bg-white rounded-lg border-2 border-solid border-white;
|
||||
}
|
||||
// Firefox
|
||||
&::-moz-progress-bar {
|
||||
@apply bg-blue rounded-lg border-2 border-solid border-white;
|
||||
}
|
||||
// Firefox
|
||||
&::-moz-progress-bar {
|
||||
@apply bg-gradient-to-r from-green from-75% to-green-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;
|
||||
}
|
||||
}
|
||||
#sp {
|
||||
// Chrome, Safari, Edge, Opera
|
||||
&::-webkit-progress-value {
|
||||
@apply bg-gradient-to-r from-blue from-75% to-blue-200 rounded-sm;
|
||||
}
|
||||
&::-webkit-progress-bar {
|
||||
@apply bg-white rounded-sm border border-solid border-black;
|
||||
}
|
||||
// Firefox
|
||||
&::-moz-progress-bar {
|
||||
@apply bg-gradient-to-r from-blue from-75% to-blue-200 rounded-sm border border-solid border-black;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,39 +1,49 @@
|
||||
<template>
|
||||
<ul class="list-none flex gap-2.5 items-center m-0 max-md:pl-0">
|
||||
<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>
|
||||
<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">
|
||||
<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">World</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 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 menu</p>
|
||||
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
||||
</div>
|
||||
<a class="group-hover: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/world.png" />
|
||||
</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 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-6 h-5 mx-[9px] my-[11px] object-contain" draggable="false" src="/assets/icons/menu-icon.svg" />
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item group relative" @click="gameStore.toggleUserPanel">
|
||||
<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">Inventory</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 top-1/2 left-14 -translate-y-1/2 w-20 h-6 text-center bg-gray-800 border-2 border-solid border-gray-500 rounded-3xl hidden">
|
||||
<p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">User Profile</p>
|
||||
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
||||
</div>
|
||||
<a class="group-hover: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/treasure-chest.png" />
|
||||
<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-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>
|
||||
</li>
|
||||
</ul>
|
||||
@ -43,4 +53,5 @@
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
let characterLevel = gameStore.character?.level.toString().padStart(2, '0')
|
||||
</script>
|
||||
|
23
src/components/gui/Minimap.vue
Normal file
@ -0,0 +1,23 @@
|
||||
<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>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
</script>
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="absolute z-50 w-full h-dvh top-0 left-0 bg-black/60" v-show="gameStore.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="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="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-700 border-solid border-2 border-gray-500 rounded-md z-50 flex flex-col backdrop-blur-sm shadow-lg">
|
||||
<div class="p-2.5 flex max-sm:flex-wrap justify-between items-center gap-5 border-solid border-0 border-b border-gray-500">
|
||||
<h3 class="m-0 font-medium shrink-0">Game menu</h3>
|
||||
<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>
|
||||
@ -10,7 +10,7 @@
|
||||
<button @click.stop="userPanelScreen = 'settings'" :class="{ active: userPanelScreen === 'settings' }" class="btn-cyan py-1.5 px-4 min-w-24">Settings</button>
|
||||
</div>
|
||||
<div class="flex gap-2.5">
|
||||
<button class="w-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" />
|
||||
</button>
|
||||
</div>
|
||||
|
@ -39,7 +39,7 @@
|
||||
</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 class="m-4">
|
||||
<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 gap-3 justify-center">
|
||||
<!-- 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" />
|
||||
</div>
|
||||
|
||||
<!-- 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" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 justify-center">
|
||||
<!-- 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" />
|
||||
</div>
|
||||
|
||||
<!-- 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" />
|
||||
</div>
|
||||
|
||||
<!-- 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" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 justify-center">
|
||||
<!-- 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" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<!-- 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" />
|
||||
</div>
|
||||
|
||||
<!-- 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" />
|
||||
</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 class="m-4">
|
||||
<h4 class="font-medium text-lg max-w-[375px]">Equipment Bonus</h4>
|
||||
|
@ -3,14 +3,14 @@
|
||||
<div class="m-4 relative">
|
||||
<h4 class="m-auto font-medium text-lg max-w-[375px]">Inventory</h4>
|
||||
<div class="flex gap-3 mt-4 mx-auto flex-wrap max-w-[375px]">
|
||||
<div v-for="n in 24" class="bg-gray-300/80 border-solid border-2 border-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 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 class="m-4">
|
||||
<h4 class="m-auto font-medium text-lg max-w-[375px]">Chest items</h4>
|
||||
<div class="flex gap-3 mt-4 mx-auto flex-wrap max-w-[375px]">
|
||||
<div v-for="n in 12" class="bg-gray-300/80 border-solid border-2 border-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>
|
||||
|
@ -4,26 +4,26 @@
|
||||
<!-- Settings Categories -->
|
||||
<div class="relative p-2.5">
|
||||
<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>
|
||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'character' }" @click.stop="settingCategory = 'character'">
|
||||
<span>Character</span>
|
||||
<div class="absolute left-0 bottom-0 w-full h-px bg-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': settingCategory === 'account' }" @click.stop="settingCategory = 'account'">
|
||||
<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 class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'audio' }" @click.stop="settingCategory = 'audio'">
|
||||
<span>Audio</span>
|
||||
<div class="absolute left-0 bottom-0 w-full h-px bg-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': settingCategory === 'video' }" @click.stop="settingCategory = 'video'">
|
||||
<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>
|
||||
</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 -->
|
||||
<div class="overflow-auto h-full w-10/12 flex flex-col relative">
|
||||
|
@ -6,11 +6,11 @@
|
||||
<form class="flex gap-2.5 flex-wrap">
|
||||
<div class="form-field-half max-w-[300px]">
|
||||
<label for="name">Name</label>
|
||||
<input class="input-cyan" :class="{ inactive: !editCharacter }" type="text" name="name" placeholder="Ethereal" :disabled="!editCharacter" />
|
||||
<input class="input-field" :class="{ inactive: !editCharacter }" type="text" name="name" placeholder="Ethereal" :disabled="!editCharacter" />
|
||||
</div>
|
||||
<div class="form-field-half max-w-[300px] relative">
|
||||
<label for="class">Class</label>
|
||||
<select class="input-cyan" v-model="characterClass" :class="{ inactive: !editCharacter }" name="class" :disabled="!editCharacter">
|
||||
<select class="input-field" v-model="characterClass" :class="{ inactive: !editCharacter }" name="class" :disabled="!editCharacter">
|
||||
<option value="Knight" :selected="characterClass == 'Knight'" :disabled="characterClass == 'Knight'">Knight</option>
|
||||
<option value="Paladin" :selected="characterClass == 'Paladin'" :disabled="characterClass == 'Paladin'">Paladin</option>
|
||||
</select>
|
||||
|
@ -1,14 +1,17 @@
|
||||
<template>
|
||||
<!-- Chat bubble -->
|
||||
<Container ref="charChatContainer" :depth="999" :x="currentX" :y="currentY">
|
||||
<RoundRectangle @create="createChatBubble" :origin-x="0.5" :origin-y="7.5" :fillColor="0xffffff" :width="194" :height="21" :radius="5" />
|
||||
<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' }" />
|
||||
</Container>
|
||||
<!-- Character name and health -->
|
||||
<Container :depth="999" :x="currentX" :y="currentY">
|
||||
<Text @create="createText" :text="character.name" :origin-x="0.5" :origin-y="9" />
|
||||
<RoundRectangle :origin-x="0.5" :origin-y="18.5" :fillColor="0xffffff" :width="74" :height="6" :radius="5" />
|
||||
<RoundRectangle :origin-x="0.5" :origin-y="36.4" :fillColor="0x00b3b3" :width="70" :height="3" :radius="5" />
|
||||
</Container>
|
||||
<Container :depth="isometricDepth" :x="currentX" :y="currentY" ref="charContainer">
|
||||
<!-- Character sprite -->
|
||||
<Container ref="charContainer" :depth="isometricDepth" :x="currentX" :y="currentY">
|
||||
<Sprite ref="charSprite" :origin-y="1" :flipX="isFlippedX" :flipY="false" />
|
||||
</Container>
|
||||
</template>
|
||||
@ -19,7 +22,7 @@ import { type ExtendedCharacter } from '@/types'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useZoneStore } from '@/stores/zoneStore'
|
||||
import { watch, computed, ref, onMounted, onUnmounted } from 'vue'
|
||||
import { Container, refObj, RoundRectangle, Sprite, Text, useGame } from 'phavuer'
|
||||
import { Container, refObj, RoundRectangle, Sprite, Text, useGame, useScene } from 'phavuer'
|
||||
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||
|
||||
enum Direction {
|
||||
@ -40,6 +43,7 @@ const charSprite = refObj<Phaser.GameObjects.Sprite>()
|
||||
const game = useGame()
|
||||
const gameStore = useGameStore()
|
||||
const zoneStore = useZoneStore()
|
||||
const scene = useScene()
|
||||
|
||||
const currentX = ref(0)
|
||||
const currentY = ref(0)
|
||||
@ -134,12 +138,11 @@ const createChatBubble = (container: Phaser.GameObjects.Container) => {
|
||||
const createChatText = (text: Phaser.GameObjects.Text) => {
|
||||
text.setName(`${props.character.name}_chatText`)
|
||||
text.setFontSize(13)
|
||||
text.setFontStyle('bold')
|
||||
text.setFontFamily('Arial')
|
||||
|
||||
// Fix text alignment on Windows
|
||||
if (game.device.os.windows) {
|
||||
text.setOrigin(0.5, 9.8)
|
||||
// Fix text alignment on Windows and Android
|
||||
if (game.device.os.windows || game.device.os.android) {
|
||||
text.setOrigin(0.5, 9.75)
|
||||
}
|
||||
}
|
||||
|
||||
@ -147,8 +150,8 @@ const createText = (text: Phaser.GameObjects.Text) => {
|
||||
text.setFontSize(13)
|
||||
text.setFontFamily('Arial')
|
||||
|
||||
// Fix text alignment on Windows
|
||||
if (game.device.os.windows) {
|
||||
// Fix text alignment on Windows and Android
|
||||
if (game.device.os.windows || game.device.os.android) {
|
||||
text.setOrigin(0.5, 8)
|
||||
}
|
||||
}
|
||||
@ -175,6 +178,10 @@ onMounted(() => {
|
||||
|
||||
if (props.character.id === gameStore.character!.id) {
|
||||
zoneStore.setCharacterLoaded(true)
|
||||
|
||||
// #146 : Set camera position to character, need to be improved still
|
||||
scene.cameras.main.startFollow(charContainer.value as Phaser.GameObjects.Container)
|
||||
scene.cameras.main.stopFollow()
|
||||
}
|
||||
|
||||
// Set sprite
|
||||
|
@ -38,24 +38,20 @@ const modalOpened = ref(props.modalOpened)
|
||||
</script>
|
||||
|
||||
<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>
|
||||
<div class="text-white">
|
||||
<slot name="modalHeader"></slot>
|
||||
</div>
|
||||
<slot name="modalHeader"></slot>
|
||||
</template>
|
||||
<template #modalBody>
|
||||
<div class="text-white h-full">
|
||||
<div class="flex h-full flex-col justify-between">
|
||||
<span class="p-2">
|
||||
<slot name="modalBody"></slot>
|
||||
</span>
|
||||
<div class="flex justify-between p-2">
|
||||
<button class="btn-cyan py-1.5 px-4 min-w-24 inline-block" @click="props.cancelFunction()">
|
||||
<div class="h-[calc(100%_-_32px)] p-4">
|
||||
<div class="h-full flex flex-col justify-between">
|
||||
<slot name="modalBody"></slot>
|
||||
<div class="grid grid-flow-col justify-stretch gap-4">
|
||||
<button class="btn-empty py-1.5 px-4 min-w-24 inline-block" @click="props.cancelFunction()">
|
||||
{{ props.cancelButtonText }}
|
||||
</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 }}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -8,21 +8,13 @@ import { onBeforeUnmount, ref } from 'vue'
|
||||
import { usePointerHandlers } from '@/composables/usePointerHandlers'
|
||||
import { useCameraControls } from '@/composables/useCameraControls'
|
||||
|
||||
interface Props {
|
||||
layer: Phaser.Tilemaps.TilemapLayer
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const props = defineProps<{ layer: Phaser.Tilemaps.TilemapLayer }>()
|
||||
const scene = useScene()
|
||||
|
||||
const waypoint = ref({
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0
|
||||
})
|
||||
const waypoint = ref({ visible: false, x: 0, y: 0 })
|
||||
|
||||
const { camera, isDragging } = useCameraControls(scene)
|
||||
const { setupPointerHandlers, cleanupPointerHandlers } = usePointerHandlers(scene, props.layer, waypoint, camera, isDragging)
|
||||
const { camera } = useCameraControls(scene)
|
||||
const { setupPointerHandlers, cleanupPointerHandlers } = usePointerHandlers(scene, props.layer, waypoint, camera)
|
||||
|
||||
setupPointerHandlers()
|
||||
onBeforeUnmount(cleanupPointerHandlers)
|
||||
|
@ -1,23 +1,26 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-if="isModalOpenRef" class="fixed bg-gray-300/80 border-solid border-2 border-cyan-200 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-cyan-200">
|
||||
<slot name="modalHeader" />
|
||||
<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 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" />
|
||||
</div>
|
||||
<div class="flex gap-2.5">
|
||||
<button @click="toggleFullScreen" class="w-5 h-5 m-0 p-0 relative hover:scale-110 transition-transform duration-300 ease-in-out" v-if="canFullScreen">
|
||||
<img :alt="isFullScreen ? 'exit full-screen' : 'full-screen'" draggable="false" :src="isFullScreen ? '/assets/icons/minimize.svg' : '/assets/icons/full-screen.svg'" class="w-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 @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" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-hidden grow">
|
||||
<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 v-if="$slots.modalFooter" class="px-5 min-h-12 flex justify-end gap-7.5 items-center border-solid border-t border-cyan-200">
|
||||
<slot name="modalFooter" />
|
||||
<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" />
|
||||
</div>
|
||||
<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" />
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
@ -84,7 +87,7 @@ let startHeight = 0
|
||||
let preFullScreenState = { x: 0, y: 0, width: 0, height: 0 }
|
||||
|
||||
const modalStyle = computed(() => ({
|
||||
borderRadius: isFullScreen.value ? '0' : '10px',
|
||||
borderRadius: isFullScreen.value ? '0' : '6px',
|
||||
top: isFullScreen.value ? '0' : `${y.value}px`,
|
||||
left: isFullScreen.value ? '0' : `${x.value}px`,
|
||||
width: isFullScreen.value ? '100vw' : `${width.value}px`,
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<Modal v-for="notification in gameStore.getNotifications" :key="notification.id" :isModalOpen="true" @modal:close="closeNotification(notification.id)">
|
||||
<template #modalHeader v-if="notification.title">
|
||||
<h3 class="m-0 font-medium shrink-0">{{ notification.title }}</h3>
|
||||
<h3 class="m-0 font-medium shrink-0 text-white">{{ notification.title }}</h3>
|
||||
</template>
|
||||
<template #modalBody v-if="notification.message">
|
||||
<p class="m-4">{{ notification.message }}</p>
|
||||
|
@ -1,6 +1,5 @@
|
||||
<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)" />
|
||||
<!-- <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)" />-->
|
||||
<Image v-for="object in zoneStore.zone?.zoneObjects" v-bind="getImageProps(object)" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@ -15,19 +14,12 @@ 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) => {
|
||||
const getImageProps = (object: ZoneObject) => {
|
||||
return {
|
||||
depth: calculateIsometricDepth(object.positionX, object.positionY, object.object.frameWidth, object.object.frameHeight),
|
||||
x: tileToWorldX(props.tilemap as any, object.positionX, object.positionY),
|
||||
y: tileToWorldY(props.tilemap as any, object.positionX, object.positionY),
|
||||
flipX: object.isRotated,
|
||||
texture: object.object.id,
|
||||
originY: Number(object.object.originX),
|
||||
originX: Number(object.object.originY)
|
||||
|
@ -17,7 +17,6 @@ const scene = useScene()
|
||||
|
||||
const zoneTilemap = createTilemap()
|
||||
const tiles = createTileLayer()
|
||||
let tileArray = createTileArray()
|
||||
|
||||
function createTilemap() {
|
||||
const zoneData = new Phaser.Tilemaps.MapData({
|
||||
@ -38,11 +37,11 @@ function createTileLayer() {
|
||||
const uniqueTiles = new Set(tilesFromZone.flat().filter(Boolean))
|
||||
|
||||
const tilesetImages = Array.from(uniqueTiles).map((tile, index) => {
|
||||
return zoneTilemap.addTilesetImage(tile, tile, config.tile_size.x, config.tile_size.y, 0, 0, index + 1, { x: 0, y: -config.tile_size.y })
|
||||
return zoneTilemap.addTilesetImage(tile, tile, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y })
|
||||
}) as any
|
||||
|
||||
// Add blank tile
|
||||
tilesetImages.push(zoneTilemap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 0, 0, 0, { 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, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
|
||||
|
||||
layer.setDepth(0)
|
||||
@ -51,17 +50,11 @@ function createTileLayer() {
|
||||
return layer
|
||||
}
|
||||
|
||||
function createTileArray() {
|
||||
return Array.from({ length: zoneStore.zone?.width ?? 0 }, () => Array.from({ length: zoneStore.zone?.height ?? 0 }, () => 'blank_tile'))
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
if (zoneStore.zone?.tiles) {
|
||||
setAllTiles(zoneTilemap, tiles, zoneStore.zone.tiles)
|
||||
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')))
|
||||
if (!zoneStore.zone?.tiles) {
|
||||
return
|
||||
}
|
||||
setAllTiles(zoneTilemap, tiles, zoneStore.zone.tiles)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
|
@ -8,7 +8,7 @@
|
||||
import { useScene } from 'phavuer'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useZoneStore } from '@/stores/zoneStore'
|
||||
import { onBeforeMount, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { onBeforeMount, onBeforeUnmount, ref } from 'vue'
|
||||
import type { Character as CharacterT, Zone as ZoneT, ExtendedCharacter as ExtendedCharacterT } from '@/types'
|
||||
import Tiles from '@/components/zone/Tiles.vue'
|
||||
import Objects from '@/components/zone/Objects.vue'
|
||||
|
@ -1,75 +1,83 @@
|
||||
import { ref, type Ref } from 'vue'
|
||||
import { type Ref, ref } from 'vue'
|
||||
import { getTile, tileToWorldXY } from '@/composables/zoneComposable'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import config from '@/config'
|
||||
|
||||
export function useGamePointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
|
||||
const gameStore = useGameStore()
|
||||
const lastDragTime = ref(0)
|
||||
const dragTimeout = 500 // 500ms timeout for dragging, to prevent accidental clicks
|
||||
const pointerStartPosition = ref({ x: 0, y: 0 })
|
||||
const dragThreshold = 5 // pixels
|
||||
|
||||
function updateWaypoint(pointer: Phaser.Input.Pointer) {
|
||||
const { x: px, y: py } = camera.getWorldPoint(pointer.x, pointer.y)
|
||||
const pointerTile = getTile(px, py, layer)
|
||||
|
||||
waypoint.value.visible = !!pointerTile
|
||||
if (!pointerTile) return
|
||||
|
||||
const worldPoint = tileToWorldXY(layer, pointerTile.x, pointerTile.y)
|
||||
waypoint.value.x = worldPoint.positionX
|
||||
waypoint.value.y = worldPoint.positionY + config.tile_size.y + 15
|
||||
function updateWaypoint(worldX: number, worldY: number) {
|
||||
const pointerTile = getTile(layer, worldX, worldY)
|
||||
if (pointerTile) {
|
||||
const worldPoint = tileToWorldXY(layer, pointerTile.x, pointerTile.y)
|
||||
waypoint.value = {
|
||||
visible: true,
|
||||
x: worldPoint.positionX,
|
||||
y: worldPoint.positionY + config.tile_size.y + 15
|
||||
}
|
||||
} else {
|
||||
waypoint.value.visible = false
|
||||
}
|
||||
}
|
||||
|
||||
function dragZone(pointer: Phaser.Input.Pointer) {
|
||||
if (!gameStore.isPlayerDraggingCamera) return
|
||||
|
||||
const { x, y, prevPosition } = pointer
|
||||
const { scrollX, scrollY, zoom } = camera
|
||||
camera.setScroll(scrollX - (x - prevPosition.x) / zoom, scrollY - (y - prevPosition.y) / zoom)
|
||||
|
||||
lastDragTime.value = Date.now()
|
||||
function handlePointerDown(pointer: Phaser.Input.Pointer) {
|
||||
pointerStartPosition.value = { x: pointer.x, y: pointer.y }
|
||||
gameStore.setPlayerDraggingCamera(true)
|
||||
}
|
||||
|
||||
function handlePointerMove(pointer: Phaser.Input.Pointer) {
|
||||
dragZone(pointer)
|
||||
updateWaypoint(pointer)
|
||||
const { worldX, worldY } = pointer
|
||||
updateWaypoint(worldX, worldY)
|
||||
|
||||
if (gameStore.isPlayerDraggingCamera) {
|
||||
const distance = Phaser.Math.Distance.Between(pointerStartPosition.value.x, pointerStartPosition.value.y, pointer.x, pointer.y)
|
||||
|
||||
if (distance > dragThreshold) {
|
||||
const { x, y, prevPosition } = pointer
|
||||
const { scrollX, scrollY, zoom } = camera
|
||||
camera.setScroll(scrollX - (x - prevPosition.x) / zoom, scrollY - (y - prevPosition.y) / zoom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clickTile(pointer: Phaser.Input.Pointer) {
|
||||
const currentTime = Date.now()
|
||||
if (currentTime - lastDragTime.value < dragTimeout) {
|
||||
return
|
||||
function handlePointerUp(pointer: Phaser.Input.Pointer) {
|
||||
const distance = Phaser.Math.Distance.Between(pointerStartPosition.value.x, pointerStartPosition.value.y, pointer.x, pointer.y)
|
||||
|
||||
if (distance <= dragThreshold) {
|
||||
const pointerTile = getTile(layer, pointer.worldX, pointer.worldY)
|
||||
if (pointerTile) {
|
||||
gameStore.connection?.emit('character:initMove', {
|
||||
positionX: pointerTile.x,
|
||||
positionY: pointerTile.y
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const { x: px, y: py } = camera.getWorldPoint(pointer.x, pointer.y)
|
||||
const pointerTile = getTile(px, py, layer)
|
||||
|
||||
if (!pointerTile) return
|
||||
|
||||
gameStore.connection?.emit('character:initMove', {
|
||||
positionX: pointerTile.x,
|
||||
positionY: pointerTile.y
|
||||
})
|
||||
gameStore.setPlayerDraggingCamera(false)
|
||||
}
|
||||
|
||||
function handleZoom(pointer: Phaser.Input.Pointer) {
|
||||
if (!(pointer.event instanceof WheelEvent) || !pointer.event.shiftKey) return
|
||||
|
||||
const deltaY = pointer.event.deltaY
|
||||
let zoomLevel = camera.zoom - deltaY * 0.005
|
||||
if(zoomLevel <= 0 || zoomLevel >= 3) return
|
||||
camera.setZoom(zoomLevel)
|
||||
if (pointer.event instanceof WheelEvent && pointer.event.shiftKey) {
|
||||
const deltaY = pointer.event.deltaY
|
||||
let zoomLevel = camera.zoom - deltaY * 0.005
|
||||
zoomLevel = Phaser.Math.Clamp(zoomLevel, 1, 3)
|
||||
camera.setZoom(zoomLevel)
|
||||
}
|
||||
}
|
||||
|
||||
const setupPointerHandlers = () => {
|
||||
scene.input.on(Phaser.Input.Events.POINTER_UP, clickTile)
|
||||
scene.input.on(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
|
||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
|
||||
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
||||
scene.input.on(Phaser.Input.Events.POINTER_WHEEL, handleZoom)
|
||||
}
|
||||
|
||||
const cleanupPointerHandlers = () => {
|
||||
scene.input.off(Phaser.Input.Events.POINTER_UP, clickTile)
|
||||
scene.input.off(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
|
||||
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
|
||||
scene.input.off(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
||||
scene.input.off(Phaser.Input.Events.POINTER_WHEEL, handleZoom)
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { computed, type Ref } from 'vue'
|
||||
import { getTile, tileToWorldXY } from '@/composables/zoneComposable'
|
||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
import config from '@/config'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import config from '@/config'
|
||||
|
||||
export function useZoneEditorPointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
|
||||
const gameStore = useGameStore()
|
||||
@ -11,22 +11,26 @@ export function useZoneEditorPointerHandlers(scene: Phaser.Scene, layer: Phaser.
|
||||
|
||||
function updateWaypoint(pointer: Phaser.Input.Pointer) {
|
||||
const { x: px, y: py } = camera.getWorldPoint(pointer.x, pointer.y)
|
||||
const pointerTile = getTile(px, py, layer)
|
||||
const pointerTile = getTile(layer, px, py)
|
||||
|
||||
waypoint.value.visible = !!pointerTile
|
||||
if (!pointerTile) return
|
||||
|
||||
const worldPoint = tileToWorldXY(layer, pointerTile.x, pointerTile.y)
|
||||
waypoint.value.x = worldPoint.positionX
|
||||
waypoint.value.y = worldPoint.positionY + config.tile_size.y + 15
|
||||
if (pointerTile) {
|
||||
const worldPoint = tileToWorldXY(layer, pointerTile.x, pointerTile.y)
|
||||
waypoint.value = {
|
||||
visible: true,
|
||||
x: worldPoint.positionX,
|
||||
y: worldPoint.positionY + config.tile_size.y + 15
|
||||
}
|
||||
} else {
|
||||
waypoint.value.visible = false
|
||||
}
|
||||
}
|
||||
|
||||
function dragZone(pointer: Phaser.Input.Pointer) {
|
||||
if (!gameStore.isPlayerDraggingCamera) return
|
||||
|
||||
const { x, y, prevPosition } = pointer
|
||||
const { scrollX, scrollY, zoom } = camera
|
||||
camera.setScroll(scrollX - (x - prevPosition.x) / zoom, scrollY - (y - prevPosition.y) / zoom)
|
||||
if (gameStore.isPlayerDraggingCamera) {
|
||||
const { x, y, prevPosition } = pointer
|
||||
const { scrollX, scrollY, zoom } = camera
|
||||
camera.setScroll(scrollX - (x - prevPosition.x) / zoom, scrollY - (y - prevPosition.y) / zoom)
|
||||
}
|
||||
}
|
||||
|
||||
function handlePointerMove(pointer: Phaser.Input.Pointer) {
|
||||
@ -37,12 +41,13 @@ export function useZoneEditorPointerHandlers(scene: Phaser.Scene, layer: Phaser.
|
||||
}
|
||||
|
||||
function handleZoom(pointer: Phaser.Input.Pointer) {
|
||||
if (!(pointer.event instanceof WheelEvent) || !pointer.event.shiftKey) return
|
||||
|
||||
const deltaY = pointer.event.deltaY
|
||||
let zoomLevel = camera.zoom - deltaY * 0.005
|
||||
if(zoomLevel <= 0 || zoomLevel >= 3) return
|
||||
camera.setZoom(zoomLevel)
|
||||
if (pointer.event instanceof WheelEvent && pointer.event.shiftKey) {
|
||||
const deltaY = pointer.event.deltaY
|
||||
let zoomLevel = camera.zoom - deltaY * 0.005
|
||||
if (zoomLevel > 0 && zoomLevel < 3) {
|
||||
camera.setZoom(zoomLevel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const setupPointerHandlers = () => {
|
||||
|
@ -1,32 +1,15 @@
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { watch } from 'vue'
|
||||
import { useZoneStore } from '@/stores/zoneStore'
|
||||
|
||||
export function useCameraControls(scene: Phaser.Scene): any {
|
||||
export function useCameraControls(scene: Phaser.Scene) {
|
||||
const gameStore = useGameStore()
|
||||
const zoneStore = useZoneStore()
|
||||
const camera = scene.cameras.main
|
||||
|
||||
function onPointerDown(pointer: Phaser.Input.Pointer) {
|
||||
gameStore.setPlayerDraggingCamera(true)
|
||||
}
|
||||
|
||||
function onPointerUp() {
|
||||
gameStore.setPlayerDraggingCamera(false)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => zoneStore.characterLoaded,
|
||||
(characterLoaded) => {
|
||||
if (!characterLoaded) return
|
||||
// scene.cameras.main.startFollow(scene.children.getByName(gameStore.character!.name) as Phaser.GameObjects.Container);
|
||||
}
|
||||
)
|
||||
const onPointerDown = () => gameStore.setPlayerDraggingCamera(true)
|
||||
const onPointerUp = () => gameStore.setPlayerDraggingCamera(false)
|
||||
|
||||
scene.input.on(Phaser.Input.Events.POINTER_DOWN, onPointerDown)
|
||||
scene.input.on(Phaser.Input.Events.POINTER_UP, onPointerUp)
|
||||
|
||||
return {
|
||||
camera
|
||||
}
|
||||
return { camera }
|
||||
}
|
||||
|
@ -1,32 +1,23 @@
|
||||
import { computed, type Ref, watch } from 'vue'
|
||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
import { useGamePointerHandlers } from '@/composables/pointerHandlers/useGamePointerHandlers'
|
||||
import { useZoneEditorPointerHandlers } from '@/composables/pointerHandlers/useZoneEditorPointerHandlers'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useGamePointerHandlers } from './pointerHandlers/useGamePointerHandlers'
|
||||
import { useZoneEditorPointerHandlers } from './pointerHandlers/useZoneEditorPointerHandlers'
|
||||
|
||||
export function usePointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
|
||||
const zoneEditorStore = useZoneEditorStore()
|
||||
|
||||
const gameHandlers = useGamePointerHandlers(scene, layer, waypoint, camera)
|
||||
const zoneEditorHandlers = useZoneEditorPointerHandlers(scene, layer, waypoint, camera)
|
||||
|
||||
const currentHandlers = computed(() => (zoneEditorStore.active ? zoneEditorHandlers : gameHandlers))
|
||||
|
||||
const setupPointerHandlers = () => {
|
||||
currentHandlers.value.setupPointerHandlers()
|
||||
}
|
||||
|
||||
const cleanupPointerHandlers = () => {
|
||||
currentHandlers.value.cleanupPointerHandlers()
|
||||
}
|
||||
const setupPointerHandlers = () => currentHandlers.value.setupPointerHandlers()
|
||||
const cleanupPointerHandlers = () => currentHandlers.value.cleanupPointerHandlers()
|
||||
|
||||
watch(
|
||||
() => zoneEditorStore.active,
|
||||
(newValue, oldValue) => {
|
||||
if (newValue !== oldValue) {
|
||||
cleanupPointerHandlers()
|
||||
setupPointerHandlers()
|
||||
}
|
||||
() => {
|
||||
cleanupPointerHandlers()
|
||||
setupPointerHandlers()
|
||||
}
|
||||
)
|
||||
|
||||
|