Compare commits
66 Commits
feature/12
...
feature/13
Author | SHA1 | Date | |
---|---|---|---|
3c7e96ea7f | |||
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 | |||
9e96b2b32a | |||
8eec2e12ce | |||
adc85d49a4 | |||
3dbd68d5cf | |||
2a34c7eea9 | |||
3c5ceaae2d | |||
31ce0a8264 | |||
b4050bee01 | |||
cffab00974 | |||
a92675e4c0 | |||
b2266f5a10 | |||
6cee0b93e6 | |||
b4403f3267 | |||
fa12ce2ec8 | |||
130df8f144 | |||
1105a53feb | |||
104e9e46fb | |||
2223491571 | |||
ff61f88d62 | |||
0f231e10fa | |||
71042881dc | |||
50feea0c9c | |||
dd6b6f2a96 | |||
c09f503d54 | |||
5f44a9aebd |
5
.env.example
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
VITE_NAME=New Quest
|
||||||
|
VITE_DEVELOPMENT=true
|
||||||
|
VITE_SERVER_ENDPOINT=http://localhost:4000
|
||||||
|
VITE_TILE_SIZE_X=64
|
||||||
|
VITE_TILE_SIZE_Y=32
|
18
Dockerfile
@ -4,6 +4,24 @@ WORKDIR /usr/src/app
|
|||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ARG VITE_NAME=${VITE_NAME}
|
||||||
|
ENV VITE_NAME=${VITE_NAME}
|
||||||
|
|
||||||
|
ARG VITE_DEVELOPMENT=${VITE_DEVELOPMENT}
|
||||||
|
ENV VITE_DEVELOPMENT=${VITE_DEVELOPMENT}
|
||||||
|
|
||||||
|
ARG VITE_SERVER_ENDPOINT=${VITE_SERVER_ENDPOINT}
|
||||||
|
ENV VITE_SERVER_ENDPOINT=${VITE_SERVER_ENDPOINT}
|
||||||
|
|
||||||
|
ARG VITE_TILE_SIZE_X=${VITE_TILE_SIZE_X}
|
||||||
|
ENV VITE_TILE_SIZE_X=${VITE_TILE_SIZE_X}
|
||||||
|
|
||||||
|
ARG VITE_TILE_SIZE_Y=${VITE_TILE_SIZE_Y}
|
||||||
|
ENV VITE_TILE_SIZE_Y=${VITE_TILE_SIZE_Y}
|
||||||
|
|
||||||
|
# Build the application
|
||||||
RUN npm run build-ntc
|
RUN npm run build-ntc
|
||||||
|
|
||||||
# Production stage
|
# Production stage
|
||||||
|
1137
package-lock.json
generated
32
package.json
@ -15,15 +15,15 @@
|
|||||||
"format": "prettier --write src/"
|
"format": "prettier --write src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vueuse/core": "^10.11.0",
|
"@vueuse/core": "^10.5.0",
|
||||||
"@vueuse/integrations": "^10.11.0",
|
"@vueuse/integrations": "^10.5.0",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.7",
|
||||||
"phaser": "^3.80.1",
|
"phaser": "^3.85.2",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.6",
|
||||||
"socket.io-client": "^4.7.5",
|
"socket.io-client": "^4.8.0",
|
||||||
"universal-cookie": "^6.1.3",
|
"universal-cookie": "^6.1.3",
|
||||||
"vue": "^3.4.33",
|
"vue": "^3.5.10",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.22.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rushstack/eslint-patch": "^1.10.3",
|
"@rushstack/eslint-patch": "^1.10.3",
|
||||||
@ -40,16 +40,16 @@
|
|||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-plugin-vue": "^9.27.0",
|
"eslint-plugin-vue": "^9.27.0",
|
||||||
"jsdom": "^24.1.1",
|
"jsdom": "^24.1.1",
|
||||||
"npm-run-all2": "^6.2.2",
|
"npm-run-all2": "^6.2.3",
|
||||||
"phaser3-rex-plugins": "^1.80.5",
|
"phaser3-rex-plugins": "^1.80.8",
|
||||||
"phavuer": "^0.16.1",
|
"phavuer": "^0.16.1",
|
||||||
"postcss": "^8.4.39",
|
"postcss": "^8.4.47",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"sass": "^1.77.8",
|
"sass": "^1.79.4",
|
||||||
"tailwindcss": "^3.4.6",
|
"tailwindcss": "^3.4.13",
|
||||||
"typescript": "~5.5.3",
|
"typescript": "~5.6.2",
|
||||||
"vite": "^5.3.4",
|
"vite": "^5.4.8",
|
||||||
"vite-plugin-vue-devtools": "^7.3.6",
|
"vite-plugin-vue-devtools": "^7.4.6",
|
||||||
"vitest": "^2.0.3",
|
"vitest": "^2.0.3",
|
||||||
"vue-tsc": "^1.6.5"
|
"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 |
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/login/login-bg.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
23
public/assets/login/login-box-inner.svg
Normal file
After Width: | Height: | Size: 301 KiB |
23
public/assets/login/login-box-outer.svg
Normal file
After Width: | Height: | Size: 400 KiB |
BIN
public/assets/login/nq-logo-v1.png
Normal file
After Width: | Height: | Size: 20 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 +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 |
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 |
@ -30,4 +30,7 @@ const screen = computed(() => {
|
|||||||
}
|
}
|
||||||
return 'login' // default fallback
|
return 'login' // default fallback
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Disable right click
|
||||||
|
addEventListener('contextmenu', (event) => event.preventDefault())
|
||||||
</script>
|
</script>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
// Fonts
|
// 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');
|
||||||
|
|
||||||
//Globals
|
//Globals
|
||||||
|
|
||||||
@ -26,14 +26,14 @@ h5,
|
|||||||
h6,
|
h6,
|
||||||
button,
|
button,
|
||||||
a {
|
a {
|
||||||
@apply font-titles text-white font-medium m-0;
|
@apply font-default text-gray-200 font-medium m-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
p,
|
p,
|
||||||
span,
|
span,
|
||||||
li,
|
li,
|
||||||
label {
|
label {
|
||||||
@apply font-default text-white;
|
@apply font-default text-gray-200;
|
||||||
}
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
@ -56,12 +56,8 @@ input {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-cyan {
|
.input-field {
|
||||||
@apply py-2 px-2.5 font-titles border border-solid border-cyan bg-white/70 rounded;
|
@apply px-4 py-3 text-base focus-visible:outline-none bg-gray border border-solid border-gray-500 rounded text-gray-300;
|
||||||
&:focus,
|
|
||||||
&:focus-visible {
|
|
||||||
@apply outline-2 outline-cyan;
|
|
||||||
}
|
|
||||||
&.inactive {
|
&.inactive {
|
||||||
@apply bg-gray-600/50 hover:cursor-not-allowed;
|
@apply bg-gray-600/50 hover:cursor-not-allowed;
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
@ -87,20 +83,20 @@ button {
|
|||||||
@apply text-center;
|
@apply text-center;
|
||||||
|
|
||||||
&.btn-cyan {
|
&.btn-cyan {
|
||||||
@apply bg-cyan/50 border border-solid border-white/25 rounded drop-shadow-20;
|
@apply bg-cyan text-gray-50 text-base rounded py-3;
|
||||||
|
|
||||||
&.active,
|
&.active,
|
||||||
&:hover {
|
&:hover {
|
||||||
@apply bg-cyan;
|
@apply bg-cyan-800;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.btn-bordeaux {
|
&.btn-red {
|
||||||
@apply bg-bordeaux/50 border border-solid border-white/25 rounded drop-shadow-20;
|
@apply bg-red text-gray-50;
|
||||||
|
|
||||||
&.active,
|
&.active,
|
||||||
&:hover {
|
&:hover {
|
||||||
@apply bg-bordeaux;
|
@apply bg-red-300;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
109
src/components/Effects.vue
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<template>
|
||||||
|
<Scene name="effects" @preload="preloadScene" @create="createScene" @update="updateScene">
|
||||||
|
</Scene>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Scene } from 'phavuer'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
|
import { onBeforeMount, onBeforeUnmount, ref } from 'vue'
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
|
|
||||||
|
const sceneRef = ref<Phaser.Scene | null>(null)
|
||||||
|
|
||||||
|
// Effect-related refs
|
||||||
|
const dayNightCycle = ref<Phaser.GameObjects.Graphics | null>(null)
|
||||||
|
const rainEmitter = ref<Phaser.GameObjects.Particles.ParticleEmitter | null>(null)
|
||||||
|
const fogSprite = ref<Phaser.GameObjects.Sprite | null>(null)
|
||||||
|
|
||||||
|
// Effect parameters
|
||||||
|
const dayNightDuration = 300000 // 5 minutes in milliseconds
|
||||||
|
const maxDarkness = 0.7
|
||||||
|
|
||||||
|
const preloadScene = async (scene: Phaser.Scene) => {
|
||||||
|
scene.load.image('raindrop', 'assets/raindrop.png')
|
||||||
|
scene.load.image('fog', 'assets/fog.png')
|
||||||
|
}
|
||||||
|
|
||||||
|
const createScene = async (scene: Phaser.Scene) => {
|
||||||
|
sceneRef.value = scene
|
||||||
|
createDayNightCycle(scene)
|
||||||
|
createRainEffect(scene)
|
||||||
|
createFogEffect(scene)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateScene = (scene: Phaser.Scene, time: number) => {
|
||||||
|
updateDayNightCycle(time)
|
||||||
|
updateFogEffect()
|
||||||
|
}
|
||||||
|
|
||||||
|
const createDayNightCycle = (scene: Phaser.Scene) => {
|
||||||
|
dayNightCycle.value = scene.add.graphics()
|
||||||
|
dayNightCycle.value.setDepth(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateDayNightCycle = (time: number) => {
|
||||||
|
if (!dayNightCycle.value) return
|
||||||
|
|
||||||
|
const darkness = Math.sin((time % dayNightDuration) / dayNightDuration * Math.PI) * maxDarkness
|
||||||
|
dayNightCycle.value.clear()
|
||||||
|
dayNightCycle.value.fillStyle(0x000000, darkness)
|
||||||
|
dayNightCycle.value.fillRect(0, 0, window.innerWidth, window.innerHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
const 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)
|
||||||
|
toggleRain(true) // Start with rain off
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleRain = (isRaining: boolean) => {
|
||||||
|
if (rainEmitter.value) {
|
||||||
|
rainEmitter.value.setVisible(isRaining)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) // yeetdasasdasd
|
||||||
|
fogSprite.value.setDepth(950) // yeetdasasdasd
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateFogEffect = () => {
|
||||||
|
if (fogSprite.value) {
|
||||||
|
// Example: Oscillate fog opacity
|
||||||
|
const fogOpacity = (Math.sin(Date.now() / 5000) + 1) / 2 * 0.3
|
||||||
|
fogSprite.value.setAlpha(fogOpacity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose methods to control effects
|
||||||
|
const controlEffects = {
|
||||||
|
toggleRain,
|
||||||
|
setFogDensity: (density: number) => {
|
||||||
|
if (fogSprite.value) {
|
||||||
|
fogSprite.value.setAlpha(density)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make control methods available to parent components
|
||||||
|
defineExpose(controlEffects)
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (sceneRef.value) sceneRef.value.scene.remove('effects')
|
||||||
|
})
|
||||||
|
</script>
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<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">
|
<div v-for="(chip, i) in internalValue" :key="i" class="flex gap-2.5 items-center bg-cyan rounded py-1 px-2">
|
||||||
<span class="text-xs">{{ chip }}</span>
|
<span class="text-xs">{{ chip }}</span>
|
||||||
<button type="button" class="text-xs cursor-pointer text-white font-light font-default not-italic hover:text-gray-50" @click="deleteChip(i)" aria-label="Remove chip">×</button>
|
<button type="button" class="text-xs cursor-pointer text-white font-light font-default not-italic hover:text-gray-50" @click="deleteChip(i)" aria-label="Remove chip">×</button>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<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>
|
<template #modalHeader>
|
||||||
<h3 class="m-0 font-medium shrink-0">GM Panel</h3>
|
<h3 class="m-0 font-medium shrink-0 text-gray-300">GM Panel</h3>
|
||||||
<div class="flex gap-1.5 flex-wrap">
|
<div class="flex gap-1.5 flex-wrap">
|
||||||
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">General</button>
|
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">General</button>
|
||||||
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Users</button>
|
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Users</button>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal :isModalOpen="true" :closable="false" :is-resizable="false" :modal-width="modalWidth" :modal-height="modalHeight" :modal-position-x="posXY.x" :modal-position-y="posXY.y">
|
<Modal :isModalOpen="true" :closable="false" :is-resizable="false" :modal-width="modalWidth" :modal-height="modalHeight" :modal-position-x="posXY.x" :modal-position-y="posXY.y">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="m-0 font-medium shrink-0">GM tools</h3>
|
<h3 class="m-0 font-medium shrink-0 text-gray-300">GM tools</h3>
|
||||||
</template>
|
</template>
|
||||||
<template #modalBody>
|
<template #modalBody>
|
||||||
<div class="content flex flex-col gap-2.5 m-4 h-20">
|
<div class="content flex flex-col gap-2.5 m-4 h-20">
|
||||||
@ -20,7 +20,7 @@ import { onMounted, ref } from 'vue'
|
|||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const modalWidth = ref(200)
|
const modalWidth = ref(200)
|
||||||
const modalHeight = ref(160)
|
const modalHeight = ref(180)
|
||||||
|
|
||||||
let posXY = ref({ x: 0, y: 0 })
|
let posXY = ref({ x: 0, y: 0 })
|
||||||
|
|
||||||
|
@ -3,22 +3,22 @@
|
|||||||
<div class="relative p-2.5 flex flex-col items-center justify-between h-72">
|
<div class="relative p-2.5 flex flex-col items-center justify-between h-72">
|
||||||
<div class="filler"></div>
|
<div class="filler"></div>
|
||||||
<img class="max-h-56" :src="`${config.server_endpoint}/assets/objects/${selectedObject?.id}.png`" :alt="'Object ' + selectedObject?.id" />
|
<img class="max-h-56" :src="`${config.server_endpoint}/assets/objects/${selectedObject?.id}.png`" :alt="'Object ' + selectedObject?.id" />
|
||||||
<button class="btn-bordeaux px-4 py-1.5 min-w-24" type="button" @click.prevent="removeObject">Remove</button>
|
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeObject">Remove</button>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
|
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="m-2.5 p-2.5 block">
|
<div class="m-2.5 p-2.5 block">
|
||||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject">
|
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject">
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="name">Name</label>
|
<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>
|
||||||
<div class="form-field-half">
|
<div class="form-field-half">
|
||||||
<label for="origin-x">Origin X</label>
|
<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>
|
||||||
<div class="form-field-half">
|
<div class="form-field-half">
|
||||||
<label for="origin-y">Origin Y</label>
|
<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>
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="origin-x">Tags</label>
|
<label for="origin-x">Tags</label>
|
||||||
@ -26,22 +26,22 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="origin-x">Is animated</label>
|
<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="false">No</option>
|
||||||
<option :value="true">Yes</option>
|
<option :value="true">Yes</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="frame-speed">Frame speed</label>
|
<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>
|
||||||
<div class="form-field-half">
|
<div class="form-field-half">
|
||||||
<label for="frame-width">Frame width</label>
|
<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>
|
||||||
<div class="form-field-half">
|
<div class="form-field-half">
|
||||||
<label for="frame-height">Frame height</label>
|
<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>
|
</div>
|
||||||
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative p-2.5 flex items-center gap-x-2.5">
|
<div class="relative p-2.5 flex items-center gap-x-2.5">
|
||||||
<input v-model="searchQuery" class="input-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">
|
<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" />
|
<input class="hidden" id="upload-asset" ref="objectUploadField" type="file" accept="image/png" multiple @change="handleFileUpload" />
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
@ -4,12 +4,12 @@
|
|||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<div class="w-full flex flex-col">
|
<div class="w-full flex flex-col">
|
||||||
<label class="mb-1.5 font-titles" for="name">Name</label>
|
<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>
|
||||||
|
|
||||||
<div class="w-full flex gap-2 mt-2 pb-4 relative">
|
<div class="w-full flex gap-2 mt-2 pb-4 relative">
|
||||||
<button class="btn-cyan px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="saveSprite">Save</button>
|
<button class="btn-cyan px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="saveSprite">Save</button>
|
||||||
<button class="btn-bordeaux px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="deleteSprite">Delete</button>
|
<button class="btn-red px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="deleteSprite">Delete</button>
|
||||||
<div class="w-[calc(100%_+_32px)] absolute left-[-15px] bottom-0 h-px bg-cyan-200"></div>
|
<div class="w-[calc(100%_+_32px)] absolute left-[-15px] bottom-0 h-px bg-cyan-200"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -19,40 +19,40 @@
|
|||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
{{ action.action }}
|
{{ 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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveSprite">
|
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveSprite">
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="action">Action</label>
|
<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>
|
||||||
<div class="form-field-half">
|
<div class="form-field-half">
|
||||||
<label for="origin-x">Origin X</label>
|
<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>
|
||||||
<div class="form-field-half">
|
<div class="form-field-half">
|
||||||
<label for="origin-y">Origin Y</label>
|
<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>
|
||||||
<div class="form-field-half">
|
<div class="form-field-half">
|
||||||
<label for="is-animated">Is animated</label>
|
<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="false">No</option>
|
||||||
<option :value="true">Yes</option>
|
<option :value="true">Yes</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-half" v-if="action.isAnimated">
|
<div class="form-field-half" v-if="action.isAnimated">
|
||||||
<label for="is-looping">Is looping</label>
|
<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="false">No</option>
|
||||||
<option :value="true">Yes</option>
|
<option :value="true">Yes</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-full" v-if="action.isAnimated">
|
<div class="form-field-full" v-if="action.isAnimated">
|
||||||
<label for="frame-speed">Frame speed</label>
|
<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>
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<SpriteActionsInput v-model="action.sprites" />
|
<SpriteActionsInput v-model="action.sprites" />
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative p-2.5 flex items-center gap-x-2.5">
|
<div class="relative p-2.5 flex items-center gap-x-2.5">
|
||||||
<input v-model="searchQuery" class="input-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">
|
<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">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||||
|
@ -3,14 +3,14 @@
|
|||||||
<div class="relative p-2.5 flex flex-col items-center justify-between h-72">
|
<div class="relative p-2.5 flex flex-col items-center justify-between h-72">
|
||||||
<div class="filler"></div>
|
<div class="filler"></div>
|
||||||
<img class="max-h-72" :src="`${config.server_endpoint}/assets/tiles/${selectedTile?.id}.png`" :alt="'Tile ' + selectedTile?.id" />
|
<img class="max-h-72" :src="`${config.server_endpoint}/assets/tiles/${selectedTile?.id}.png`" :alt="'Tile ' + selectedTile?.id" />
|
||||||
<button class="btn-bordeaux px-4 py-1.5 min-w-24" type="button" @click.prevent="deleteTile">Delete</button>
|
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="deleteTile">Delete</button>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
|
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="m-2.5 p-2.5 block">
|
<div class="m-2.5 p-2.5 block">
|
||||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveTile">
|
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveTile">
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="name">Name</label>
|
<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>
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="origin-x">Tags</label>
|
<label for="origin-x">Tags</label>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative p-2.5 flex items-center gap-x-2.5">
|
<div class="relative p-2.5 flex items-center gap-x-2.5">
|
||||||
<input v-model="searchQuery" class="input-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">
|
<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" />
|
<input class="hidden" id="upload-asset" ref="tileUploadField" type="file" accept="image/png" multiple @change="handleFileUpload" />
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
@ -12,14 +12,14 @@
|
|||||||
<TeleportModal v-if="shouldShowTeleportModal" />
|
<TeleportModal v-if="shouldShowTeleportModal" />
|
||||||
|
|
||||||
<Container :depth="2">
|
<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)" />
|
<Image v-for="object in zoneObjects" :depth="calculateIsometricDepth(object.positionX, object.positionY, 0)" :key="object.id" v-bind="getObjectImageProps(object)" @pointerup="() => setSelectedZoneObject(object)" :flipX="object.isRotated" />
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
<Container :depth="3">
|
<Container :depth="3">
|
||||||
<Image v-for="tile in zoneEventTiles" :key="tile.id" v-bind="getEventTileImageProps(tile)" />
|
<Image v-for="tile in zoneEventTiles" :key="tile.id" v-bind="getEventTileImageProps(tile)" />
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
<SelectedZoneObject v-if="zoneEditorStore.selectedZoneObject" @update_depth="updateZoneObjectDepth" @delete="deleteZoneObject" @move="handleMove" />
|
<SelectedZoneObject v-if="zoneEditorStore.selectedZoneObject" @update_depth="updateZoneObjectDepth" @delete="deleteZoneObject" @move="handleMove" @rotate="handleRotate" />
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -79,8 +79,8 @@ function createTilemap() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createTileLayer() {
|
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 }))
|
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, 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 as any, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
|
const layer = zoneTilemap.createBlankLayer('tiles', tilesetImages as any, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
|
||||||
|
|
||||||
@ -146,6 +146,7 @@ function addZoneObject(tile: Phaser.Tilemaps.Tile) {
|
|||||||
objectId: selectedObject.value!.id,
|
objectId: selectedObject.value!.id,
|
||||||
object: selectedObject.value!,
|
object: selectedObject.value!,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
|
isRotated: false,
|
||||||
positionX: tile.x,
|
positionX: tile.x,
|
||||||
positionY: tile.y
|
positionY: tile.y
|
||||||
}
|
}
|
||||||
@ -208,7 +209,7 @@ function save() {
|
|||||||
tiles: tileArray,
|
tiles: tileArray,
|
||||||
pvp: zone.value.pvp,
|
pvp: zone.value.pvp,
|
||||||
zoneEventTiles: zoneEventTiles.value.map(({ id, zoneId, type, positionX, positionY, teleport }) => ({ id, zoneId, type, positionX, positionY, teleport })),
|
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 }))
|
zoneObjects: zoneObjects.value.map(({ id, zoneId, objectId, depth, isRotated, positionX, positionY }) => ({ id, zoneId, objectId, depth, isRotated, positionX, positionY }))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (zoneEditorStore.isSettingsModalShown) {
|
if (zoneEditorStore.isSettingsModalShown) {
|
||||||
@ -248,31 +249,12 @@ function handleMove() {
|
|||||||
console.log('move btn clicked')
|
console.log('move btn clicked')
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeMount(async () => {
|
function handleRotate(objectId: string) {
|
||||||
tileArray.forEach((row, y) => row.forEach((_, x) => placeTile(zoneTilemap, tiles, x, y, 'blank_tile')))
|
const object = zoneObjects.value.find((obj) => obj.id === objectId)
|
||||||
|
if (object) {
|
||||||
if (zone.value?.tiles) {
|
object.isRotated = !object.isRotated
|
||||||
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()
|
|
||||||
})
|
|
||||||
|
|
||||||
// watch zoneEditorStore.objectList and update originX and originY of objects in zoneObjects
|
// watch zoneEditorStore.objectList and update originX and originY of objects in zoneObjects
|
||||||
watch(
|
watch(
|
||||||
@ -317,5 +299,29 @@ const setSelectedZoneObject = (zoneObject: ZoneObject | null) => {
|
|||||||
onBeforeMount(async () => {
|
onBeforeMount(async () => {
|
||||||
await gameStore.fetchAllZoneAssets()
|
await gameStore.fetchAllZoneAssets()
|
||||||
await loadAssets(scene)
|
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>
|
</script>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal :isModalOpen="true" @modal:close="() => zoneEditorStore.toggleCreateZoneModal()" :modal-width="300" :modal-height="400" :is-resizable="false">
|
<Modal :isModalOpen="true" @modal:close="() => zoneEditorStore.toggleCreateZoneModal()" :modal-width="300" :modal-height="400" :is-resizable="false">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="m-0 font-medium shrink-0">Create new zone</h3>
|
<h3 class="m-0 font-medium shrink-0 text-gray-300">Create new zone</h3>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #modalBody>
|
<template #modalBody>
|
||||||
@ -10,19 +10,19 @@
|
|||||||
<div class="gap-2.5 flex flex-wrap">
|
<div class="gap-2.5 flex flex-wrap">
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="name">Name</label>
|
<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>
|
||||||
<div class="form-field-half">
|
<div class="form-field-half">
|
||||||
<label for="name">Width</label>
|
<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>
|
||||||
<div class="form-field-half">
|
<div class="form-field-half">
|
||||||
<label for="name">Height</label>
|
<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>
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="name">PVP enabled</label>
|
<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="false">No</option>
|
||||||
<option :value="true">Yes</option>
|
<option :value="true">Yes</option>
|
||||||
</select>
|
</select>
|
||||||
|
@ -2,16 +2,16 @@
|
|||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<Modal :isModalOpen="zoneEditorStore.isObjectListModalShown" :modal-width="645" :modal-height="260" @modal:close="() => (zoneEditorStore.isObjectListModalShown = false)">
|
<Modal :isModalOpen="zoneEditorStore.isObjectListModalShown" :modal-width="645" :modal-height="260" @modal:close="() => (zoneEditorStore.isObjectListModalShown = false)">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="text-lg">Objects</h3>
|
<h3 class="text-lg text-gray-300">Objects</h3>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="w-full flex gap-1.5 flex-row">
|
<div class="w-full flex gap-1.5 flex-row">
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-1.5 font-titles hidden" for="search">Search...</label>
|
<label class="mb-1.5 font-titles hidden" for="search">Search...</label>
|
||||||
<input @mousedown.stop class="input-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>-->
|
<!-- <div>-->
|
||||||
<!-- <label class="mb-1.5 font-titles hidden" for="depth">Depth</label>-->
|
<!-- <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" />-->
|
<!-- <input v-model="objectDepth" @mousedown.stop class="input-field" type="number" name="depth" placeholder="Depth" />-->
|
||||||
<!-- </div>-->
|
<!-- </div>-->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col items-center py-5 px-3 fixed bottom-14 right-0">
|
<div class="flex flex-col items-center py-5 px-3 fixed bottom-14 right-0" v-if="zoneEditorStore.selectedZoneObject">
|
||||||
<div class="self-end mt-2 flex gap-2">
|
<div class="self-end mt-2 flex gap-2">
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-1.5 font-titles block text-sm text-gray-700 hidden" for="depth">Depth</label>
|
<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" />
|
<input v-model="objectDepth" @mousedown.stop @input="handleDepthInput" class="input-field max-w-24 px-2 py-1 border rounded" type="number" name="depth" placeholder="Depth" />
|
||||||
</div>
|
</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" />
|
<img src="/assets/icons/trashcan.svg" class="w-4 h-4" alt="Delete" />
|
||||||
</button>
|
</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="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" :disabled="!isObjectSelected">Move</button>
|
<button @mousedown.stop @click="handleMove" class="btn-cyan py-1.5 px-4 min-w-24">Move</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -18,13 +18,11 @@
|
|||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
|
|
||||||
const emit = defineEmits(['update_depth', 'move', 'delete'])
|
const emit = defineEmits(['update_depth', 'move', 'delete', 'rotate'])
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
|
|
||||||
const objectDepth = ref(zoneEditorStore.objectDepth)
|
const objectDepth = ref(zoneEditorStore.objectDepth)
|
||||||
|
|
||||||
const isObjectSelected = computed(() => !!zoneEditorStore.selectedZoneObject)
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => zoneEditorStore.selectedZoneObject,
|
() => zoneEditorStore.selectedZoneObject,
|
||||||
(selectedZoneObject) => {
|
(selectedZoneObject) => {
|
||||||
@ -39,6 +37,10 @@ const handleDepthInput = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleRotate = () => {
|
||||||
|
emit('rotate', zoneEditorStore.selectedZoneObject?.id)
|
||||||
|
}
|
||||||
|
|
||||||
const handleMove = () => {
|
const handleMove = () => {
|
||||||
emit('move')
|
emit('move')
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal :is-modal-open="true" @modal:close="() => zoneEditorStore.setTool('move')" :modal-width="300" :modal-height="350" :is-resizable="false">
|
<Modal :is-modal-open="true" @modal:close="() => zoneEditorStore.setTool('move')" :modal-width="300" :modal-height="350" :is-resizable="false">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="m-0 font-medium shrink-0">Teleport settings</h3>
|
<h3 class="m-0 font-medium shrink-0 text-gray-300">Teleport settings</h3>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #modalBody>
|
<template #modalBody>
|
||||||
@ -10,15 +10,15 @@
|
|||||||
<div class="gap-2.5 flex flex-wrap">
|
<div class="gap-2.5 flex flex-wrap">
|
||||||
<div class="form-field-half">
|
<div class="form-field-half">
|
||||||
<label for="positionX">Position X</label>
|
<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>
|
||||||
<div class="form-field-half">
|
<div class="form-field-half">
|
||||||
<label for="positionY">Position Y</label>
|
<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>
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="rotation">Rotation</label>
|
<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="0">North</option>
|
||||||
<option :value="2">East</option>
|
<option :value="2">East</option>
|
||||||
<option :value="4">South</option>
|
<option :value="4">South</option>
|
||||||
@ -27,7 +27,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="toZoneId">Zone to teleport to</label>
|
<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 :value="0">Select zone</option>
|
||||||
<option v-for="zone in zoneEditorStore.zoneList" :key="zone.id" :value="zone.id">{{ zone.name }}</option>
|
<option v-for="zone in zoneEditorStore.zoneList" :key="zone.id" :value="zone.id">{{ zone.name }}</option>
|
||||||
</select>
|
</select>
|
||||||
@ -79,4 +79,4 @@ function updateTeleportSettings() {
|
|||||||
toZoneId: toZoneId.value
|
toZoneId: toZoneId.value
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -2,12 +2,12 @@
|
|||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<Modal :isModalOpen="zoneEditorStore.isTileListModalShown" :modal-width="645" :modal-height="600" @modal:close="() => (zoneEditorStore.isTileListModalShown = false)">
|
<Modal :isModalOpen="zoneEditorStore.isTileListModalShown" :modal-width="645" :modal-height="600" @modal:close="() => (zoneEditorStore.isTileListModalShown = false)">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="text-lg">Tiles</h3>
|
<h3 class="text-lg text-gray-300">Tiles</h3>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="w-full flex gap-1.5 flex-row">
|
<div class="w-full flex gap-1.5 flex-row">
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-1.5 font-titles hidden" for="search">Search...</label>
|
<label class="mb-1.5 font-titles hidden" for="search">Search...</label>
|
||||||
<input @mousedown.stop class="input-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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex justify-center p-5">
|
<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 class="toolbar fixed bottom-0 left-0 m-3 rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10">
|
||||||
<div ref="clickOutsideElement" class="tools flex gap-2.5" v-if="zoneEditorStore.zone">
|
<div ref="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')">
|
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': zoneEditorStore.tool === 'move' }" @click="handleClick('move')">
|
||||||
<img class="invert w-5 h-5" src="/assets/icons/zoneEditor/move.svg" alt="Move camera" /> <span :class="{ 'ml-2.5': zoneEditorStore.tool !== 'move' }">(M)</span>
|
<img class="invert w-5 h-5" src="/assets/icons/zoneEditor/move.svg" alt="Move camera" /> <span :class="{ 'ml-2.5': zoneEditorStore.tool !== 'move' }">(M)</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="w-px bg-cyan"></div>
|
<div class="w-px bg-cyan"></div>
|
||||||
|
|
||||||
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan-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>
|
<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" v-if="zoneEditorStore.tool === 'pencil'">
|
||||||
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectPencilOpen }">
|
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectPencilOpen }">
|
||||||
{{ zoneEditorStore.drawMode }}
|
{{ zoneEditorStore.drawMode }}
|
||||||
<img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/zoneEditor/chevron.svg" />
|
<img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/zoneEditor/chevron.svg" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray-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')">
|
<span class="py-2 px-2.5 relative hover:bg-cyan/50" @click="setDrawMode('tile')">
|
||||||
Tile
|
Tile
|
||||||
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
|
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
|
||||||
@ -35,14 +35,14 @@
|
|||||||
|
|
||||||
<div class="w-px bg-cyan"></div>
|
<div class="w-px bg-cyan"></div>
|
||||||
|
|
||||||
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan-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>
|
<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" v-if="zoneEditorStore.tool === 'eraser'">
|
||||||
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectEraserOpen }">
|
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectEraserOpen }">
|
||||||
{{ zoneEditorStore.eraserMode }}
|
{{ zoneEditorStore.eraserMode }}
|
||||||
<img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/zoneEditor/chevron.svg" />
|
<img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/zoneEditor/chevron.svg" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray-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')">
|
<span class="py-2 px-2.5 relative hover:bg-cyan/50" @click="setEraserMode('tile')">
|
||||||
Tile
|
Tile
|
||||||
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
|
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
|
||||||
@ -62,7 +62,7 @@
|
|||||||
|
|
||||||
<div class="w-px bg-cyan"></div>
|
<div class="w-px bg-cyan"></div>
|
||||||
|
|
||||||
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan-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>
|
<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>
|
</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>
|
<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>
|
||||||
|
|
||||||
<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="() => 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('save')" v-if="zoneEditorStore.zone">Save</button>
|
||||||
<button class="btn-cyan px-3.5" @click="() => emit('clear')" v-if="zoneEditorStore.zone">Clear</button>
|
<button class="btn-cyan px-3.5" @click="() => emit('clear')" v-if="zoneEditorStore.zone">Clear</button>
|
||||||
@ -171,13 +171,26 @@ function handleClick(tool: string) {
|
|||||||
selectEraserOpen.value = tool === 'eraser' ? !selectEraserOpen.value : false
|
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) {
|
function initKeyShortcuts(event: KeyboardEvent) {
|
||||||
if (!zoneEditorStore.zone) return
|
if (!zoneEditorStore.zone) return
|
||||||
// prevent if focused on composables
|
// prevent if focused on composables
|
||||||
if (document.activeElement?.tagName === 'INPUT') return
|
if (document.activeElement?.tagName === 'INPUT') return
|
||||||
|
|
||||||
const keyActions: any = {
|
const keyActions: { [key: string]: string } = {
|
||||||
m: 'move',
|
m: 'move',
|
||||||
p: 'pencil',
|
p: 'pencil',
|
||||||
e: 'eraser',
|
e: 'eraser',
|
||||||
@ -186,7 +199,12 @@ function initKeyShortcuts(event: KeyboardEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (keyActions.hasOwnProperty(event.key)) {
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<Modal @modal:close="() => zoneEditorStore.toggleZoneListModal()" :is-resizable="false" :is-modal-open="true" :modal-width="300" :modal-height="360">
|
<Modal @modal:close="() => zoneEditorStore.toggleZoneListModal()" :is-resizable="false" :is-modal-open="true" :modal-width="300" :modal-height="360">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="text-lg">Zones</h3>
|
<h3 class="text-lg text-gray-300">Zones</h3>
|
||||||
</template>
|
</template>
|
||||||
<template #modalBody>
|
<template #modalBody>
|
||||||
<div class="my-4 mx-auto">
|
<div class="my-4 mx-auto">
|
||||||
@ -17,7 +17,7 @@
|
|||||||
<div class="flex gap-3 items-center w-full" @click="() => loadZone(zone.id)">
|
<div class="flex gap-3 items-center w-full" @click="() => loadZone(zone.id)">
|
||||||
<span>{{ zone.name }}</span>
|
<span>{{ zone.name }}</span>
|
||||||
<span class="ml-auto gap-1 flex">
|
<span class="ml-auto gap-1 flex">
|
||||||
<button class="btn-bordeaux py-0.5 px-2.5 z-50" @click.stop="() => deleteZone(zone.id)">X</button>
|
<button class="btn-red py-0.5 px-2.5 z-50" @click.stop="() => deleteZone(zone.id)">X</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
|
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<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>
|
<template #modalHeader>
|
||||||
<h3 class="m-0 font-medium shrink-0">Zone settings</h3>
|
<h3 class="m-0 font-medium shrink-0 text-gray-300">Zone settings</h3>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #modalBody>
|
<template #modalBody>
|
||||||
@ -10,19 +10,19 @@
|
|||||||
<div class="gap-2.5 flex flex-wrap">
|
<div class="gap-2.5 flex flex-wrap">
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="name">Name</label>
|
<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>
|
||||||
<div class="form-field-half">
|
<div class="form-field-half">
|
||||||
<label for="name">Width</label>
|
<label for="name">Width</label>
|
||||||
<input class="input-cyan" v-model="width" name="name" id="name" type="number" />
|
<input class="input-field" v-model="width" name="name" id="name" type="number" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-half">
|
<div class="form-field-half">
|
||||||
<label for="name">Height</label>
|
<label for="name">Height</label>
|
||||||
<input class="input-cyan" v-model="height" name="name" id="name" type="number" />
|
<input class="input-field" v-model="height" name="name" id="name" type="number" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="pvp">PVP enabled</label>
|
<label for="pvp">PVP enabled</label>
|
||||||
<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="false">No</option>
|
||||||
<option :value="true">Yes</option>
|
<option :value="true">Yes</option>
|
||||||
</select>
|
</select>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full md:min-w-[350px] max-w-[750px] flex flex-col">
|
<div class="w-full md:min-w-[350px] max-w-[750px] flex flex-col">
|
||||||
<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 ref="chatWindow" class="w-full overflow-auto h-32 mb-5 bg-gray-300/80 rounded-lg border-2 border-solid border-cyan-200" v-show="gameStore.uiSettings.isChatOpen">
|
||||||
<div v-for="message in chats" class="flex-col py-2 items-center p-3">
|
<div v-for="message in chats" class="flex-col py-2 items-center p-3">
|
||||||
<span class="text-ellipsis overflow-hidden whitespace-nowrap text-sm">{{ message.character.name }}</span>
|
<span class="text-ellipsis overflow-hidden whitespace-nowrap text-sm">{{ message.character.name }}</span>
|
||||||
<p class="text-gray-50 m-0">{{ message.message }}</p>
|
<p class="text-gray-50 m-0">{{ message.message }}</p>
|
||||||
@ -8,7 +8,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="w-full flex">
|
<div class="w-full flex">
|
||||||
<input
|
<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-full h-12 rounded-lg text-lg px-4 py-0 bg-gray border-2 border-solid border-gray-500 text-gray-300 bg-[url('/assets/icons/submit-icon.svg')] bg-no-repeat bg-[right_25px_center] bg-[length:30px] focus:outline-none focus:ring-0 focus:border-cyan-800"
|
||||||
placeholder="Type something..."
|
placeholder="Type something..."
|
||||||
v-model="message"
|
v-model="message"
|
||||||
@keypress="handleKeyPress"
|
@keypress="handleKeyPress"
|
||||||
@ -59,61 +59,63 @@ const scrollToBottom = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.on('chat:message', (data: ChatMessage) => {
|
gameStore.connection?.on('chat:message', (data: ChatMessage) => {
|
||||||
chats.value.push(data)
|
chats.value.push(data)
|
||||||
scrollToBottom()
|
|
||||||
|
|
||||||
if (!zoneStore.characterLoaded) return
|
|
||||||
|
|
||||||
const charChatContainer = scene.children.getByName(data.character.name + '_chatContainer') as Phaser.GameObjects.Container
|
|
||||||
if (!charChatContainer) return
|
|
||||||
|
|
||||||
const chatBubble = charChatContainer.getByName(data.character.name + '_chatBubble') as Phaser.GameObjects.Container
|
|
||||||
const chatText = charChatContainer.getByName(data.character.name + '_chatText') as Phaser.GameObjects.Text
|
|
||||||
if (!chatText || !chatBubble) return
|
|
||||||
|
|
||||||
function calculateTextWidth(text: string, font: string, fontSize: number): number {
|
|
||||||
// Create a canvas element
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
const context = canvas.getContext('2d');
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('Unable to create canvas context');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the font
|
|
||||||
context.font = `${fontSize}px ${font}`;
|
|
||||||
|
|
||||||
// Measure the text width
|
|
||||||
const metrics = context.measureText(text);
|
|
||||||
|
|
||||||
return metrics.width;
|
|
||||||
}
|
|
||||||
|
|
||||||
chatBubble.width = calculateTextWidth(data.message, 'Arial', 13) + 10
|
|
||||||
chatText.setText(data.message)
|
|
||||||
|
|
||||||
charChatContainer.setVisible(true)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hide chat bubble after a few seconds
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Clear any existing hide timer
|
|
||||||
if (charChatContainer.getData('hideTimer')) {
|
|
||||||
clearTimeout(charChatContainer.getData('hideTimer'))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set a new hide timer
|
|
||||||
const hideTimer = setTimeout(() => {
|
|
||||||
charChatContainer.setVisible(false)
|
|
||||||
}, 3000)
|
|
||||||
|
|
||||||
// Store the timer on the container itself
|
|
||||||
charChatContainer.setData('hideTimer', hideTimer)
|
|
||||||
})
|
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
|
|
||||||
|
if (!zoneStore.characterLoaded) return
|
||||||
|
|
||||||
|
const charChatContainer = scene.children.getByName(data.character.name + '_chatContainer') as Phaser.GameObjects.Container
|
||||||
|
if (!charChatContainer) return
|
||||||
|
|
||||||
|
const chatBubble = charChatContainer.getByName(data.character.name + '_chatBubble') as Phaser.GameObjects.Container
|
||||||
|
const chatText = charChatContainer.getByName(data.character.name + '_chatText') as Phaser.GameObjects.Text
|
||||||
|
if (!chatText || !chatBubble) return
|
||||||
|
|
||||||
|
function calculateTextWidth(text: string, font: string, fontSize: number): number {
|
||||||
|
// Create a canvas element
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const context = canvas.getContext('2d')
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('Unable to create canvas context')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the font
|
||||||
|
context.font = `${fontSize}px ${font}`
|
||||||
|
|
||||||
|
// Measure the text width
|
||||||
|
const metrics = context.measureText(text)
|
||||||
|
|
||||||
|
return metrics.width
|
||||||
|
}
|
||||||
|
|
||||||
|
chatBubble.width = calculateTextWidth(data.message.substring(0, 90), 'Arial', 13) + 30
|
||||||
|
|
||||||
|
// setText but with max. char limit of 90
|
||||||
|
chatText.setText(data.message.substring(0, 90))
|
||||||
|
|
||||||
|
charChatContainer.setVisible(true)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide chat bubble after a few seconds
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Clear any existing hide timer
|
||||||
|
if (charChatContainer.getData('hideTimer')) {
|
||||||
|
clearTimeout(charChatContainer.getData('hideTimer'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a new hide timer
|
||||||
|
const hideTimer = setTimeout(() => {
|
||||||
|
charChatContainer.setVisible(false)
|
||||||
|
}, 3000)
|
||||||
|
|
||||||
|
// Store the timer on the container itself
|
||||||
|
charChatContainer.setData('hideTimer', hideTimer)
|
||||||
|
})
|
||||||
|
scrollToBottom()
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
gameStore.connection?.off('chat:message')
|
gameStore.connection?.off('chat:message')
|
||||||
})
|
})
|
||||||
|
9
src/components/gui/ExpBar.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
</script>
|
@ -1,46 +1,5 @@
|
|||||||
<template>
|
<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>
|
|
||||||
<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>
|
|
||||||
</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>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
9
src/components/gui/Keybindings.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
</script>
|
9
src/components/gui/Minimap.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
</script>
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="absolute z-50 w-full h-dvh top-0 left-0 bg-black/60" v-show="gameStore.isUserPanelOpen">
|
<div class="absolute z-50 w-full h-dvh top-0 left-0 bg-black/60" v-show="gameStore.uiSettings.isUserPanelOpen">
|
||||||
<div class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 max-w-[875px] max-h-[600px] h-full w-[80%] bg-gray-300/80 border-solid border-2 border-cyan-200 rounded-lg z-50 flex flex-col backdrop-blur-sm shadow-lg">
|
<div class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 max-w-[875px] max-h-[600px] h-full w-[80%] bg-gray-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="p-2.5 flex max-sm:flex-wrap justify-between items-center gap-5 border-solid border-0 border-b border-cyan-200">
|
||||||
<h3 class="m-0 font-medium shrink-0">Game menu</h3>
|
<h3 class="m-0 font-medium shrink-0">Game menu</h3>
|
||||||
|
@ -6,11 +6,11 @@
|
|||||||
<form class="flex gap-2.5 flex-wrap">
|
<form class="flex gap-2.5 flex-wrap">
|
||||||
<div class="form-field-half max-w-[300px]">
|
<div class="form-field-half max-w-[300px]">
|
||||||
<label for="name">Name</label>
|
<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>
|
||||||
<div class="form-field-half max-w-[300px] relative">
|
<div class="form-field-half max-w-[300px] relative">
|
||||||
<label for="class">Class</label>
|
<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="Knight" :selected="characterClass == 'Knight'" :disabled="characterClass == 'Knight'">Knight</option>
|
||||||
<option value="Paladin" :selected="characterClass == 'Paladin'" :disabled="characterClass == 'Paladin'">Paladin</option>
|
<option value="Paladin" :selected="characterClass == 'Paladin'" :disabled="characterClass == 'Paladin'">Paladin</option>
|
||||||
</select>
|
</select>
|
||||||
|
@ -1,58 +1,58 @@
|
|||||||
<template>
|
<template>
|
||||||
<Container ref="charChatContainer" :depth="999" v-if="props.character" :x="currentX" :y="currentY">>
|
<!-- Chat bubble -->
|
||||||
<RoundRectangle @create="createChatBubble" :origin-x="0.5" :origin-y="7.5" :fillColor="0xffffff" :width="194" :height="21" :radius="5" />
|
<Container ref="charChatContainer" :depth="999" :x="currentX" :y="currentY">
|
||||||
<Text @create="createChatText" :text="`This is a chat message 🤯👋`" :origin-x="0.5" :origin-y="10.9" :style="{ fontSize: 13, fontFamily: 'Arial', color: '#000' }" />
|
<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>
|
</Container>
|
||||||
<Container :depth="999" v-if="props.character" :x="currentX" :y="currentY">
|
<!-- Character name and health -->
|
||||||
<Text @create="createText" :text="props.character.name" :origin-x="0.5" :origin-y="9" />
|
<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="18.5" :fillColor="0xffffff" :width="74" :height="6" :radius="5" />
|
||||||
<RoundRectangle :origin-x="0.5" :origin-y="36.4" :fillColor="0x00b3b3" :width="70" :height="3" :radius="5" />
|
<RoundRectangle :origin-x="0.5" :origin-y="36.4" :fillColor="0x00b3b3" :width="70" :height="3" :radius="5" />
|
||||||
</Container>
|
</Container>
|
||||||
<Container :depth="isometricDepth" v-if="props.character" :x="currentX" :y="currentY" ref="charContainer">
|
<!-- Character sprite -->
|
||||||
<Image v-if="!props.character.characterType" texture="character" :origin-y="1" />
|
<Container ref="charContainer" :depth="isometricDepth" :x="currentX" :y="currentY">
|
||||||
<Sprite v-else :texture="charTexture" :play="props.character.isMoving ? charTexture : undefined" :origin-y="1" :flipX="props.character.rotation === 6 || props.character.rotation === 4" :flipY="false" />
|
<Sprite ref="charSprite" :origin-y="1" :flipX="isFlippedX" :flipY="false" />
|
||||||
</Container>
|
</Container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Container, Image, refObj, RoundRectangle, Sprite, Text, useScene } from 'phavuer'
|
|
||||||
import { type ExtendedCharacter as CharacterT } from '@/types'
|
|
||||||
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
|
||||||
import { watch, computed, ref, onMounted, onUnmounted, type Ref } from 'vue'
|
|
||||||
import config from '@/config'
|
import config from '@/config'
|
||||||
|
import { type ExtendedCharacter } from '@/types'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useZoneStore } from '@/stores/zoneStore'
|
import { useZoneStore } from '@/stores/zoneStore'
|
||||||
|
import { watch, computed, ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { Container, refObj, RoundRectangle, Sprite, Text, useGame, useScene } from 'phavuer'
|
||||||
|
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||||
|
|
||||||
enum Direction {
|
enum Direction {
|
||||||
POSITIVE,
|
POSITIVE,
|
||||||
NEGATIVE,
|
NEGATIVE,
|
||||||
NOCHANGE
|
UNCHANGED
|
||||||
}
|
}
|
||||||
|
|
||||||
const charChatContainer = refObj() as Ref<Phaser.GameObjects.Container>;
|
const props = defineProps<{
|
||||||
const charContainer = refObj() as Ref<Phaser.GameObjects.Container>;
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
layer: Phaser.Tilemaps.TilemapLayer
|
layer: Phaser.Tilemaps.TilemapLayer
|
||||||
character?: CharacterT
|
character: ExtendedCharacter
|
||||||
}
|
}>()
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const charChatContainer = refObj<Phaser.GameObjects.Container>()
|
||||||
character: undefined
|
const charContainer = refObj<Phaser.GameObjects.Container>()
|
||||||
})
|
const charSprite = refObj<Phaser.GameObjects.Sprite>()
|
||||||
|
|
||||||
|
const game = useGame()
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const zoneStore = useZoneStore()
|
const zoneStore = useZoneStore()
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
|
|
||||||
const isometricDepth = ref(calculateIsometricDepth(props.character!.positionX, props.character!.positionY, 28, 94, true))
|
|
||||||
const currentX = ref(0)
|
const currentX = ref(0)
|
||||||
const currentY = ref(0)
|
const currentY = ref(0)
|
||||||
const tween = ref<Phaser.Tweens.Tween | null>(null)
|
const isometricDepth = ref(1)
|
||||||
const isInitialPosition = ref(true)
|
const isInitialPosition = ref(true)
|
||||||
|
const tween = ref<Phaser.Tweens.Tween | null>(null)
|
||||||
|
|
||||||
const calculateLocalDepth = (x: number, y: number, width: number, height: number, isCharacter: boolean) => {
|
const updateIsometricDepth = (x: number, y: number) => {
|
||||||
isometricDepth.value = calculateIsometricDepth(x, y, width, height, isCharacter)
|
isometricDepth.value = calculateIsometricDepth(x, y, 28, 94, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatePosition = (x: number, y: number, direction: Direction) => {
|
const updatePosition = (x: number, y: number, direction: Direction) => {
|
||||||
@ -66,52 +66,93 @@ const updatePosition = (x: number, y: number, direction: Direction) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tween.value && tween.value.isPlaying()) {
|
if (tween.value?.isPlaying()) {
|
||||||
tween.value.stop()
|
tween.value.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate the distance between the current and target positions
|
|
||||||
*/
|
|
||||||
const distance = Math.sqrt(Math.pow(targetX - currentX.value, 2) + Math.pow(targetY - currentY.value, 2))
|
const distance = Math.sqrt(Math.pow(targetX - currentX.value, 2) + Math.pow(targetY - currentY.value, 2))
|
||||||
|
|
||||||
/**
|
if (distance >= config.tile_size.x / 1.1) {
|
||||||
* Teleport: No animation, only if the distance is greater than the tile size / 1.5
|
|
||||||
*/
|
|
||||||
if (distance >= config.tile_size.x / 1.2) {
|
|
||||||
// Teleport: No animation
|
|
||||||
currentX.value = targetX
|
currentX.value = targetX
|
||||||
currentY.value = targetY
|
currentY.value = targetY
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const duration = distance * 6
|
||||||
* Normal movement: Animate
|
|
||||||
*/
|
|
||||||
if (distance <= config.tile_size.x / 1.2) {
|
|
||||||
// Normal movement: Animate
|
|
||||||
const duration = distance * 6 // Adjust this multiplier to control overall speed
|
|
||||||
|
|
||||||
tween.value = props.layer.scene.tweens.add({
|
tween.value = props.layer.scene.tweens.add({
|
||||||
targets: { x: currentX.value, y: currentY.value },
|
targets: { x: currentX.value, y: currentY.value },
|
||||||
x: targetX,
|
x: targetX,
|
||||||
y: targetY,
|
y: targetY,
|
||||||
duration: duration,
|
duration,
|
||||||
ease: 'Linear',
|
ease: 'Linear',
|
||||||
onStart: () => {
|
onStart: () => {
|
||||||
if (direction === Direction.POSITIVE) {
|
if (direction === Direction.POSITIVE) {
|
||||||
calculateLocalDepth(x, y, 28, 94, true)
|
updateIsometricDepth(x, y)
|
||||||
}
|
|
||||||
},
|
|
||||||
onUpdate: (tween) => {
|
|
||||||
currentX.value = tween.targets[0].x ?? 0
|
|
||||||
currentY.value = tween.targets[0].y ?? 0
|
|
||||||
},
|
|
||||||
onComplete: () => {
|
|
||||||
if (direction === Direction.NEGATIVE) {
|
|
||||||
calculateLocalDepth(x, y, 28, 94, true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
onUpdate: (tween) => {
|
||||||
|
currentX.value = tween.targets[0].x
|
||||||
|
currentY.value = tween.targets[0].y
|
||||||
|
},
|
||||||
|
onComplete: () => {
|
||||||
|
if (direction === Direction.NEGATIVE) {
|
||||||
|
updateIsometricDepth(x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const calcDirection = (oldX: number, oldY: number, newX: number, newY: number): Direction => {
|
||||||
|
if (newY < oldY || newX < oldX) return Direction.NEGATIVE
|
||||||
|
if (newX > oldX || newY > oldY) return Direction.POSITIVE
|
||||||
|
return Direction.UNCHANGED
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFlippedX = computed(() => [6, 4].includes(props.character.rotation ?? 0))
|
||||||
|
|
||||||
|
const charTexture = computed(() => {
|
||||||
|
const { rotation, characterType, isMoving } = props.character
|
||||||
|
const spriteId = characterType?.sprite.id ?? 'idle_right_down'
|
||||||
|
const action = isMoving ? 'walk' : 'idle'
|
||||||
|
const direction = [0, 6].includes(rotation) ? 'left_up' : 'right_down'
|
||||||
|
|
||||||
|
return `${spriteId}-${action}_${direction}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateSprite = () => {
|
||||||
|
if (props.character.isMoving) {
|
||||||
|
charSprite.value!.anims.play(charTexture.value, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
charSprite.value!.anims.stop()
|
||||||
|
charSprite.value!.setFrame(0)
|
||||||
|
charSprite.value!.setTexture(charTexture.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const createChatBubble = (container: Phaser.GameObjects.Container) => {
|
||||||
|
container.setName(`${props.character.name}_chatBubble`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const createChatText = (text: Phaser.GameObjects.Text) => {
|
||||||
|
text.setName(`${props.character.name}_chatText`)
|
||||||
|
text.setFontSize(13)
|
||||||
|
text.setFontFamily('Arial')
|
||||||
|
|
||||||
|
// Fix text alignment on Windows and Android
|
||||||
|
if (game.device.os.windows || game.device.os.android) {
|
||||||
|
text.setOrigin(0.5, 9.75)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createText = (text: Phaser.GameObjects.Text) => {
|
||||||
|
text.setFontSize(13)
|
||||||
|
text.setFontFamily('Arial')
|
||||||
|
|
||||||
|
// Fix text alignment on Windows and Android
|
||||||
|
if (game.device.os.windows || game.device.os.android) {
|
||||||
|
text.setOrigin(0.5, 8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,80 +160,38 @@ watch(
|
|||||||
() => props.character,
|
() => props.character,
|
||||||
(newChar, oldChar) => {
|
(newChar, oldChar) => {
|
||||||
if (!newChar) return
|
if (!newChar) return
|
||||||
|
|
||||||
if (!oldChar || newChar.positionX !== oldChar.positionX || newChar.positionY !== oldChar.positionY) {
|
if (!oldChar || newChar.positionX !== oldChar.positionX || newChar.positionY !== oldChar.positionY) {
|
||||||
if (!oldChar) {
|
const direction = !oldChar ? Direction.POSITIVE : calcDirection(oldChar.positionX, oldChar.positionY, newChar.positionX, newChar.positionY)
|
||||||
updatePosition(newChar.positionX, newChar.positionY, Direction.POSITIVE)
|
updatePosition(newChar.positionX, newChar.positionY, direction)
|
||||||
} else {
|
|
||||||
const direction = calcDirection(oldChar.positionX, oldChar.positionY, newChar.positionX, newChar.positionY)
|
|
||||||
updatePosition(newChar.positionX, newChar.positionY, direction)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
{ immediate: true, deep: true }
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const calcDirection = (oldX: number, oldY: number, newX: number, newY: number) => {
|
watch(() => props.character.isMoving, updateSprite)
|
||||||
if (newY < oldY || newX < oldX) {
|
watch(() => props.character.rotation, updateSprite)
|
||||||
return Direction.NEGATIVE
|
|
||||||
}
|
|
||||||
if (newX > oldX || newY > oldY) {
|
|
||||||
return Direction.POSITIVE
|
|
||||||
}
|
|
||||||
return Direction.NOCHANGE
|
|
||||||
}
|
|
||||||
|
|
||||||
const charTexture = computed(() => {
|
|
||||||
if (!props.character?.characterType?.sprite) {
|
|
||||||
return 'idle_right_down'
|
|
||||||
}
|
|
||||||
|
|
||||||
const rotation = props.character.rotation
|
|
||||||
const spriteId = props.character.characterType.sprite.id
|
|
||||||
const action = props.character.isMoving ? 'walk' : 'idle'
|
|
||||||
|
|
||||||
if (rotation === 0 || rotation === 6) {
|
|
||||||
return `${spriteId}-${action}_left_up`
|
|
||||||
} else if (rotation === 2 || rotation === 4) {
|
|
||||||
return `${spriteId}-${action}_right_down`
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${spriteId}-${action}_left_up`
|
|
||||||
})
|
|
||||||
|
|
||||||
const createChatBubble = (container: Phaser.GameObjects.Container) => {
|
|
||||||
container.setName(props.character?.name + '_chatBubble')
|
|
||||||
}
|
|
||||||
|
|
||||||
const createChatText = (text: Phaser.GameObjects.Text) => {
|
|
||||||
text.setName(props.character?.name + '_chatText')
|
|
||||||
text.setFontSize(13)
|
|
||||||
text.setFontFamily('Arial')
|
|
||||||
}
|
|
||||||
|
|
||||||
const createText = (text: Phaser.GameObjects.Text) => {
|
|
||||||
text.setFontSize(13)
|
|
||||||
text.setFontFamily('Arial')
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// Check if player is this character, then lock with camera
|
charChatContainer.value!.setName(`${props.character!.name}_chatContainer`)
|
||||||
if (props.character && props.character.id === gameStore.character?.id) {
|
charChatContainer.value!.setVisible(false)
|
||||||
charChatContainer.value?.setName(props.character.name + '_chatContainer')
|
charContainer.value!.setName(props.character!.name)
|
||||||
charChatContainer.value?.setVisible(false)
|
|
||||||
|
|
||||||
charContainer.value?.setName(props.character.name)
|
if (props.character.id === gameStore.character!.id) {
|
||||||
|
zoneStore.setCharacterLoaded(true)
|
||||||
|
|
||||||
zoneStore.characterLoaded = 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.character) {
|
// Set sprite
|
||||||
updatePosition(props.character.positionX, props.character.positionY, Direction.POSITIVE)
|
charSprite.value!.setTexture(charTexture.value)
|
||||||
}
|
charSprite.value!.setFlipX(isFlippedX.value)
|
||||||
|
|
||||||
|
updatePosition(props.character.positionX, props.character.positionY, props.character.rotation)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (tween.value) {
|
tween.value?.stop()
|
||||||
tween.value.stop()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -40,12 +40,12 @@ const modalOpened = ref(props.modalOpened)
|
|||||||
<template>
|
<template>
|
||||||
<Modal :closable="false" :is-resizable="false" :isModalOpen="true" @modal:close="() => (modalOpened = !modalOpened)" :modal-width="300" :modal-height="190">
|
<Modal :closable="false" :is-resizable="false" :isModalOpen="true" @modal:close="() => (modalOpened = !modalOpened)" :modal-width="300" :modal-height="190">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<div class="text-white">
|
<div class="text-gray-300">
|
||||||
<slot name="modalHeader"></slot>
|
<slot name="modalHeader"></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #modalBody>
|
<template #modalBody>
|
||||||
<div class="text-white h-full">
|
<div class="text-gray-300 h-full">
|
||||||
<div class="flex h-full flex-col justify-between">
|
<div class="flex h-full flex-col justify-between">
|
||||||
<span class="p-2">
|
<span class="p-2">
|
||||||
<slot name="modalBody"></slot>
|
<slot name="modalBody"></slot>
|
||||||
|
@ -8,21 +8,13 @@ import { onBeforeUnmount, ref } from 'vue'
|
|||||||
import { usePointerHandlers } from '@/composables/usePointerHandlers'
|
import { usePointerHandlers } from '@/composables/usePointerHandlers'
|
||||||
import { useCameraControls } from '@/composables/useCameraControls'
|
import { useCameraControls } from '@/composables/useCameraControls'
|
||||||
|
|
||||||
interface Props {
|
const props = defineProps<{ layer: Phaser.Tilemaps.TilemapLayer }>()
|
||||||
layer: Phaser.Tilemaps.TilemapLayer
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
|
|
||||||
const waypoint = ref({
|
const waypoint = ref({ visible: false, x: 0, y: 0 })
|
||||||
visible: false,
|
|
||||||
x: 0,
|
|
||||||
y: 0
|
|
||||||
})
|
|
||||||
|
|
||||||
const { camera, isDragging } = useCameraControls(scene)
|
const { camera } = useCameraControls(scene)
|
||||||
const { setupPointerHandlers, cleanupPointerHandlers } = usePointerHandlers(scene, props.layer, waypoint, camera, isDragging)
|
const { setupPointerHandlers, cleanupPointerHandlers } = usePointerHandlers(scene, props.layer, waypoint, camera)
|
||||||
|
|
||||||
setupPointerHandlers()
|
setupPointerHandlers()
|
||||||
onBeforeUnmount(cleanupPointerHandlers)
|
onBeforeUnmount(cleanupPointerHandlers)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<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 v-if="isModalOpenRef" class="fixed bg-gray border-solid border-2 border-gray-500 z-50 flex flex-col backdrop-blur-sm shadow-lg" :style="modalStyle">
|
||||||
<div @mousedown="startDrag" class="cursor-move p-2.5 flex justify-between items-center border-solid border-0 border-b border-cyan-200">
|
<div @mousedown="startDrag" class="cursor-move p-2.5 flex justify-between items-center border-solid border-0 border-b border-gray-500">
|
||||||
<slot name="modalHeader" />
|
<slot name="modalHeader" />
|
||||||
<div class="flex gap-2.5">
|
<div class="flex gap-2.5">
|
||||||
<button @click="toggleFullScreen" class="w-5 h-5 m-0 p-0 relative hover:scale-110 transition-transform duration-300 ease-in-out" v-if="canFullScreen">
|
<button @click="toggleFullScreen" class="w-5 h-5 m-0 p-0 relative hover:scale-110 transition-transform duration-300 ease-in-out" v-if="canFullScreen">
|
||||||
@ -16,7 +16,7 @@
|
|||||||
<slot name="modalBody" />
|
<slot name="modalBody" />
|
||||||
<img v-if="isResizable && !isFullScreen" src="/assets/icons/resize-icon.svg" alt="resize" class="absolute bottom-0 right-0 w-5 h-5 cursor-nwse-resize invert-[60%]" @mousedown="startResize" />
|
<img v-if="isResizable && !isFullScreen" src="/assets/icons/resize-icon.svg" alt="resize" class="absolute bottom-0 right-0 w-5 h-5 cursor-nwse-resize invert-[60%]" @mousedown="startResize" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="$slots.modalFooter" class="px-5 min-h-12 flex justify-end gap-7.5 items-center border-solid border-t border-cyan-200">
|
<div v-if="$slots.modalFooter" class="px-5 min-h-12 flex justify-end gap-7.5 items-center border-solid border-t border-gray-500">
|
||||||
<slot name="modalFooter" />
|
<slot name="modalFooter" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal v-for="notification in gameStore.getNotifications" :key="notification.id" :isModalOpen="true" @modal:close="closeNotification(notification.id)">
|
<Modal v-for="notification in gameStore.getNotifications" :key="notification.id" :isModalOpen="true" @modal:close="closeNotification(notification.id)">
|
||||||
<template #modalHeader v-if="notification.title">
|
<template #modalHeader v-if="notification.title">
|
||||||
<h3 class="m-0 font-medium shrink-0">{{ notification.title }}</h3>
|
<h3 class="m-0 font-medium shrink-0 text-gray-300">{{ notification.title }}</h3>
|
||||||
</template>
|
</template>
|
||||||
<template #modalBody v-if="notification.message">
|
<template #modalBody v-if="notification.message">
|
||||||
<p class="m-4">{{ notification.message }}</p>
|
<p class="m-4">{{ notification.message }}</p>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<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)" />
|
<Image v-for="object in zoneStore.zone?.zoneObjects" :depth="calculateIsometricDepth(object.positionX, object.positionY, object.object.frameWidth, object.object.frameHeight)" :key="object.id" v-bind="getObjectImageProps(object)" :flipX="object.isRotated" />
|
||||||
<!-- <Text v-for="object in zoneStore.zone?.zoneObjects" :key="object.id" :depth="99999" :text="Math.ceil(calculateIsometricDepth(object.positionX, object.positionY, object.object.frameWidth, object.object.frameHeight))" v-bind="getObjectProps(object)" />-->
|
<!-- <Text v-for="object in zoneStore.zone?.zoneObjects" :key="object.id" :depth="99999" :text="Math.ceil(calculateIsometricDepth(object.positionX, object.positionY, object.object.frameWidth, object.object.frameHeight))" v-bind="getObjectProps(object)" />-->
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -38,14 +38,15 @@ function createTileLayer() {
|
|||||||
const uniqueTiles = new Set(tilesFromZone.flat().filter(Boolean))
|
const uniqueTiles = new Set(tilesFromZone.flat().filter(Boolean))
|
||||||
|
|
||||||
const tilesetImages = Array.from(uniqueTiles).map((tile, index) => {
|
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
|
}) as any
|
||||||
|
|
||||||
// Add blank tile
|
// 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
|
const layer = zoneTilemap.createBlankLayer('tiles', tilesetImages, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
|
||||||
|
|
||||||
layer.setDepth(0)
|
layer.setDepth(0)
|
||||||
|
layer.setCullPadding(2, 2)
|
||||||
|
|
||||||
return layer
|
return layer
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
import { useScene } from 'phavuer'
|
import { useScene } from 'phavuer'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useZoneStore } from '@/stores/zoneStore'
|
import { useZoneStore } from '@/stores/zoneStore'
|
||||||
import { onBeforeUnmount, ref } from 'vue'
|
import { onBeforeMount, onBeforeUnmount, ref } from 'vue'
|
||||||
import type { Character as CharacterT, Zone as ZoneT, ExtendedCharacter as ExtendedCharacterT } from '@/types'
|
import type { Character as CharacterT, Zone as ZoneT, ExtendedCharacter as ExtendedCharacterT } from '@/types'
|
||||||
import Tiles from '@/components/zone/Tiles.vue'
|
import Tiles from '@/components/zone/Tiles.vue'
|
||||||
import Objects from '@/components/zone/Objects.vue'
|
import Objects from '@/components/zone/Objects.vue'
|
||||||
@ -26,16 +26,6 @@ type zoneLoadData = {
|
|||||||
characters: CharacterT[]
|
characters: CharacterT[]
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection!.emit('zone:character:join', async (response: zoneLoadData) => {
|
|
||||||
// Fetch assets for new zone
|
|
||||||
await gameStore.fetchZoneAssets(response.zone.id)
|
|
||||||
await loadAssets(scene)
|
|
||||||
|
|
||||||
// Set zone and characters
|
|
||||||
zoneStore.setZone(response.zone)
|
|
||||||
zoneStore.setCharacters(response.characters)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Event listeners
|
// Event listeners
|
||||||
gameStore.connection!.on('zone:character:teleport', async (data: zoneLoadData) => {
|
gameStore.connection!.on('zone:character:teleport', async (data: zoneLoadData) => {
|
||||||
/**
|
/**
|
||||||
@ -58,6 +48,8 @@ gameStore.connection!.on('zone:character:teleport', async (data: zoneLoadData) =
|
|||||||
})
|
})
|
||||||
|
|
||||||
gameStore.connection!.on('zone:character:join', async (data: ExtendedCharacterT) => {
|
gameStore.connection!.on('zone:character:join', async (data: ExtendedCharacterT) => {
|
||||||
|
// If data is from the current user, don't add it to the store
|
||||||
|
if (data.id === gameStore.character?.id) return
|
||||||
zoneStore.addCharacter(data)
|
zoneStore.addCharacter(data)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -69,6 +61,18 @@ gameStore.connection!.on('character:move', (data: ExtendedCharacterT) => {
|
|||||||
zoneStore.updateCharacter(data)
|
zoneStore.updateCharacter(data)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
gameStore.connection!.emit('zone:character:join', async (response: zoneLoadData) => {
|
||||||
|
// Fetch assets for new zone
|
||||||
|
await gameStore.fetchZoneAssets(response.zone.id)
|
||||||
|
await loadAssets(scene)
|
||||||
|
|
||||||
|
// Set zone and characters
|
||||||
|
zoneStore.setZone(response.zone)
|
||||||
|
zoneStore.setCharacters(response.characters)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
zoneStore.reset()
|
zoneStore.reset()
|
||||||
gameStore.connection!.off('zone:character:teleport')
|
gameStore.connection!.off('zone:character:teleport')
|
||||||
|
@ -1,72 +1,83 @@
|
|||||||
import { type Ref } from 'vue'
|
import { type Ref, ref } from 'vue'
|
||||||
import { getTile, tileToWorldXY } from '@/composables/zoneComposable'
|
import { getTile, tileToWorldXY } from '@/composables/zoneComposable'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import config from '@/config'
|
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) {
|
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 gameStore = useGameStore()
|
||||||
|
const pointerStartPosition = ref({ x: 0, y: 0 })
|
||||||
|
const dragThreshold = 5 // pixels
|
||||||
|
|
||||||
function updateWaypoint(pointer: Phaser.Input.Pointer) {
|
function updateWaypoint(worldX: number, worldY: number) {
|
||||||
const { x: px, y: py } = camera.getWorldPoint(pointer.x, pointer.y)
|
const pointerTile = getTile(worldX, worldY, layer)
|
||||||
const pointerTile = getTile(px, py, layer)
|
if (pointerTile) {
|
||||||
|
const worldPoint = tileToWorldXY(layer, pointerTile.x, pointerTile.y)
|
||||||
waypoint.value.visible = !!pointerTile
|
waypoint.value = {
|
||||||
if (!pointerTile) {
|
visible: true,
|
||||||
return
|
x: worldPoint.positionX,
|
||||||
|
y: worldPoint.positionY + config.tile_size.y + 15
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
waypoint.value.visible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const worldPoint = tileToWorldXY(layer, pointerTile.x, pointerTile.y)
|
|
||||||
waypoint.value.x = worldPoint.positionX
|
|
||||||
waypoint.value.y = worldPoint.positionY + config.tile_size.y + 15
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function dragZone(pointer: Phaser.Input.Pointer) {
|
function handlePointerDown(pointer: Phaser.Input.Pointer) {
|
||||||
if (!gameStore.isPlayerDraggingCamera) {
|
pointerStartPosition.value = { x: pointer.x, y: pointer.y }
|
||||||
return
|
gameStore.setPlayerDraggingCamera(true)
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
function handlePointerMove(pointer: Phaser.Input.Pointer) {
|
||||||
dragZone(pointer)
|
const { worldX, worldY } = pointer
|
||||||
updateWaypoint(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) {
|
function handlePointerUp(pointer: Phaser.Input.Pointer) {
|
||||||
const { x: px, y: py } = camera.getWorldPoint(pointer.x, pointer.y)
|
const distance = Phaser.Math.Distance.Between(pointerStartPosition.value.x, pointerStartPosition.value.y, pointer.x, pointer.y)
|
||||||
const pointerTile = getTile(px, py, layer)
|
|
||||||
|
|
||||||
if (!pointerTile) {
|
if (distance <= dragThreshold) {
|
||||||
return
|
const pointerTile = getTile(pointer.worldX, pointer.worldY, layer)
|
||||||
|
if (pointerTile) {
|
||||||
|
gameStore.connection?.emit('character:initMove', {
|
||||||
|
positionX: pointerTile.x,
|
||||||
|
positionY: pointerTile.y
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit('character:initMove', {
|
gameStore.setPlayerDraggingCamera(false)
|
||||||
positionX: pointerTile.x,
|
|
||||||
positionY: pointerTile.y
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleZoom({ event, deltaY }: Phaser.Input.Pointer) {
|
function handleZoom(pointer: Phaser.Input.Pointer) {
|
||||||
if (event instanceof WheelEvent && event.shiftKey) {
|
if (pointer.event instanceof WheelEvent && pointer.event.shiftKey) {
|
||||||
return
|
const deltaY = pointer.event.deltaY
|
||||||
|
let zoomLevel = camera.zoom - deltaY * 0.005
|
||||||
|
zoomLevel = Phaser.Math.Clamp(zoomLevel, 1, 3)
|
||||||
|
camera.setZoom(zoomLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
scene.scale.setZoom(scene.scale.zoom - deltaY * 0.01)
|
|
||||||
camera = scene.cameras.main
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const setupPointerHandlers = () => {
|
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_MOVE, handlePointerMove)
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_WHEEL, handleZoom)
|
scene.input.on(Phaser.Input.Events.POINTER_WHEEL, handleZoom)
|
||||||
}
|
}
|
||||||
|
|
||||||
const cleanupPointerHandlers = () => {
|
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_MOVE, handlePointerMove)
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_WHEEL, handleZoom)
|
scene.input.off(Phaser.Input.Events.POINTER_WHEEL, handleZoom)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { computed, type Ref, ref } from 'vue'
|
import { computed, type Ref } from 'vue'
|
||||||
import { getTile, tileToWorldXY } from '@/composables/zoneComposable'
|
import { getTile, tileToWorldXY } from '@/composables/zoneComposable'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
import config from '@/config'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
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) {
|
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()
|
const gameStore = useGameStore()
|
||||||
@ -13,14 +13,16 @@ export function useZoneEditorPointerHandlers(scene: Phaser.Scene, layer: Phaser.
|
|||||||
const { x: px, y: py } = camera.getWorldPoint(pointer.x, pointer.y)
|
const { x: px, y: py } = camera.getWorldPoint(pointer.x, pointer.y)
|
||||||
const pointerTile = getTile(px, py, layer)
|
const pointerTile = getTile(px, py, layer)
|
||||||
|
|
||||||
waypoint.value.visible = !!pointerTile
|
if (pointerTile) {
|
||||||
if (!pointerTile) {
|
const worldPoint = tileToWorldXY(layer, pointerTile.x, pointerTile.y)
|
||||||
return
|
waypoint.value = {
|
||||||
|
visible: true,
|
||||||
|
x: worldPoint.positionX,
|
||||||
|
y: worldPoint.positionY + config.tile_size.y + 15
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
waypoint.value.visible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const worldPoint = tileToWorldXY(layer, pointerTile.x, pointerTile.y)
|
|
||||||
waypoint.value.x = worldPoint.positionX
|
|
||||||
waypoint.value.y = worldPoint.positionY + config.tile_size.y + 15
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function dragZone(pointer: Phaser.Input.Pointer) {
|
function dragZone(pointer: Phaser.Input.Pointer) {
|
||||||
@ -38,13 +40,14 @@ export function useZoneEditorPointerHandlers(scene: Phaser.Scene, layer: Phaser.
|
|||||||
updateWaypoint(pointer)
|
updateWaypoint(pointer)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleZoom({ event, deltaY }: Phaser.Input.Pointer) {
|
function handleZoom(pointer: Phaser.Input.Pointer) {
|
||||||
if (event! instanceof WheelEvent && !event.shiftKey) {
|
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) {
|
||||||
|
camera.setZoom(zoomLevel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scene.scale.setZoom(scene.scale.zoom - deltaY * 0.01)
|
|
||||||
camera = scene.cameras.main
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const setupPointerHandlers = () => {
|
const setupPointerHandlers = () => {
|
||||||
|
@ -1,36 +1,15 @@
|
|||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useScene } from 'phavuer'
|
|
||||||
import { watch } from 'vue'
|
|
||||||
import { useZoneStore } from '@/stores/zoneStore'
|
import { useZoneStore } from '@/stores/zoneStore'
|
||||||
|
|
||||||
export function useCameraControls(scene: Phaser.Scene): any {
|
export function useCameraControls(scene: Phaser.Scene) {
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const zoneStore = useZoneStore()
|
|
||||||
const camera = scene.cameras.main
|
const camera = scene.cameras.main
|
||||||
|
|
||||||
function onPointerDown(pointer: Phaser.Input.Pointer) {
|
const onPointerDown = () => gameStore.setPlayerDraggingCamera(true)
|
||||||
if (pointer.event instanceof MouseEvent || pointer.event.shiftKey) {
|
const onPointerUp = () => gameStore.setPlayerDraggingCamera(false)
|
||||||
gameStore.setPlayerDraggingCamera(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onPointerUp() {
|
|
||||||
gameStore.setPlayerDraggingCamera(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => zoneStore.characterLoaded,
|
|
||||||
(characterLoaded) => {
|
|
||||||
if(characterLoaded) {
|
|
||||||
scene.cameras.main.startFollow(scene.children.getByName(gameStore.character!.name) as Phaser.GameObjects.Container);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_DOWN, onPointerDown)
|
scene.input.on(Phaser.Input.Events.POINTER_DOWN, onPointerDown)
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_UP, onPointerUp)
|
scene.input.on(Phaser.Input.Events.POINTER_UP, onPointerUp)
|
||||||
|
|
||||||
return {
|
return { camera }
|
||||||
camera
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,33 +1,23 @@
|
|||||||
import { computed, type Ref, watch } from 'vue'
|
import { computed, type Ref, watch } from 'vue'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
import { useGamePointerHandlers } from '@/composables/pointerHandlers/useGamePointerHandlers'
|
import { useGamePointerHandlers } from './pointerHandlers/useGamePointerHandlers'
|
||||||
import { useZoneEditorPointerHandlers } from '@/composables/pointerHandlers/useZoneEditorPointerHandlers'
|
import { useZoneEditorPointerHandlers } from './pointerHandlers/useZoneEditorPointerHandlers'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
|
|
||||||
export function usePointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Ref<Phaser.Cameras.Scene2D.Camera>, isDragging: Ref<boolean>) {
|
export function usePointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
|
||||||
const gameStore = useGameStore()
|
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
|
const gameHandlers = useGamePointerHandlers(scene, layer, waypoint, camera)
|
||||||
const gameHandlers = useGamePointerHandlers(scene, layer, waypoint, camera, isDragging)
|
const zoneEditorHandlers = useZoneEditorPointerHandlers(scene, layer, waypoint, camera)
|
||||||
const zoneEditorHandlers = useZoneEditorPointerHandlers(scene, layer, waypoint, camera, isDragging)
|
|
||||||
|
|
||||||
const currentHandlers = computed(() => (zoneEditorStore.active ? zoneEditorHandlers : gameHandlers))
|
const currentHandlers = computed(() => (zoneEditorStore.active ? zoneEditorHandlers : gameHandlers))
|
||||||
|
|
||||||
const setupPointerHandlers = () => {
|
const setupPointerHandlers = () => currentHandlers.value.setupPointerHandlers()
|
||||||
currentHandlers.value.setupPointerHandlers()
|
const cleanupPointerHandlers = () => currentHandlers.value.cleanupPointerHandlers()
|
||||||
}
|
|
||||||
|
|
||||||
const cleanupPointerHandlers = () => {
|
|
||||||
currentHandlers.value.cleanupPointerHandlers()
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => zoneEditorStore.active,
|
() => zoneEditorStore.active,
|
||||||
(newValue, oldValue) => {
|
() => {
|
||||||
if (newValue !== oldValue) {
|
cleanupPointerHandlers()
|
||||||
cleanupPointerHandlers()
|
setupPointerHandlers()
|
||||||
setupPointerHandlers()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
const dev: boolean = true
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'New Quest',
|
name: import.meta.env.VITE_NAME,
|
||||||
development: dev,
|
development: import.meta.env.VITE_DEVELOPMENT === 'true',
|
||||||
server_endpoint: dev ? 'http://localhost:4000' : 'https://nq-server.cr-a.directonline.io',
|
server_endpoint: import.meta.env.VITE_SERVER_ENDPOINT,
|
||||||
tile_size: { x: 64, y: 32, z: 1 }
|
tile_size: {
|
||||||
|
x: Number(import.meta.env.VITE_TILE_SIZE_X),
|
||||||
|
y: Number(import.meta.env.VITE_TILE_SIZE_Y)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @TODO: Implement .env like server has
|
|
||||||
*/
|
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bg-gray-300 relative">
|
<div class="bg-gray-900 relative">
|
||||||
<div class="absolute bg-[url('/assets/shapes/select-screen-bg-shape.svg')] bg-no-repeat bg-center w-full h-full"></div>
|
<div class="absolute bg-[url('/assets/shapes/select-screen-bg-shape.svg')] bg-no-repeat bg-center w-full h-full"></div>
|
||||||
<div class="ui-wrapper h-dvh flex flex-col justify-center items-center gap-20 px-10 sm:px-20">
|
<div class="ui-wrapper h-dvh flex flex-col justify-center items-center gap-20 px-10 sm:px-20">
|
||||||
<div class="filler"></div>
|
<div class="filler"></div>
|
||||||
@ -8,14 +8,16 @@
|
|||||||
<div
|
<div
|
||||||
v-for="character in characters"
|
v-for="character in characters"
|
||||||
:key="character.id"
|
:key="character.id"
|
||||||
class="group first:ml-auto last:mr-auto m-4 w-[170px] h-[275px] flex flex-col shrink-0 rounded-2xl relative bg-[url('/assets/shapes/character-select-shape.svg')] bg-no-repeat shadow-character"
|
class="group first:ml-auto last:mr-auto m-4 w-[170px] h-[275px] flex flex-col shrink-0 relative shadow-character"
|
||||||
:class="{ active: selected_character == character.id }"
|
:class="{ active: selected_character == character.id }"
|
||||||
>
|
>
|
||||||
|
<img src="/assets/login/login-box-outer.svg" class="absolute w-full h-full max-lg:hidden" />
|
||||||
|
<img src="/assets/login/login-box-inner.svg" class="absolute left-2 bottom-2 w-[calc(100%_-_16px)] h-[calc(100%_-_40px)] max-lg:hidden" />
|
||||||
<input class="opacity-0 h-full w-full absolute m-0 z-10" type="radio" :id="character.id" name="character" :value="character.id" v-model="selected_character" />
|
<input class="opacity-0 h-full w-full absolute m-0 z-10" type="radio" :id="character.id" name="character" :value="character.id" v-model="selected_character" />
|
||||||
<label class="font-bold absolute left-1/2 top-5 max-w-32 -translate-x-1/2 -translate-y-1/2 text-center text-ellipsis overflow-hidden whitespace-nowrap drop-shadow-text" :for="character.id">{{ character.name }}</label>
|
<label class="font-bold absolute left-1/2 top-4 max-w-32 -translate-x-1/2 -translate-y-1/2 text-center text-ellipsis overflow-hidden whitespace-nowrap drop-shadow-text" :for="character.id">{{ character.name }}</label>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="delete bg-red w-8 h-8 p-[3px] rounded-full absolute -right-4 top-0 -translate-y-1/2 z-10 border-2 border-solid border-white hover:bg-red-100"
|
class="delete bg-red w-8 h-8 p-[3px] rounded-full absolute -right-4 top-0 -translate-y-1/2 z-10 border-2 border-solid border-white hover:bg-red-300"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
deletingCharacter = character
|
deletingCharacter = character
|
||||||
@ -28,15 +30,15 @@
|
|||||||
<div class="sprite-container flex flex-col items-center m-auto">
|
<div class="sprite-container flex flex-col items-center m-auto">
|
||||||
<img class="drop-shadow-20" draggable="false" src="/assets/avatar/default/0.png" />
|
<img class="drop-shadow-20" draggable="false" src="/assets/avatar/default/0.png" />
|
||||||
</div>
|
</div>
|
||||||
<span class="absolute bottom-5 w-full text-center translate-y-1/2 z-10 drop-shadow-text">Lvl. {{ character.level }}</span>
|
<span class="absolute bottom-6 w-full text-center translate-y-1/2 z-10">Lvl. {{ character.level }}</span>
|
||||||
<div class="selected-character group-[.active]:max-w-[170px] absolute max-w-0 w-4/6 h-[3px] bg-white rounded-[3px] left-1/2 -bottom-4 -translate-x-1/2 transition-all ease-in-out duration-300"></div>
|
<div class="selected-character group-[.active]:max-w-[170px] absolute max-w-0 w-4/6 h-[3px] bg-gray-500 rounded-[3px] left-1/2 -bottom-4 -translate-x-1/2 transition-all ease-in-out duration-300"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="character new-character first:ml-auto mr-auto m-4 w-[170px] h-[275px] flex flex-col shrink-0 rounded-2xl relative bg-gray-50/50 bg-no-repeat shadow-character" v-if="characters.length < 4">
|
<div class="character new-character first:ml-auto mr-auto m-4 w-[170px] h-[275px] flex flex-col shrink-0 rounded-2xl relative bg-gray-500/50 bg-no-repeat shadow-character" v-if="characters.length < 4">
|
||||||
<button class="h-full w-full py-10 flex flex-col justify-between" @click="isModalOpen = true">
|
<button class="h-full w-full py-10 flex flex-col justify-between" @click="isModalOpen = true">
|
||||||
<div class="filler"></div>
|
<div class="filler"></div>
|
||||||
<img class="w-24 h-24 m-auto" draggable="false" src="/assets/icons/plus-icon.svg" />
|
<img class="w-24 h-24 m-auto" draggable="false" src="/assets/icons/plus-icon.svg" />
|
||||||
<span class="self-center text-base absolute bottom-5 w-full text-center translate-y-1/2 z-10 drop-shadow-text">Create new</span>
|
<span class="self-center text-base absolute bottom-5 w-full text-center translate-y-1/2 z-10">Create new</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -46,13 +48,13 @@
|
|||||||
|
|
||||||
<div class="button-wrapper flex gap-8" v-if="!isLoading">
|
<div class="button-wrapper flex gap-8" v-if="!isLoading">
|
||||||
<button
|
<button
|
||||||
class="btn-bordeaux py-2 pr-2.5 pl-8 min-w-24 relative rounded text-xl flex gap-4 items-center transition-all ease-in-out duration-200 hover:gap-5 disabled:bg-cyan/50 disabled:hover:bg-opacity-50 disabled:cursor-not-allowed disabled:hover:gap-[15px]"
|
class="btn-red py-2 pr-2.5 pl-8 min-w-24 relative rounded text-xl flex gap-4 items-center transition-all ease-in-out duration-200 hover:gap-5 disabled:bg-red/50 disabled:hover:bg-opacity-50 disabled:cursor-not-allowed disabled:hover:gap-[15px]"
|
||||||
@click.stop="gameStore.disconnectSocket()"
|
@click.stop="gameStore.disconnectSocket()"
|
||||||
>
|
>
|
||||||
<img class="h-8 drop-shadow-20 rotate-180" draggable="false" src="/assets/icons/arrow.svg" alt="Logout icon" />
|
<img class="h-8 drop-shadow-20 rotate-180" draggable="false" src="/assets/icons/arrow.svg" alt="Logout icon" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn-cyan py-2 pr-2.5 pl-8 min-w-24 relative rounded text-xl flex gap-4 items-center transition-all ease-in-out duration-200 hover:gap-5 disabled:bg-cyan/50 disabled:hover:bg-opacity-50 disabled:cursor-not-allowed disabled:hover:gap-[15px]"
|
class="btn-cyan py-2 px-2.5 pl-8 min-w-24 relative rounded text-xl flex gap-4 items-center transition-all ease-in-out duration-200 hover:gap-5 disabled:bg-cyan-800 disabled:hover:bg-opacity-50 disabled:cursor-not-allowed disabled:hover:gap-[15px]"
|
||||||
:disabled="!selected_character"
|
:disabled="!selected_character"
|
||||||
@click="select_character()"
|
@click="select_character()"
|
||||||
>
|
>
|
||||||
@ -66,7 +68,7 @@
|
|||||||
<!-- CREATE CHARACTER MODAL -->
|
<!-- CREATE CHARACTER MODAL -->
|
||||||
<Modal :isModalOpen="isModalOpen" @modal:close="isModalOpen = false">
|
<Modal :isModalOpen="isModalOpen" @modal:close="isModalOpen = false">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="m-0 font-medium">Create your character</h3>
|
<h3 class="m-0 font-medium text-gray-300">Create your character</h3>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #modalBody>
|
<template #modalBody>
|
||||||
@ -74,7 +76,7 @@
|
|||||||
<form method="post" @submit.prevent="create" class="inline">
|
<form method="post" @submit.prevent="create" class="inline">
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="name">Name</label>
|
<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>
|
||||||
<button class="btn-cyan py-1.5 px-4 mr-5 min-w-24 inline-block" type="submit">CREATE</button>
|
<button class="btn-cyan py-1.5 px-4 mr-5 min-w-24 inline-block" type="submit">CREATE</button>
|
||||||
</form>
|
</form>
|
||||||
@ -86,7 +88,7 @@
|
|||||||
<!-- DELETE CHARACTER MODAL -->
|
<!-- DELETE CHARACTER MODAL -->
|
||||||
<ConfirmationModal v-if="deletingCharacter != null" :confirm-function="delete_character.bind(this, deletingCharacter.id)" :cancel-function="(() => (deletingCharacter = null)).bind(this)" confirm-button-text="Delete">
|
<ConfirmationModal v-if="deletingCharacter != null" :confirm-function="delete_character.bind(this, deletingCharacter.id)" :cancel-function="(() => (deletingCharacter = null)).bind(this)" confirm-button-text="Delete">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="m-0 font-medium">Deleting character</h3>
|
<h3 class="m-0 font-medium text-gray-300">Deleting character</h3>
|
||||||
</template>
|
</template>
|
||||||
<template #modalBody>
|
<template #modalBody>
|
||||||
You are about to delete <span class="font-extrabold">{{ deletingCharacter.name }}</span
|
You are about to delete <span class="font-extrabold">{{ deletingCharacter.name }}</span
|
||||||
|
@ -1,25 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex justify-center items-center h-dvh relative">
|
<div class="flex justify-center items-center h-dvh relative">
|
||||||
<GmTools v-if="isLoaded && gameStore.character?.role === 'gm'" />
|
<GmTools v-if="gameStore.character?.role === 'gm'" />
|
||||||
<GmPanel v-if="isLoaded && gameStore.character?.role === 'gm'" />
|
<GmPanel v-if="gameStore.character?.role === 'gm'" />
|
||||||
<Inventory />
|
|
||||||
|
|
||||||
<div v-if="!zoneEditorStore.active">
|
<div v-if="!zoneEditorStore.active">
|
||||||
<Game :config="gameConfig" @create="createGame" class="111mt-[-60px]">
|
<Game :config="gameConfig" @create="createGame">
|
||||||
<Scene name="main" @preload="preloadScene" @create="createScene">
|
<Scene name="main" @preload="preloadScene" @create="createScene">
|
||||||
<div class="fixed inset-x-0 top-0 flex justify-start items-end p-10 pointer-events-none">
|
<div v-if="isLoaded">
|
||||||
<div class="pointer-events-auto">
|
<Menu />
|
||||||
<Hud />
|
<Hud />
|
||||||
</div>
|
<Keybindings />
|
||||||
</div>
|
<Minimap />
|
||||||
<Zone v-if="isLoaded" />
|
<Zone />
|
||||||
<div class="fixed inset-x-0 bottom-0 flex justify-between gap-5 items-end py-10 px-5 xxs:p-10 pointer-events-none max-md:flex-wrap max-md:flex-col-reverse">
|
<Chat />
|
||||||
<div class="pointer-events-auto w-full">
|
<Inventory />
|
||||||
<Chat />
|
<ExpBar />
|
||||||
</div>
|
|
||||||
<div class="pointer-events-auto max-xs:m-auto mr-auto">
|
<Effects />
|
||||||
<Menubar />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Scene>
|
</Scene>
|
||||||
</Game>
|
</Game>
|
||||||
@ -27,7 +24,7 @@
|
|||||||
<div v-if="zoneEditorStore.active">
|
<div v-if="zoneEditorStore.active">
|
||||||
<Game :config="gameConfig" @create="createGame">
|
<Game :config="gameConfig" @create="createGame">
|
||||||
<Scene name="main" @preload="preloadScene" @create="createScene">
|
<Scene name="main" @preload="preloadScene" @create="createScene">
|
||||||
<ZoneEditor v-if="isLoaded" :key="JSON.stringify(zoneEditorStore.zone)" />
|
<ZoneEditor v-if="isLoaded" :key="JSON.stringify(`${zoneEditorStore.zone?.id}_${zoneEditorStore.zone?.createdAt}_${zoneEditorStore.zone?.updatedAt}`)" />
|
||||||
</Scene>
|
</Scene>
|
||||||
</Game>
|
</Game>
|
||||||
</div>
|
</div>
|
||||||
@ -40,15 +37,20 @@ import { ref, onBeforeUnmount } from 'vue'
|
|||||||
import { Game, Scene } from 'phavuer'
|
import { Game, Scene } from 'phavuer'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
|
import Menu from '@/components/gui/Menu.vue'
|
||||||
|
import ExpBar from '@/components/gui/ExpBar.vue'
|
||||||
import Hud from '@/components/gui/Hud.vue'
|
import Hud from '@/components/gui/Hud.vue'
|
||||||
import Zone from '@/components/zone/Zone.vue'
|
import Zone from '@/components/zone/Zone.vue'
|
||||||
|
import Keybindings from '@/components/gui/Keybindings.vue'
|
||||||
import Chat from '@/components/gui/Chat.vue'
|
import Chat from '@/components/gui/Chat.vue'
|
||||||
import Menubar from '@/components/gui/Menu.vue'
|
|
||||||
import GmTools from '@/components/gameMaster/GmTools.vue'
|
import GmTools from '@/components/gameMaster/GmTools.vue'
|
||||||
import ZoneEditor from '@/components/gameMaster/zoneEditor/ZoneEditor.vue'
|
import ZoneEditor from '@/components/gameMaster/zoneEditor/ZoneEditor.vue'
|
||||||
import GmPanel from '@/components/gameMaster/GmPanel.vue'
|
import GmPanel from '@/components/gameMaster/GmPanel.vue'
|
||||||
import Inventory from '@/components/gui/UserPanel.vue'
|
import Inventory from '@/components/gui/UserPanel.vue'
|
||||||
|
import Effects from '@/components/Effects.vue'
|
||||||
import { loadAssets } from '@/composables/zoneComposable'
|
import { loadAssets } from '@/composables/zoneComposable'
|
||||||
|
import Minimap from '@/components/gui/Minimap.vue'
|
||||||
|
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
@ -59,14 +61,7 @@ const gameConfig = {
|
|||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
height: window.innerHeight,
|
height: window.innerHeight,
|
||||||
type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS
|
type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS
|
||||||
scale: {
|
resolution: 5
|
||||||
mode: Phaser.Scale.RESIZE,
|
|
||||||
autoCenter: Phaser.Scale.CENTER_BOTH,
|
|
||||||
width: window.innerWidth,
|
|
||||||
height: window.innerHeight
|
|
||||||
},
|
|
||||||
resolution: 2,
|
|
||||||
pixelArt: true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const createGame = (game: Phaser.Game) => {
|
const createGame = (game: Phaser.Game) => {
|
||||||
@ -76,9 +71,20 @@ const createGame = (game: Phaser.Game) => {
|
|||||||
addEventListener('resize', () => {
|
addEventListener('resize', () => {
|
||||||
game.scale.resize(window.innerWidth, window.innerHeight)
|
game.scale.resize(window.innerWidth, window.innerHeight)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// We don't support canvas mode, only WebGL
|
||||||
|
if (game.renderer.type === Phaser.CANVAS) {
|
||||||
|
gameStore.addNotification({
|
||||||
|
title: 'Warning',
|
||||||
|
message: 'Your browser does not support WebGL. Please use a modern browser like Chrome, Firefox, or Edge.'
|
||||||
|
})
|
||||||
|
gameStore.disconnectSocket()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const preloadScene = async (scene: Phaser.Scene) => {
|
const preloadScene = async (scene: Phaser.Scene) => {
|
||||||
|
isLoaded.value = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create loading bar
|
* Create loading bar
|
||||||
*/
|
*/
|
||||||
@ -122,10 +128,6 @@ const preloadScene = async (scene: Phaser.Scene) => {
|
|||||||
scene.load.image('blank_tile', '/assets/zone/blank_tile.png')
|
scene.load.image('blank_tile', '/assets/zone/blank_tile.png')
|
||||||
scene.load.image('blank_object', '/assets/zone/blank_tile.png')
|
scene.load.image('blank_object', '/assets/zone/blank_tile.png')
|
||||||
scene.load.image('waypoint', '/assets/waypoint.png')
|
scene.load.image('waypoint', '/assets/waypoint.png')
|
||||||
scene.textures.addBase64(
|
|
||||||
'character',
|
|
||||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAABeCAYAAAAwnXTzAAAHWUlEQVR4nLVaQUhcRxj+DCuILruHXdbnYkBimkdFdouwFBdWKCWHHkoChragmAo9pKG3ltQGWrGHkEpzS0t7Mgn2EiIYcvQiLurBIF0RYYtbFiK6Lhqyiy4BBXt4+8/OzHvz3ryN/eCx782bN9/8//z/P//MbMvHP/wJXXTMTJ45lR+PT7XothHwQzQ8OIBUpt/2fmLaeq9D7EnYMTN5Njw4ALM3iFC8B0Y6ifZYWKjzKJ1EaSWHienJMy/SCzpkqUy/QNZqJISrPRaGkU7i/p0xpdq1CAHA7A0CAIx0UlnHD6mSkKQLxXu8+iTArWOuhCrUyhWclDZc67hJqWWlTqTt2LCV6aApQj8EMjxVWt0tAgBOD8tNEcjwlDBfOAIKm/jE7NJq8Mvr374b4affjwAAXvzyF7uXG300/4Ddz++8wkj3RRwr2vNUaWklBwBIZfqZWud3XmF+5xU66vd+oCQ8Hp9qmVtdx1p2k5Ee5PdweljGm5dPWb2R7ovCd29ePsWPd8aUhFpWupbdZLH0IL+HqGk1/BunSjm+quCqUpISqBtPHQf5PUZCF8HLXTzHkCddy26y8tPDMmrliu06PSxjYvqJcqrSDm0yKY2nfJH0KmhHmuHBAUZKSDnUc5POFyGRAg1p5Q4A3rO+L0JKL1KZflR3i8gXjpj1eklGaHFLovhchhpWobpbxL3ZJfasIneUkCciEBmlGbVyhQUEI52EgSTux3uY5HMzzvmNjZBmegCOUhEJNQwA5m6R1QvFe5CKW3WdSAVCPmkCgEvXh5gkcupgIInQSg5VjozqlFZyVi60Kovj4IeUNF26PsSSI0I4kWEXJU2yqsOJDMxb3yAU78Hw4IAt1WCEfNJkpJNoNRIArIwsWp8L+VyG3vOgMqpHnedhG8Oo2SVIxZM45TI8qC7FU0v6JaGOoFKzN4hAJCb0tFausDBGjfFxk1BaybFyoJGayGoVCGWLlBtVxU1qvLSSw+lhGaWVHPKFI8cc1aZSUiep5yC/h7XsJlIAG0sqB7gZpFD/zW5ibnUd9++MoT0WtlmrMrSRdGv1BlKZfmEmICLZMPh508mwAoDo7ABQ2cgyKeZW1zE8OCDMhYAVT6NSJneQ32tIqoBSQpKOwEedqNmFQCQGACzMWeUApI7JlsoIzd4gomYXUyUvXePDBhkt2wAgbNQtmiMiQ5LBrFS2UKcPeMnk8XEaL6dFj+AW1NhBfk8YfBmqDI0vzxeOUCtXYKSTgi8yQjnC6OKktGGLMG4QJHRSixcZRReybN5NaCrjy3wvSAF/SzVZa4yQxo8wt7ou5KR+iem7ViMhGKRNwtPDstKk6T0gWiCpToZTO4ywGYNxa5hwUtoQgrgt0ni5BFBP87nnKzeusXIvMEK/FqrTeL5whCvliqA9bSut7hY91w0q1MoV5vwX5JnCrbdEqiLmh0O2cPJF2xhWd4vMJQDgqK8PjytvrZfrb4H1A9wMtwHcIpW+yxeOrLp9faBk9J9nz3HlxjV8cHsMmF2yCPlI4GQwV69eFZ4fLywIHZDrLtB7au/Zc6R+/rUhoc5+WmdnJ7sfHR1V1tvf3xeeP39wG68Xl7H203cNQn7mdsqYebLLly8DALq7u21kOzs7trLXi8vo/OJrtG1kgdklu5W6SetGRuVUh7DwIoeT0gbCiQwAzi0oVMnz17uALPXv35+w9m0ShhMZtuOkgqy6xcVFdr+8vGwjvTe7hM/SX+F4fKolAFj+E4jEEDaA2ZFbglsAliF0dnZie3ubqYxIt7e3Gen+/j4WFhYQ3NrC8YciKd0HAMuHDFgBViYLbm2BjFx2D75D5Ao3w22Yc9FOALB85dJhWbnTezPcBoDzP0UdszfoGfgDx+NTLXMzk2epTD9C77+nrGj2BvEw3jizoJyVUslGYrzquBAlMKORM2uZLBTvQdTsYhd/YEI5rQ4uAA3zdUsZqMFAJIZAJCYQ+Nn5F9xClSoQKOMOJzIIRGKOK1xtwuPxqRbV5g4vQauRUBqXnIi5EhKpfl/9k9kIdeB1SHLuhEDzZxZNEXqReTm+L0J5g6EZ+D4KciNqj4VtsViGloRyiuhkkfyK2A2+JSTnr0Ecr3/nl2CkvY3JFyG/tpe3wKq7RVSfFT3b8GU0tL1FC1GnTQfPTvshBKzEFnA2f6fNPBm+/VCWxG8A9yWhMOdJO06+5kMv0OTLuwMvmfzODdoS8u4ANLdsA5oM3u+CcyM8l/PD/wPnRqi7R+CL0K1R3Uzg3CTUzQLOhZCOGXR2ObT+wENwU5vuloq2hHTI7JVa3B0dQsfM5JlqQav9jyEi9UIo3oO7o0PKVbRWaNMJzHydqNmFUHwPZm8Q96QzRF+zhXz0I4PPCAKRrLXInRXnx6anJ1re5QtHMFGsvwdqANph7VrQ3oxvQuv/F43zishHwzBvAak/HmJi+kl9Aeo807seyTqBl8BIW3PeSWlDiDrn9ldBWo4PYwAmrKyskWIsue4Eq+B6jk9w2yTyu8T7D0vv92u9uVoPAAAAAElFTkSuQmCC'
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the assets into the Phaser scene
|
* Load the assets into the Phaser scene
|
||||||
@ -144,14 +146,22 @@ const createScene = async (scene: Phaser.Scene) => {
|
|||||||
scene.anims.create({
|
scene.anims.create({
|
||||||
key: asset.key,
|
key: asset.key,
|
||||||
frameRate: 7,
|
frameRate: 7,
|
||||||
/** @TODO: Fix end, which is total amount of frames */
|
frames: scene.anims.generateFrameNumbers(asset.key, { start: 0, end: asset.frameCount! - 1 }),
|
||||||
frames: scene.anims.generateFrameNumbers(asset.key, { start: 0, end: 4 }),
|
|
||||||
repeat: -1
|
repeat: -1
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
isLoaded.value = false
|
||||||
gameStore.disconnectSocket()
|
gameStore.disconnectSocket()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
canvas {
|
||||||
|
image-rendering: -moz-crisp-edges;
|
||||||
|
image-rendering: -webkit-crisp-edges;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -1,28 +1,66 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bg-gray-300">
|
<div class="relative max-lg:h-dvh">
|
||||||
<div class="absolute bg-[url('/assets/shapes/select-screen-bg-shape.svg')] bg-no-repeat bg-center w-full h-full z-10 pointer-events-none"></div>
|
<div class="lg:bg-gradient-to-r bg-gradient-to-b from-gray-900 to-transparent w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute right-0 max-lg:bottom-0 lg:top-0 z-10"></div>
|
||||||
<div class="z-20 w-full h-dvh flex items-center justify-between flex-col relative">
|
<div class="bg-[url('/assets/login/login-bg.png')] w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute right-0 max-lg:bottom-0 lg:top-0 bg-no-repeat bg-cover bg-center"></div>
|
||||||
<div class="filler"></div>
|
<div class="bg-gray-900 z-20 w-full lg:w-1/2 h-[65dvh] lg:h-dvh relative">
|
||||||
<h1 class="mt-28 text-center text-6xl">NEW QUEST</h1>
|
<div class="h-dvh flex items-center lg:justify-center flex-col px-8 max-lg:pt-20">
|
||||||
<form @submit.prevent="loginFunc">
|
<img src="/assets/login/nq-logo-v1.png" class="mb-10" />
|
||||||
<div class="my-20 mx-0 w-full flex flex-col gap-6">
|
<div class="relative">
|
||||||
<div class="w-full grid gap-4">
|
<img src="/assets/login/login-box-outer.svg" class="absolute w-full h-full max-lg:hidden" />
|
||||||
<div class="flex flex-col bg-white/50 rounded-[3px] border border-solid border-gray-50 sm:min-w-[500px] sm:w-unset w-full my-0 mx-auto">
|
<img src="/assets/login/login-box-inner.svg" class="absolute left-2 top-2 w-[calc(100%_-_16px)] h-[calc(100%_-_16px)] max-lg:hidden" />
|
||||||
<label class="text-black bg-white/50 p-1 text-sm rounded-t-[3px]" for="username">Username</label>
|
|
||||||
<input class="p-1 text-sm focus-visible:outline-none" id="username" v-model="username" type="text" name="username" required autofocus />
|
<!-- Login Form -->
|
||||||
|
<form v-show="switchForm === 'login'" @submit.prevent="loginFunc" class="relative px-6 py-11">
|
||||||
|
<div class="flex flex-col gap-5 p-2 mb-8 relative">
|
||||||
|
<div class="w-full grid gap-3 relative">
|
||||||
|
<input class="input-field xs:min-w-[350px] min-w-64" id="username" v-model="username" type="text" name="username" placeholder="Username" required autofocus />
|
||||||
|
<div class="relative">
|
||||||
|
<input class="input-field xs:min-w-[350px] min-w-64" id="password" v-model="password" type="password" name="password" placeholder="Password" required />
|
||||||
|
<button class="p-0 absolute right-3 w-4 h-3 top-1/2 -translate-y-1/2 bg-[url('/assets/icons/eye.svg')] bg-no-repeat"></button>
|
||||||
|
</div>
|
||||||
|
<span v-if="loginError" class="text-red-200 text-xs absolute top-full mt-1">{{ loginError }}</span>
|
||||||
|
</div>
|
||||||
|
<button class="text-right text-cyan-300 text-base">Forgot password?</button>
|
||||||
|
<button class="btn-cyan px-0 xs:w-full" type="submit">Play now</button>
|
||||||
|
|
||||||
|
<!-- Divider shape -->
|
||||||
|
<div class="absolute w-40 h-0.5 -bottom-8 left-1/2 -translate-x-1/2 flex justify-between">
|
||||||
|
<div class="w-0.5 h-full bg-gray-300"></div>
|
||||||
|
<div class="w-36 h-full bg-gray-300"></div>
|
||||||
|
<div class="w-0.5 h-full bg-gray-300"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col bg-white/50 rounded-[3px] border border-solid border-gray-50 sm:min-w-[500px] sm:w-unset w-full my-0 mx-auto">
|
<div class="pt-8">
|
||||||
<label class="text-black bg-white/50 p-1 text-sm rounded-t-[3px]" for="password">Password</label>
|
<p class="m-0 text-center">Don't have an account? <button class="text-cyan-300 text-base p-0" @click.prevent="switchForm = 'register'">Sign up</button></p>
|
||||||
<input class="p-1 text-sm focus-visible:outline-none" id="password" v-model="password" type="password" name="password" required />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
<div class="flex justify-center sm:gap-4 gap-2">
|
|
||||||
<button class="btn-cyan py-2 px-0 min-w-24" type="submit"><span class="m-auto">PLAY</span></button>
|
<!-- Register Form -->
|
||||||
<button class="btn-cyan py-2 px-0 min-w-24" type="button" @click.prevent="registerFunc"><span class="m-auto">REGISTER</span></button>
|
<form v-show="switchForm === 'register'" @submit.prevent="registerFunc" class="relative px-6 py-11">
|
||||||
<button class="btn-cyan py-2 px-0 min-w-24"><span class="m-auto">CREDITS</span></button>
|
<div class="flex flex-col gap-5 p-2 mb-8 relative">
|
||||||
</div>
|
<div class="w-full grid gap-3 relative">
|
||||||
|
<input class="input-field xs:min-w-[350px] min-w-64" id="username" v-model="username" type="text" name="username" placeholder="Username" required autofocus />
|
||||||
|
<div class="relative">
|
||||||
|
<input class="input-field xs:min-w-[350px] min-w-64" id="password" v-model="password" type="password" name="password" placeholder="Password" required />
|
||||||
|
<button class="p-0 absolute right-3 w-4 h-3 top-1/2 -translate-y-1/2 bg-[url('/assets/icons/eye.svg')] bg-no-repeat"></button>
|
||||||
|
</div>
|
||||||
|
<span v-if="loginError" class="text-red-200 text-xs absolute top-full mt-1">{{ loginError }}</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn-cyan xs:w-full" type="submit">Register now</button>
|
||||||
|
|
||||||
|
<!-- Divider shape -->
|
||||||
|
<div class="absolute w-40 h-0.5 -bottom-8 left-1/2 -translate-x-1/2 flex justify-between">
|
||||||
|
<div class="w-0.5 h-full bg-gray-300"></div>
|
||||||
|
<div class="w-36 h-full bg-gray-300"></div>
|
||||||
|
<div class="w-0.5 h-full bg-gray-300"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pt-8">
|
||||||
|
<p class="m-0 text-center">Already have an account? <button class="text-cyan-300 text-base p-0" @click.prevent="switchForm = 'login'">Log in</button></p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -36,7 +74,8 @@ import { useCookies } from '@vueuse/integrations/useCookies'
|
|||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const username = ref('')
|
const username = ref('')
|
||||||
const password = ref('')
|
const password = ref('')
|
||||||
|
const switchForm = ref('login')
|
||||||
|
const loginError = ref('')
|
||||||
// automatic login because of development
|
// automatic login because of development
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const token = useCookies().get('token')
|
const token = useCookies().get('token')
|
||||||
@ -49,7 +88,7 @@ onMounted(async () => {
|
|||||||
async function loginFunc() {
|
async function loginFunc() {
|
||||||
// check if username and password are valid
|
// check if username and password are valid
|
||||||
if (username.value === '' || password.value === '') {
|
if (username.value === '' || password.value === '') {
|
||||||
gameStore.addNotification({ message: 'Please enter a valid username and password' })
|
loginError.value = 'Please enter a valid username and password'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,10 +96,9 @@ async function loginFunc() {
|
|||||||
const response = await login(username.value, password.value)
|
const response = await login(username.value, password.value)
|
||||||
|
|
||||||
if (response.success === undefined) {
|
if (response.success === undefined) {
|
||||||
gameStore.addNotification({ message: response.error })
|
loginError.value = response.error
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.setToken(response.token)
|
gameStore.setToken(response.token)
|
||||||
gameStore.initConnection()
|
gameStore.initConnection()
|
||||||
return true // Indicate success
|
return true // Indicate success
|
||||||
@ -69,7 +107,7 @@ async function loginFunc() {
|
|||||||
async function registerFunc() {
|
async function registerFunc() {
|
||||||
// check if username and password are valid
|
// check if username and password are valid
|
||||||
if (username.value === '' || password.value === '') {
|
if (username.value === '' || password.value === '') {
|
||||||
gameStore.addNotification({ message: 'Please enter a valid username and password' })
|
loginError.value = 'Please enter a valid username and password'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,13 +115,14 @@ async function registerFunc() {
|
|||||||
const response = await register(username.value, password.value)
|
const response = await register(username.value, password.value)
|
||||||
|
|
||||||
if (response.success === undefined) {
|
if (response.success === undefined) {
|
||||||
gameStore.addNotification({ message: response.error })
|
loginError.value = response.error
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const loginSuccess = await loginFunc()
|
const loginSuccess = await loginFunc()
|
||||||
if (!loginSuccess) {
|
if (!loginSuccess) {
|
||||||
gameStore.addNotification({ message: 'Login after registration failed. Please try logging in manually.' })
|
loginError.value = 'Login after registration failed. Please try logging in manually.'
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -5,19 +5,26 @@ import config from '@/config'
|
|||||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||||
|
|
||||||
export const useGameStore = defineStore('game', {
|
export const useGameStore = defineStore('game', {
|
||||||
state: () => ({
|
state: () => {
|
||||||
notifications: [] as Notification[],
|
return {
|
||||||
assets: [] as Asset[],
|
loginMessage: null as string | null,
|
||||||
token: '' as string | null,
|
notifications: [] as Notification[],
|
||||||
connection: null as Socket | null,
|
assets: [] as Asset[],
|
||||||
user: null as User | null,
|
token: '' as string | null,
|
||||||
character: null as Character | null,
|
connection: null as Socket | null,
|
||||||
isGmPanelOpen: false,
|
user: null as User | null,
|
||||||
isPlayerDraggingCamera: false,
|
character: null as Character | null,
|
||||||
isCameraFollowingCharacter: false,
|
isPlayerDraggingCamera: false,
|
||||||
isChatOpen: false,
|
gameSettings: {
|
||||||
isUserPanelOpen: false
|
isCameraFollowingCharacter: false
|
||||||
}),
|
},
|
||||||
|
uiSettings: {
|
||||||
|
isChatOpen: false,
|
||||||
|
isUserPanelOpen: false,
|
||||||
|
isGmPanelOpen: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
getters: {
|
getters: {
|
||||||
getNotifications: (state: any) => state.notifications,
|
getNotifications: (state: any) => state.notifications,
|
||||||
getAssetByKey: (state) => {
|
getAssetByKey: (state) => {
|
||||||
@ -92,7 +99,7 @@ export const useGameStore = defineStore('game', {
|
|||||||
this.character = character
|
this.character = character
|
||||||
},
|
},
|
||||||
toggleGmPanel() {
|
toggleGmPanel() {
|
||||||
this.isGmPanelOpen = !this.isGmPanelOpen
|
this.uiSettings.isGmPanelOpen = !this.uiSettings.isGmPanelOpen
|
||||||
},
|
},
|
||||||
togglePlayerDraggingCamera() {
|
togglePlayerDraggingCamera() {
|
||||||
this.isPlayerDraggingCamera = !this.isPlayerDraggingCamera
|
this.isPlayerDraggingCamera = !this.isPlayerDraggingCamera
|
||||||
@ -101,16 +108,16 @@ export const useGameStore = defineStore('game', {
|
|||||||
this.isPlayerDraggingCamera = moving
|
this.isPlayerDraggingCamera = moving
|
||||||
},
|
},
|
||||||
toggleCameraFollowingCharacter() {
|
toggleCameraFollowingCharacter() {
|
||||||
this.isCameraFollowingCharacter = !this.isCameraFollowingCharacter
|
this.gameSettings.isCameraFollowingCharacter = !this.gameSettings.isCameraFollowingCharacter
|
||||||
},
|
},
|
||||||
setCameraFollowingCharacter(following: boolean) {
|
setCameraFollowingCharacter(following: boolean) {
|
||||||
this.isCameraFollowingCharacter = following
|
this.gameSettings.isCameraFollowingCharacter = following
|
||||||
},
|
},
|
||||||
toggleChat() {
|
toggleChat() {
|
||||||
this.isChatOpen = !this.isChatOpen
|
this.uiSettings.isChatOpen = !this.uiSettings.isChatOpen
|
||||||
},
|
},
|
||||||
toggleUserPanel() {
|
toggleUserPanel() {
|
||||||
this.isUserPanelOpen = !this.isUserPanelOpen
|
this.uiSettings.isUserPanelOpen = !this.uiSettings.isUserPanelOpen
|
||||||
},
|
},
|
||||||
initConnection() {
|
initConnection() {
|
||||||
this.connection = io(config.server_endpoint, {
|
this.connection = io(config.server_endpoint, {
|
||||||
@ -151,10 +158,11 @@ export const useGameStore = defineStore('game', {
|
|||||||
this.token = null
|
this.token = null
|
||||||
this.user = null
|
this.user = null
|
||||||
this.character = null
|
this.character = null
|
||||||
this.isGmPanelOpen = false
|
this.uiSettings.isGmPanelOpen = false
|
||||||
this.isPlayerDraggingCamera = false
|
this.isPlayerDraggingCamera = false
|
||||||
this.isChatOpen = false
|
this.gameSettings.isCameraFollowingCharacter = false
|
||||||
this.isUserPanelOpen = false
|
this.uiSettings.isChatOpen = false
|
||||||
|
this.uiSettings.isUserPanelOpen = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import type { Zone, Object, Tile, ZoneObject } from '@/types'
|
import type { Zone, Object, Tile, ZoneObject, ZoneEffects } from '@/types'
|
||||||
|
|
||||||
type TeleportSettings = {
|
export type TeleportSettings = {
|
||||||
toZoneId: number
|
toZoneId: number
|
||||||
toPositionX: number
|
toPositionX: number
|
||||||
toPositionY: number
|
toPositionY: number
|
||||||
@ -10,37 +10,40 @@ type TeleportSettings = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useZoneEditorStore = defineStore('zoneEditor', {
|
export const useZoneEditorStore = defineStore('zoneEditor', {
|
||||||
state: () => ({
|
state: () => {
|
||||||
active: false,
|
return {
|
||||||
zone: null as Zone | null,
|
active: false,
|
||||||
tool: 'move',
|
zone: null as Zone | null,
|
||||||
drawMode: 'tile',
|
tool: 'move',
|
||||||
eraserMode: 'tile',
|
drawMode: 'tile',
|
||||||
zoneList: [] as Zone[],
|
eraserMode: 'tile',
|
||||||
tileList: [] as Tile[],
|
zoneList: [] as Zone[],
|
||||||
objectList: [] as Object[],
|
tileList: [] as Tile[],
|
||||||
selectedTile: null as Tile | null,
|
objectList: [] as Object[],
|
||||||
selectedObject: null as Object | null,
|
selectedTile: null as Tile | null,
|
||||||
selectedZoneObject: null as ZoneObject | null,
|
selectedObject: null as Object | null,
|
||||||
objectDepth: 0,
|
selectedZoneObject: null as ZoneObject | null,
|
||||||
isTileListModalShown: false,
|
objectDepth: 0,
|
||||||
isObjectListModalShown: false,
|
isTileListModalShown: false,
|
||||||
isZoneListModalShown: false,
|
isObjectListModalShown: false,
|
||||||
isCreateZoneModalShown: false,
|
isZoneListModalShown: false,
|
||||||
isSettingsModalShown: false,
|
isCreateZoneModalShown: false,
|
||||||
zoneSettings: {
|
isSettingsModalShown: false,
|
||||||
name: '',
|
zoneSettings: {
|
||||||
width: 0,
|
name: '',
|
||||||
height: 0,
|
width: 0,
|
||||||
pvp: false
|
height: 0,
|
||||||
},
|
pvp: false,
|
||||||
teleportSettings: {
|
effects: [] as ZoneEffects[]
|
||||||
toZoneId: 0,
|
},
|
||||||
toPositionX: 0,
|
teleportSettings: {
|
||||||
toPositionY: 0,
|
toZoneId: 0,
|
||||||
toRotation: 0
|
toPositionX: 0,
|
||||||
|
toPositionY: 0,
|
||||||
|
toRotation: 0
|
||||||
|
} as TeleportSettings
|
||||||
}
|
}
|
||||||
}),
|
},
|
||||||
actions: {
|
actions: {
|
||||||
toggleActive() {
|
toggleActive() {
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
@ -64,6 +67,10 @@ export const useZoneEditorStore = defineStore('zoneEditor', {
|
|||||||
if (!this.zone) return
|
if (!this.zone) return
|
||||||
this.zone.pvp = pvp
|
this.zone.pvp = pvp
|
||||||
},
|
},
|
||||||
|
setZoneEffects(zoneEffects: ZoneEffects) {
|
||||||
|
if (!this.zone) return
|
||||||
|
this.zone.zoneEffects = zoneEffects
|
||||||
|
},
|
||||||
setTool(tool: string) {
|
setTool(tool: string) {
|
||||||
this.tool = tool
|
this.tool = tool
|
||||||
},
|
},
|
||||||
|
@ -2,11 +2,13 @@ import { defineStore } from 'pinia'
|
|||||||
import type { ExtendedCharacter, Zone } from '@/types'
|
import type { ExtendedCharacter, Zone } from '@/types'
|
||||||
|
|
||||||
export const useZoneStore = defineStore('zone', {
|
export const useZoneStore = defineStore('zone', {
|
||||||
state: () => ({
|
state: () => {
|
||||||
zone: null as Zone | null,
|
return {
|
||||||
characters: [] as ExtendedCharacter[],
|
zone: null as Zone | null,
|
||||||
characterLoaded: false,
|
characters: [] as ExtendedCharacter[],
|
||||||
}),
|
characterLoaded: false
|
||||||
|
}
|
||||||
|
},
|
||||||
getters: {
|
getters: {
|
||||||
getCharacterById: (state) => {
|
getCharacterById: (state) => {
|
||||||
return (id: number) => state.characters.find((char) => char.id === id)
|
return (id: number) => state.characters.find((char) => char.id === id)
|
||||||
@ -37,9 +39,13 @@ export const useZoneStore = defineStore('zone', {
|
|||||||
removeCharacter(character_id: number) {
|
removeCharacter(character_id: number) {
|
||||||
this.characters = this.characters.filter((char) => char.id !== character_id)
|
this.characters = this.characters.filter((char) => char.id !== character_id)
|
||||||
},
|
},
|
||||||
|
setCharacterLoaded(loaded: boolean) {
|
||||||
|
this.characterLoaded = loaded
|
||||||
|
},
|
||||||
reset() {
|
reset() {
|
||||||
this.zone = null
|
this.zone = null
|
||||||
this.characters = []
|
this.characters = []
|
||||||
|
this.characterLoaded = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
12
src/types.ts
@ -1,5 +1,6 @@
|
|||||||
export type Notification = {
|
export type Notification = {
|
||||||
id?: string
|
id?: string
|
||||||
|
title?: string
|
||||||
message?: string
|
message?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -7,6 +8,7 @@ export type Asset = {
|
|||||||
key: string
|
key: string
|
||||||
url: string
|
url: string
|
||||||
group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
|
group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
|
||||||
|
frameCount?: number
|
||||||
frameWidth?: number
|
frameWidth?: number
|
||||||
frameHeight?: number
|
frameHeight?: number
|
||||||
}
|
}
|
||||||
@ -51,6 +53,7 @@ export type Zone = {
|
|||||||
height: number
|
height: number
|
||||||
tiles: any | null
|
tiles: any | null
|
||||||
pvp: boolean
|
pvp: boolean
|
||||||
|
zoneEffects: ZoneEffects
|
||||||
zoneEventTiles: ZoneEventTile[]
|
zoneEventTiles: ZoneEventTile[]
|
||||||
zoneObjects: ZoneObject[]
|
zoneObjects: ZoneObject[]
|
||||||
characters: Character[]
|
characters: Character[]
|
||||||
@ -59,6 +62,14 @@ export type Zone = {
|
|||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ZoneEffects = {
|
||||||
|
id: string
|
||||||
|
zoneId: number
|
||||||
|
zone: Zone
|
||||||
|
effect: string
|
||||||
|
strength: number
|
||||||
|
}
|
||||||
|
|
||||||
export type ZoneObject = {
|
export type ZoneObject = {
|
||||||
id: string
|
id: string
|
||||||
zoneId: number
|
zoneId: number
|
||||||
@ -66,6 +77,7 @@ export type ZoneObject = {
|
|||||||
objectId: string
|
objectId: string
|
||||||
object: Object
|
object: Object
|
||||||
depth: number
|
depth: number
|
||||||
|
isRotated: boolean
|
||||||
positionX: number
|
positionX: number
|
||||||
positionY: number
|
positionY: number
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,11 @@ export default {
|
|||||||
theme: {
|
theme: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
'titles': ['Poppins', 'serif'],
|
'titles': ['Poppins', 'serif'],
|
||||||
'default': ['Inter', 'serif']
|
'default': ['Quicksand', 'serif']
|
||||||
},
|
},
|
||||||
backgroundSize: {
|
backgroundSize: {
|
||||||
|
'cover': 'cover',
|
||||||
|
'contain': 'contain',
|
||||||
'30px': '30px'
|
'30px': '30px'
|
||||||
},
|
},
|
||||||
screens: {
|
screens: {
|
||||||
@ -47,16 +49,23 @@ export default {
|
|||||||
'character': '0 4px 30px rgba(0, 0, 0, 0.1)',
|
'character': '0 4px 30px rgba(0, 0, 0, 0.1)',
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
red: {
|
cyan: {
|
||||||
DEFAULT: '#d50000',
|
DEFAULT: '#0D6d69',
|
||||||
50: '#d50000',
|
50: '#f3faf8',
|
||||||
100: '#b30000'
|
100: '#D7F0EC',
|
||||||
|
200: '#b1dfd9',
|
||||||
|
300: '#80c8c1',
|
||||||
|
600: '#0D6D69',
|
||||||
|
800: '#244b4c',
|
||||||
|
900: '#204040',
|
||||||
|
950: '#0f2324'
|
||||||
},
|
},
|
||||||
bordeaux: {
|
red: {
|
||||||
DEFAULT: '#800020',
|
DEFAULT: '#e15970',
|
||||||
50: '#cc0033',
|
100: '#ffefef',
|
||||||
100: '#800020',
|
200: '#e15970',
|
||||||
200: '#4c0000'
|
300: '#b73f54',
|
||||||
|
400: '#332426'
|
||||||
},
|
},
|
||||||
blue: {
|
blue: {
|
||||||
DEFAULT: '#00c2ff'
|
DEFAULT: '#00c2ff'
|
||||||
@ -67,20 +76,20 @@ export default {
|
|||||||
purple: {
|
purple: {
|
||||||
DEFAULT: '#9841e6'
|
DEFAULT: '#9841e6'
|
||||||
},
|
},
|
||||||
cyan: {
|
|
||||||
DEFAULT: '#368f8b',
|
|
||||||
50: '#00b3b3',
|
|
||||||
100: '#368f8b',
|
|
||||||
200: '#376362'
|
|
||||||
},
|
|
||||||
gray: {
|
gray: {
|
||||||
DEFAULT: '#7f7f7f',
|
DEFAULT: '#262626',
|
||||||
50: '#d3d3d3',
|
50: '#fcfcfd',
|
||||||
100: '#7f7f7f',
|
100: '#f7f7f8',
|
||||||
200: '#696969',
|
150: '#f1f1f3',
|
||||||
300: '#313638',
|
175: '#e4e4e7',
|
||||||
500: '#778899',
|
200: '#999999',
|
||||||
600: '#B1B2B5'
|
300: '#808080',
|
||||||
|
400: '#666666',
|
||||||
|
500: '#4d4d4d',
|
||||||
|
600: '#333333',
|
||||||
|
700: '#262626',
|
||||||
|
800: '#1a1a1a',
|
||||||
|
900: '#0d0d0d'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|