Compare commits

...

66 Commits

Author SHA1 Message Date
3c7e96ea7f Setup skeleton for new HUD, updated fog img, updated effects 2024-10-13 12:25:13 +02:00
c86fd2e564 Merge remote-tracking branch 'origin/main' into feature/new-design-FE 2024-10-12 21:37:58 +02:00
f2e439831a npm update, worked on zone effects 2024-10-12 21:36:18 +02:00
474de8b14a Changed global styling so its readable
Placeholder changes before new design is implemented
2024-10-12 21:32:41 +02:00
b264ab3e40 Stash WIP zone effect 2024-10-11 15:04:50 +02:00
4293ec63b6 Adjusted global style classes 2024-10-09 21:46:12 +02:00
34393a31ac Added mobile responsive version to login 2024-10-09 21:26:38 +02:00
53daa758a8 Removed returns 2024-10-08 22:39:20 +02:00
86f2510f3a Changed login validation logic 2024-10-08 22:34:16 +02:00
e610e866c7 Added new colors, adjusted login styling 2024-10-08 21:28:16 +02:00
15442764c2 Adjusted login validation 2024-10-04 22:31:16 +02:00
8c3a488e7d WIP login screen 2024-10-04 20:52:21 +02:00
f51cb839bf npm update 2024-10-04 20:06:29 +02:00
376f8653d6 Zone editor bug fix 2024-10-02 19:56:07 +02:00
9b474909b3 #178 : switch tool mode with key shortcuts in zone editor 2024-10-02 19:38:53 +02:00
95d322a63c Separated menu items and buttons in zone editor 2024-10-02 19:21:54 +02:00
16a8435f7b #146 : Set camera on player's position after loading into a zone 2024-10-02 17:07:43 +02:00
334ceaa8f3 Refactor cameraControls and pointerHandlers, npm update, npm run format 2024-10-02 16:17:34 +02:00
f6b6b4b8ea #91 : Zone editor: allow objects to be rotated 2024-10-01 22:00:08 +02:00
7a50385420 Also apply text offset fix for Android 2024-10-01 00:48:55 +02:00
e5213cf5e6 npm update 2024-09-30 20:27:33 +02:00
cc07d132e8 Reapply "#169: Set different parameters to tileLayers to fix tile bleeding"
This reverts commit 99d66a7c42.

# Conflicts:
#	src/components/gameMaster/zoneEditor/ZoneEditor.vue
2024-09-30 20:25:20 +02:00
61b2717fb5 Reapply "TileLayer bug fix"
This reverts commit eb4ae4a625.

# Conflicts:
#	src/components/gameMaster/zoneEditor/ZoneEditor.vue
2024-09-30 20:24:39 +02:00
99d66a7c42 Revert "#169: Set different parameters to tileLayers to fix tile bleeding"
This reverts commit 0ffec44038.
2024-09-29 21:55:54 +02:00
eb4ae4a625 Revert "TileLayer bug fix"
This reverts commit 586600da7c.
2024-09-29 21:55:44 +02:00
586600da7c TileLayer bug fix 2024-09-29 21:50:27 +02:00
0ffec44038 #169: Set different parameters to tileLayers to fix tile bleeding 2024-09-29 21:23:46 +02:00
1b86b25bc2 Added comments 2024-09-29 21:23:29 +02:00
cd9316a384 Minor camera improvements 2024-09-29 20:13:54 +02:00
80bb38a6f7 Updated libs 2024-09-29 16:19:48 +02:00
0a37a09ed4 Updated libs 2024-09-29 16:19:42 +02:00
1f46b94441 Minor improvement 2024-09-29 16:07:55 +02:00
df3b9db45d Removed redundant parameters 2024-09-29 15:24:47 +02:00
d214bd37ad npm update, improved gameStore structure 2024-09-29 15:12:20 +02:00
c31dada1f9 CSS improvement 2024-09-29 01:39:54 +02:00
9cf872b7e2 Image quality bs 2024-09-29 01:37:33 +02:00
5b31729f64 Minor improvements 2024-09-29 01:07:53 +02:00
fda5224806 Attempt to fix keeping image quality on zoom out 2024-09-29 00:52:14 +02:00
fd599907e5 Disabled context menu 2024-09-29 00:51:50 +02:00
c2ae271306 Minor bug fix 2024-09-28 21:53:21 +02:00
494576b284 Updated game config for hopefully less image quality loss 2024-09-28 21:23:47 +02:00
9e96b2b32a Merge remote-tracking branch 'origin/main' into feature/151-broken-zoom 2024-09-28 20:30:36 +02:00
8eec2e12ce #165 : Show message if player runs in canvas mode, then disconnect 2024-09-28 20:30:26 +02:00
adc85d49a4 Change zoom to camera property 2024-09-28 20:22:01 +02:00
3dbd68d5cf Put zoom calculation in variable 2024-09-28 20:17:37 +02:00
2a34c7eea9 ENV bs 2024-09-28 20:16:57 +02:00
3c5ceaae2d Added min and max to zoom 2024-09-28 20:14:46 +02:00
31ce0a8264 Added dot env support 2024-09-28 20:02:59 +02:00
b4050bee01 Use WebGL by default. 2024-09-28 19:24:11 +02:00
cffab00974 #81 : Prevent walk after dragging zone 2024-09-28 19:19:00 +02:00
a92675e4c0 Position fix for texts 2024-09-28 17:46:05 +02:00
b2266f5a10 More whitespace around chat bubble 2024-09-28 03:11:49 +02:00
6cee0b93e6 Set char. limit of 90 to chat bubble 2024-09-28 03:10:02 +02:00
b4403f3267 Chat res. fix 2024-09-28 03:07:44 +02:00
fa12ce2ec8 Minor improvements 2024-09-28 02:52:36 +02:00
130df8f144 npm run format 2024-09-28 02:17:51 +02:00
1105a53feb Minor improvements 2024-09-28 02:17:33 +02:00
104e9e46fb Receive frameCount for assets from server 2024-09-28 00:59:10 +02:00
2223491571 #155 : Hide HUD elements if Game.vue is unloaded 2024-09-28 00:45:19 +02:00
ff61f88d62 #157 : Somewhat fixed resolution 2024-09-28 00:37:54 +02:00
0f231e10fa #159 : Camera control fixes 2024-09-28 00:21:24 +02:00
71042881dc Fix for #132 (character continues anim after moving is finished) 2024-09-27 23:56:02 +02:00
50feea0c9c Disabled dev 2024-09-26 00:43:22 +02:00
dd6b6f2a96 Potential fix 2024-09-26 00:41:40 +02:00
c09f503d54 Removed placeholder text 2024-09-25 14:06:22 +02:00
5f44a9aebd Removed scale, added resolution to game config and phaser text obj 2024-09-25 14:03:04 +02:00
68 changed files with 1628 additions and 1070 deletions

5
.env.example Normal file
View 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

View File

@ -4,6 +4,24 @@ WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci
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
# Production stage

1137
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -15,15 +15,15 @@
"format": "prettier --write src/"
},
"dependencies": {
"@vueuse/core": "^10.11.0",
"@vueuse/integrations": "^10.11.0",
"axios": "^1.7.2",
"phaser": "^3.80.1",
"pinia": "^2.1.7",
"socket.io-client": "^4.7.5",
"@vueuse/core": "^10.5.0",
"@vueuse/integrations": "^10.5.0",
"axios": "^1.7.7",
"phaser": "^3.85.2",
"pinia": "^2.1.6",
"socket.io-client": "^4.8.0",
"universal-cookie": "^6.1.3",
"vue": "^3.4.33",
"zod": "^3.23.8"
"vue": "^3.5.10",
"zod": "^3.22.2"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.10.3",
@ -40,16 +40,16 @@
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.27.0",
"jsdom": "^24.1.1",
"npm-run-all2": "^6.2.2",
"phaser3-rex-plugins": "^1.80.5",
"npm-run-all2": "^6.2.3",
"phaser3-rex-plugins": "^1.80.8",
"phavuer": "^0.16.1",
"postcss": "^8.4.39",
"postcss": "^8.4.47",
"prettier": "^3.3.3",
"sass": "^1.77.8",
"tailwindcss": "^3.4.6",
"typescript": "~5.5.3",
"vite": "^5.3.4",
"vite-plugin-vue-devtools": "^7.3.6",
"sass": "^1.79.4",
"tailwindcss": "^3.4.13",
"typescript": "~5.6.2",
"vite": "^5.4.8",
"vite-plugin-vue-devtools": "^7.4.6",
"vitest": "^2.0.3",
"vue-tsc": "^1.6.5"
}

BIN
public/assets/fog.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 KiB

BIN
public/assets/fog.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 301 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
public/assets/raindrop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
public/assets/raindrop.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 372 B

After

Width:  |  Height:  |  Size: 250 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 B

After

Width:  |  Height:  |  Size: 109 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 920 B

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 834 B

After

Width:  |  Height:  |  Size: 708 B

View File

@ -30,4 +30,7 @@ const screen = computed(() => {
}
return 'login' // default fallback
})
// Disable right click
addEventListener('contextmenu', (event) => event.preventDefault())
</script>

View File

@ -3,7 +3,7 @@
@tailwind utilities;
// Fonts
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@300..700&family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
//Globals
@ -26,14 +26,14 @@ h5,
h6,
button,
a {
@apply font-titles text-white font-medium m-0;
@apply font-default text-gray-200 font-medium m-0;
}
p,
span,
li,
label {
@apply font-default text-white;
@apply font-default text-gray-200;
}
button,
@ -56,12 +56,8 @@ input {
}
}
.input-cyan {
@apply py-2 px-2.5 font-titles border border-solid border-cyan bg-white/70 rounded;
&:focus,
&:focus-visible {
@apply outline-2 outline-cyan;
}
.input-field {
@apply px-4 py-3 text-base focus-visible:outline-none bg-gray border border-solid border-gray-500 rounded text-gray-300;
&.inactive {
@apply bg-gray-600/50 hover:cursor-not-allowed;
&::placeholder {
@ -87,20 +83,20 @@ button {
@apply text-center;
&.btn-cyan {
@apply bg-cyan/50 border border-solid border-white/25 rounded drop-shadow-20;
@apply bg-cyan text-gray-50 text-base rounded py-3;
&.active,
&:hover {
@apply bg-cyan;
@apply bg-cyan-800;
}
}
&.btn-bordeaux {
@apply bg-bordeaux/50 border border-solid border-white/25 rounded drop-shadow-20;
&.btn-red {
@apply bg-red text-gray-50;
&.active,
&:hover {
@apply bg-bordeaux;
@apply bg-red-300;
}
}

109
src/components/Effects.vue Normal file
View 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>

View File

@ -1,5 +1,5 @@
<template>
<div class="flex flex-wrap items-center input-cyan gap-1">
<div class="flex flex-wrap items-center input-field gap-1">
<div v-for="(chip, i) in internalValue" :key="i" class="flex gap-2.5 items-center bg-cyan rounded py-1 px-2">
<span class="text-xs">{{ chip }}</span>
<button type="button" class="text-xs cursor-pointer text-white font-light font-default not-italic hover:text-gray-50" @click="deleteChip(i)" aria-label="Remove chip">×</button>

View File

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

View File

@ -1,7 +1,7 @@
<template>
<Modal :isModalOpen="true" :closable="false" :is-resizable="false" :modal-width="modalWidth" :modal-height="modalHeight" :modal-position-x="posXY.x" :modal-position-y="posXY.y">
<template #modalHeader>
<h3 class="m-0 font-medium shrink-0">GM tools</h3>
<h3 class="m-0 font-medium shrink-0 text-gray-300">GM tools</h3>
</template>
<template #modalBody>
<div class="content flex flex-col gap-2.5 m-4 h-20">
@ -20,7 +20,7 @@ import { onMounted, ref } from 'vue'
const zoneEditorStore = useZoneEditorStore()
const gameStore = useGameStore()
const modalWidth = ref(200)
const modalHeight = ref(160)
const modalHeight = ref(180)
let posXY = ref({ x: 0, y: 0 })

View File

@ -3,22 +3,22 @@
<div class="relative p-2.5 flex flex-col items-center justify-between h-72">
<div class="filler"></div>
<img class="max-h-56" :src="`${config.server_endpoint}/assets/objects/${selectedObject?.id}.png`" :alt="'Object ' + selectedObject?.id" />
<button class="btn-bordeaux px-4 py-1.5 min-w-24" type="button" @click.prevent="removeObject">Remove</button>
<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>
<div class="m-2.5 p-2.5 block">
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject">
<div class="form-field-full">
<label for="name">Name</label>
<input v-model="objectName" class="input-cyan" type="text" name="name" placeholder="Wall #1" />
<input v-model="objectName" class="input-field" type="text" name="name" placeholder="Wall #1" />
</div>
<div class="form-field-half">
<label for="origin-x">Origin X</label>
<input v-model="objectOriginX" class="input-cyan" type="number" step="any" name="origin-x" placeholder="Origin X" />
<input v-model="objectOriginX" class="input-field" type="number" step="any" name="origin-x" placeholder="Origin X" />
</div>
<div class="form-field-half">
<label for="origin-y">Origin Y</label>
<input v-model="objectOriginY" class="input-cyan" type="number" step="any" name="origin-y" placeholder="Origin Y" />
<input v-model="objectOriginY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
</div>
<div class="form-field-full">
<label for="origin-x">Tags</label>
@ -26,22 +26,22 @@
</div>
<div class="form-field-full">
<label for="origin-x">Is animated</label>
<select v-model="objectIsAnimated" class="input-cyan" name="is-animated">
<select v-model="objectIsAnimated" class="input-field" name="is-animated">
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>
</div>
<div class="form-field-full">
<label for="frame-speed">Frame speed</label>
<input v-model="objectFrameSpeed" class="input-cyan" type="number" step="any" name="frame-speed" placeholder="Frame speed" />
<input v-model="objectFrameSpeed" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame speed" />
</div>
<div class="form-field-half">
<label for="frame-width">Frame width</label>
<input v-model="objectFrameWidth" class="input-cyan" type="number" step="any" name="frame-width" placeholder="Frame width" />
<input v-model="objectFrameWidth" class="input-field" type="number" step="any" name="frame-width" placeholder="Frame width" />
</div>
<div class="form-field-half">
<label for="frame-height">Frame height</label>
<input v-model="objectFrameHeight" class="input-cyan" type="number" step="any" name="frame-height" placeholder="Frame height" />
<input v-model="objectFrameHeight" class="input-field" type="number" step="any" name="frame-height" placeholder="Frame height" />
</div>
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
</form>

View File

@ -1,6 +1,6 @@
<template>
<div class="relative p-2.5 flex items-center gap-x-2.5">
<input v-model="searchQuery" class="input-cyan flex-grow" placeholder="Search..." @input="handleSearch" />
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
<label for="upload-asset" class="bg-cyan/50 border border-solid border-white/25 rounded drop-shadow-20 p-2 inline-flex items-center justify-center hover:bg-cyan hover:cursor-pointer">
<input class="hidden" id="upload-asset" ref="objectUploadField" type="file" accept="image/png" multiple @change="handleFileUpload" />
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">

View File

@ -4,12 +4,12 @@
<div class="flex flex-wrap gap-2">
<div class="w-full flex flex-col">
<label class="mb-1.5 font-titles" for="name">Name</label>
<input v-model="spriteName" class="input-cyan" type="text" name="name" placeholder="New sprite" />
<input v-model="spriteName" class="input-field" type="text" name="name" placeholder="New sprite" />
</div>
<div class="w-full flex gap-2 mt-2 pb-4 relative">
<button class="btn-cyan px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="saveSprite">Save</button>
<button class="btn-bordeaux px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="deleteSprite">Delete</button>
<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>
</div>
@ -19,40 +19,40 @@
<template #header>
<div class="flex justify-between items-center">
{{ action.action }}
<button class="btn-bordeaux px-4 py-1.5 min-w-24" type="button" @click.prevent="() => spriteActions.splice(spriteActions.indexOf(action), 1)">Delete</button>
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="() => spriteActions.splice(spriteActions.indexOf(action), 1)">Delete</button>
</div>
</template>
<template #content>
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveSprite">
<div class="form-field-full">
<label for="action">Action</label>
<input v-model="action.action" class="input-cyan" type="text" name="action" placeholder="Action" />
<input v-model="action.action" class="input-field" type="text" name="action" placeholder="Action" />
</div>
<div class="form-field-half">
<label for="origin-x">Origin X</label>
<input v-model.number="action.originX" class="input-cyan" type="number" step="any" name="origin-x" placeholder="Origin X" />
<input v-model.number="action.originX" class="input-field" type="number" step="any" name="origin-x" placeholder="Origin X" />
</div>
<div class="form-field-half">
<label for="origin-y">Origin Y</label>
<input v-model.number="action.originY" class="input-cyan" type="number" step="any" name="origin-y" placeholder="Origin Y" />
<input v-model.number="action.originY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
</div>
<div class="form-field-half">
<label for="is-animated">Is animated</label>
<select v-model="action.isAnimated" class="input-cyan" name="is-animated">
<select v-model="action.isAnimated" class="input-field" name="is-animated">
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>
</div>
<div class="form-field-half" v-if="action.isAnimated">
<label for="is-looping">Is looping</label>
<select v-model="action.isLooping" class="input-cyan" name="is-looping">
<select v-model="action.isLooping" class="input-field" name="is-looping">
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>
</div>
<div class="form-field-full" v-if="action.isAnimated">
<label for="frame-speed">Frame speed</label>
<input v-model.number="action.frameSpeed" class="input-cyan" type="number" step="any" name="frame-speed" placeholder="Frame speed" />
<input v-model.number="action.frameSpeed" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame speed" />
</div>
<div class="form-field-full">
<SpriteActionsInput v-model="action.sprites" />

View File

@ -1,6 +1,6 @@
<template>
<div class="relative p-2.5 flex items-center gap-x-2.5">
<input v-model="searchQuery" class="input-cyan flex-grow" placeholder="Search..." @input="handleSearch" />
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
<button @click.prevent="newButtonClickHandler" class="bg-cyan/50 border border-solid border-white/25 rounded drop-shadow-20 p-2 inline-flex items-center justify-center hover:bg-cyan hover:cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />

View File

@ -3,14 +3,14 @@
<div class="relative p-2.5 flex flex-col items-center justify-between h-72">
<div class="filler"></div>
<img class="max-h-72" :src="`${config.server_endpoint}/assets/tiles/${selectedTile?.id}.png`" :alt="'Tile ' + selectedTile?.id" />
<button class="btn-bordeaux px-4 py-1.5 min-w-24" type="button" @click.prevent="deleteTile">Delete</button>
<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>
<div class="m-2.5 p-2.5 block">
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveTile">
<div class="form-field-full">
<label for="name">Name</label>
<input v-model="tileName" class="input-cyan" type="text" name="name" placeholder="Tile #1" />
<input v-model="tileName" class="input-field" type="text" name="name" placeholder="Tile #1" />
</div>
<div class="form-field-full">
<label for="origin-x">Tags</label>

View File

@ -1,6 +1,6 @@
<template>
<div class="relative p-2.5 flex items-center gap-x-2.5">
<input v-model="searchQuery" class="input-cyan flex-grow" placeholder="Search..." @input="handleSearch" />
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
<label for="upload-asset" class="bg-cyan/50 border border-solid border-white/25 rounded drop-shadow-20 p-2 inline-flex items-center justify-center hover:bg-cyan hover:cursor-pointer">
<input class="hidden" id="upload-asset" ref="tileUploadField" type="file" accept="image/png" multiple @change="handleFileUpload" />
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">

View File

@ -12,14 +12,14 @@
<TeleportModal v-if="shouldShowTeleportModal" />
<Container :depth="2">
<Image v-for="object in zoneObjects" :depth="calculateIsometricDepth(object.positionX, object.positionY, 0)" :key="object.id" v-bind="getObjectImageProps(object)" @pointerup="() => setSelectedZoneObject(object)" />
<Image v-for="object in zoneObjects" :depth="calculateIsometricDepth(object.positionX, object.positionY, 0)" :key="object.id" v-bind="getObjectImageProps(object)" @pointerup="() => setSelectedZoneObject(object)" :flipX="object.isRotated" />
</Container>
<Container :depth="3">
<Image v-for="tile in zoneEventTiles" :key="tile.id" v-bind="getEventTileImageProps(tile)" />
</Container>
<SelectedZoneObject v-if="zoneEditorStore.selectedZoneObject" @update_depth="updateZoneObjectDepth" @delete="deleteZoneObject" @move="handleMove" />
<SelectedZoneObject v-if="zoneEditorStore.selectedZoneObject" @update_depth="updateZoneObjectDepth" @delete="deleteZoneObject" @move="handleMove" @rotate="handleRotate" />
</template>
</template>
@ -79,8 +79,8 @@ function createTilemap() {
}
function createTileLayer() {
const tilesetImages = gameStore.assets.filter((asset) => asset.group === 'tiles').map((asset, index) => zoneTilemap.addTilesetImage(asset.key, asset.key, config.tile_size.x, config.tile_size.y, 0, 0, index + 1, { x: 0, y: -config.tile_size.y }))
tilesetImages.push(zoneTilemap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 0, 0, 0, { x: 0, y: -config.tile_size.y }))
const tilesetImages = gameStore.assets.filter((asset) => asset.group === 'tiles').map((asset, index) => zoneTilemap.addTilesetImage(asset.key, asset.key, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y }))
tilesetImages.push(zoneTilemap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 1, 2, 0, { x: 0, y: -config.tile_size.y }))
const layer = zoneTilemap.createBlankLayer('tiles', tilesetImages as any, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
@ -146,6 +146,7 @@ function addZoneObject(tile: Phaser.Tilemaps.Tile) {
objectId: selectedObject.value!.id,
object: selectedObject.value!,
depth: 0,
isRotated: false,
positionX: tile.x,
positionY: tile.y
}
@ -208,7 +209,7 @@ function save() {
tiles: tileArray,
pvp: zone.value.pvp,
zoneEventTiles: zoneEventTiles.value.map(({ id, zoneId, type, positionX, positionY, teleport }) => ({ id, zoneId, type, positionX, positionY, teleport })),
zoneObjects: zoneObjects.value.map(({ id, zoneId, objectId, depth, positionX, positionY }) => ({ id, zoneId, objectId, depth, positionX, positionY }))
zoneObjects: zoneObjects.value.map(({ id, zoneId, objectId, depth, isRotated, positionX, positionY }) => ({ id, zoneId, objectId, depth, isRotated, positionX, positionY }))
}
if (zoneEditorStore.isSettingsModalShown) {
@ -248,31 +249,12 @@ function handleMove() {
console.log('move btn clicked')
}
onBeforeMount(async () => {
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'))
function handleRotate(objectId: string) {
const object = zoneObjects.value.find((obj) => obj.id === objectId)
if (object) {
object.isRotated = !object.isRotated
}
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(
@ -317,5 +299,29 @@ const setSelectedZoneObject = (zoneObject: ZoneObject | null) => {
onBeforeMount(async () => {
await gameStore.fetchAllZoneAssets()
await loadAssets(scene)
tileArray.forEach((row, y) => row.forEach((_, x) => placeTile(zoneTilemap, tiles, x, y, 'blank_tile')))
if (zone.value?.tiles) {
setAllTiles(zoneTilemap, tiles, zone.value.tiles)
tileArray = zone.value.tiles.map((row) => row.map((tileId) => tileId || 'blank_tile'))
}
zoneEventTiles.value = zone.value?.zoneEventTiles ?? []
zoneObjects.value = sortByIsometricDepth(zone.value?.zoneObjects ?? [])
// Center camera
const centerY = (zoneTilemap.height * zoneTilemap.tileHeight) / 2
const centerX = (zoneTilemap.width * zoneTilemap.tileWidth) / 2
scene.cameras.main.centerOn(centerX, centerY)
})
onUnmounted(() => {
zoneEventTiles.value = []
zoneObjects.value = []
tiles?.destroy()
zoneTilemap?.removeAllLayers()
zoneTilemap?.destroy()
zoneEditorStore.reset()
})
</script>

View File

@ -1,7 +1,7 @@
<template>
<Modal :isModalOpen="true" @modal:close="() => zoneEditorStore.toggleCreateZoneModal()" :modal-width="300" :modal-height="400" :is-resizable="false">
<template #modalHeader>
<h3 class="m-0 font-medium shrink-0">Create new zone</h3>
<h3 class="m-0 font-medium shrink-0 text-gray-300">Create new zone</h3>
</template>
<template #modalBody>
@ -10,19 +10,19 @@
<div class="gap-2.5 flex flex-wrap">
<div class="form-field-full">
<label for="name">Name</label>
<input class="input-cyan max-w-64" v-model="name" name="name" id="name" />
<input class="input-field max-w-64" v-model="name" name="name" id="name" />
</div>
<div class="form-field-half">
<label for="name">Width</label>
<input class="input-cyan max-w-64" v-model="width" name="name" id="name" type="number" />
<input class="input-field max-w-64" v-model="width" name="name" id="name" type="number" />
</div>
<div class="form-field-half">
<label for="name">Height</label>
<input class="input-cyan max-w-64" v-model="height" name="name" id="name" type="number" />
<input class="input-field max-w-64" v-model="height" name="name" id="name" type="number" />
</div>
<div class="form-field-full">
<label for="name">PVP enabled</label>
<select class="input-cyan" name="pvp" id="pvp">
<select class="input-field" name="pvp" id="pvp">
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>

View File

@ -2,16 +2,16 @@
<Teleport to="body">
<Modal :isModalOpen="zoneEditorStore.isObjectListModalShown" :modal-width="645" :modal-height="260" @modal:close="() => (zoneEditorStore.isObjectListModalShown = false)">
<template #modalHeader>
<h3 class="text-lg">Objects</h3>
<h3 class="text-lg text-gray-300">Objects</h3>
<div class="flex">
<div class="w-full flex gap-1.5 flex-row">
<div>
<label class="mb-1.5 font-titles hidden" for="search">Search...</label>
<input @mousedown.stop class="input-cyan" type="text" name="search" placeholder="Search" v-model="searchQuery" />
<input @mousedown.stop class="input-field" type="text" name="search" placeholder="Search" v-model="searchQuery" />
</div>
<!-- <div>-->
<!-- <label class="mb-1.5 font-titles hidden" for="depth">Depth</label>-->
<!-- <input v-model="objectDepth" @mousedown.stop class="input-cyan" type="number" name="depth" placeholder="Depth" />-->
<!-- <input v-model="objectDepth" @mousedown.stop class="input-field" type="number" name="depth" placeholder="Depth" />-->
<!-- </div>-->
</div>
</div>

View File

@ -1,15 +1,15 @@
<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>
<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>
<button @mousedown.stop @click="handleDelete" class="btn-bordeaux py-1.5 px-4" :disabled="!isObjectSelected">
<button @mousedown.stop @click="handleDelete" class="btn-red py-1.5 px-4">
<img src="/assets/icons/trashcan.svg" class="w-4 h-4" alt="Delete" />
</button>
<button @mousedown.stop @click="zoneEditorStore.setSelectedObject(zoneEditorStore.selectedZoneObject?.object)" class="btn-cyan py-1.5 px-4" :disabled="!isObjectSelected">S</button>
<button @mousedown.stop @click="handleMove" class="btn-cyan py-1.5 px-4 min-w-24" :disabled="!isObjectSelected">Move</button>
<button @mousedown.stop @click="handleRotate" class="btn-cyan py-1.5 px-4">Rotate</button>
<button @mousedown.stop @click="handleMove" class="btn-cyan py-1.5 px-4 min-w-24">Move</button>
</div>
</div>
</template>
@ -18,13 +18,11 @@
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
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 objectDepth = ref(zoneEditorStore.objectDepth)
const isObjectSelected = computed(() => !!zoneEditorStore.selectedZoneObject)
watch(
() => zoneEditorStore.selectedZoneObject,
(selectedZoneObject) => {
@ -39,6 +37,10 @@ const handleDepthInput = () => {
}
}
const handleRotate = () => {
emit('rotate', zoneEditorStore.selectedZoneObject?.id)
}
const handleMove = () => {
emit('move')
}

View File

@ -1,7 +1,7 @@
<template>
<Modal :is-modal-open="true" @modal:close="() => zoneEditorStore.setTool('move')" :modal-width="300" :modal-height="350" :is-resizable="false">
<template #modalHeader>
<h3 class="m-0 font-medium shrink-0">Teleport settings</h3>
<h3 class="m-0 font-medium shrink-0 text-gray-300">Teleport settings</h3>
</template>
<template #modalBody>
@ -10,15 +10,15 @@
<div class="gap-2.5 flex flex-wrap">
<div class="form-field-half">
<label for="positionX">Position X</label>
<input class="input-cyan" v-model="toPositionX" name="positionX" id="positionX" type="number" />
<input class="input-field" v-model="toPositionX" name="positionX" id="positionX" type="number" />
</div>
<div class="form-field-half">
<label for="positionY">Position Y</label>
<input class="input-cyan" v-model="toPositionY" name="positionY" id="positionY" type="number" />
<input class="input-field" v-model="toPositionY" name="positionY" id="positionY" type="number" />
</div>
<div class="form-field-full">
<label for="rotation">Rotation</label>
<select v-model="toRotation" class="input-cyan" name="rotation" id="rotation">
<select v-model="toRotation" class="input-field" name="rotation" id="rotation">
<option :value="0">North</option>
<option :value="2">East</option>
<option :value="4">South</option>
@ -27,7 +27,7 @@
</div>
<div class="form-field-full">
<label for="toZoneId">Zone to teleport to</label>
<select v-model="toZoneId" class="input-cyan" name="toZoneId" id="toZoneId">
<select v-model="toZoneId" class="input-field" name="toZoneId" id="toZoneId">
<option :value="0">Select zone</option>
<option v-for="zone in zoneEditorStore.zoneList" :key="zone.id" :value="zone.id">{{ zone.name }}</option>
</select>
@ -79,4 +79,4 @@ function updateTeleportSettings() {
toZoneId: toZoneId.value
})
}
</script>
</script>

View File

@ -2,12 +2,12 @@
<Teleport to="body">
<Modal :isModalOpen="zoneEditorStore.isTileListModalShown" :modal-width="645" :modal-height="600" @modal:close="() => (zoneEditorStore.isTileListModalShown = false)">
<template #modalHeader>
<h3 class="text-lg">Tiles</h3>
<h3 class="text-lg text-gray-300">Tiles</h3>
<div class="flex">
<div class="w-full flex gap-1.5 flex-row">
<div>
<label class="mb-1.5 font-titles hidden" for="search">Search...</label>
<input @mousedown.stop class="input-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>

View File

@ -1,21 +1,21 @@
<template>
<div class="flex justify-center p-5">
<div class="toolbar fixed bottom-0 m-3 rounded left-0 right-0 flex bg-gray-300/80 solid border-solid border-2 border-cyan ext-gray-50 p-1.5 px-3 p min-w-11/12 h-10">
<div 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">
<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>
</button>
<div class="w-px bg-cyan"></div>
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan-50 gap-2.5': zoneEditorStore.tool === 'pencil' }" @click="handleClick('pencil')">
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': zoneEditorStore.tool === 'pencil' }" @click="handleClick('pencil')">
<img class="invert w-5 h-5" src="/assets/icons/zoneEditor/pencil.svg" alt="Pencil" /> <span :class="{ 'ml-2.5': zoneEditorStore.tool !== 'pencil' }">(P)</span>
<div class="select" v-if="zoneEditorStore.tool === 'pencil'">
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectPencilOpen }">
{{ zoneEditorStore.drawMode }}
<img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/zoneEditor/chevron.svg" />
</div>
<div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray-300/80 rounded min-w-28 border border-cyan border-solid text-left" v-show="selectPencilOpen && zoneEditorStore.tool === 'pencil'">
<div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectPencilOpen && zoneEditorStore.tool === 'pencil'">
<span class="py-2 px-2.5 relative hover:bg-cyan/50" @click="setDrawMode('tile')">
Tile
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
@ -35,14 +35,14 @@
<div class="w-px bg-cyan"></div>
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan-50 gap-2.5': zoneEditorStore.tool === 'eraser' }" @click="handleClick('eraser')">
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': zoneEditorStore.tool === 'eraser' }" @click="handleClick('eraser')">
<img class="invert w-5 h-5" src="/assets/icons/zoneEditor/eraser.svg" alt="Eraser" /> <span :class="{ 'ml-2.5': zoneEditorStore.tool !== 'eraser' }">(E)</span>
<div class="select" v-if="zoneEditorStore.tool === 'eraser'">
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectEraserOpen }">
{{ zoneEditorStore.eraserMode }}
<img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/zoneEditor/chevron.svg" />
</div>
<div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray-300/80 rounded min-w-28 border border-cyan border-solid text-left" v-show="selectEraserOpen">
<div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectEraserOpen">
<span class="py-2 px-2.5 relative hover:bg-cyan/50" @click="setEraserMode('tile')">
Tile
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
@ -62,7 +62,7 @@
<div class="w-px bg-cyan"></div>
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan-50 gap-2.5': zoneEditorStore.tool === 'paint' }" @click="handleClick('paint')">
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': zoneEditorStore.tool === 'paint' }" @click="handleClick('paint')">
<img class="invert w-5 h-5" src="/assets/icons/zoneEditor/paint.svg" alt="Paint bucket" /> <span :class="{ 'ml-2.5': zoneEditorStore.tool !== 'paint' }">(B)</span>
</button>
@ -71,7 +71,7 @@
<button class="flex justify-center items-center min-w-10 p-0 relative" @click="handleClick('settings')" v-if="zoneEditorStore.zone"><img class="invert w-5 h-5" src="/assets/icons/zoneEditor/gear.svg" alt="Zone settings" /> <span class="ml-2.5">(Z)</span></button>
</div>
<div class="flex gap-2.5 ml-auto">
<div class="toolbar fixed bottom-0 right-0 m-3 rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10 space-x-2">
<button class="btn-cyan px-3.5" @click="() => zoneEditorStore.toggleZoneListModal()">Load</button>
<button class="btn-cyan px-3.5" @click="() => emit('save')" v-if="zoneEditorStore.zone">Save</button>
<button class="btn-cyan px-3.5" @click="() => emit('clear')" v-if="zoneEditorStore.zone">Clear</button>
@ -171,13 +171,26 @@ function handleClick(tool: string) {
selectEraserOpen.value = tool === 'eraser' ? !selectEraserOpen.value : false
}
// Key bindings
function cycleToolMode(tool: 'pencil' | 'eraser') {
const modes = ['tile', 'object', 'teleport', 'blocking tile'];
const currentMode = tool === 'pencil' ? zoneEditorStore.drawMode : zoneEditorStore.eraserMode;
const currentIndex = modes.indexOf(currentMode);
const nextIndex = (currentIndex + 1) % modes.length;
const nextMode = modes[nextIndex];
if (tool === 'pencil') {
setDrawMode(nextMode);
} else {
setEraserMode(nextMode);
}
}
function initKeyShortcuts(event: KeyboardEvent) {
if (!zoneEditorStore.zone) return
// prevent if focused on composables
if (document.activeElement?.tagName === 'INPUT') return
const keyActions: any = {
const keyActions: { [key: string]: string } = {
m: 'move',
p: 'pencil',
e: 'eraser',
@ -186,7 +199,12 @@ function initKeyShortcuts(event: KeyboardEvent) {
}
if (keyActions.hasOwnProperty(event.key)) {
handleClick(keyActions[event.key])
const tool = keyActions[event.key];
if ((tool === 'pencil' || tool === 'eraser') && zoneEditorStore.tool === tool) {
cycleToolMode(tool);
} else {
handleClick(tool);
}
}
}

View File

@ -4,7 +4,7 @@
<Teleport to="body">
<Modal @modal:close="() => zoneEditorStore.toggleZoneListModal()" :is-resizable="false" :is-modal-open="true" :modal-width="300" :modal-height="360">
<template #modalHeader>
<h3 class="text-lg">Zones</h3>
<h3 class="text-lg text-gray-300">Zones</h3>
</template>
<template #modalBody>
<div class="my-4 mx-auto">
@ -17,7 +17,7 @@
<div class="flex gap-3 items-center w-full" @click="() => loadZone(zone.id)">
<span>{{ zone.name }}</span>
<span class="ml-auto gap-1 flex">
<button class="btn-bordeaux py-0.5 px-2.5 z-50" @click.stop="() => deleteZone(zone.id)">X</button>
<button class="btn-red py-0.5 px-2.5 z-50" @click.stop="() => deleteZone(zone.id)">X</button>
</span>
</div>
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>

View File

@ -1,7 +1,7 @@
<template>
<Modal :is-modal-open="zoneEditorStore.isSettingsModalShown" @modal:close="() => zoneEditorStore.toggleSettingsModal()" :modal-width="300" :modal-height="350" :is-resizable="false">
<Modal :is-modal-open="zoneEditorStore.isSettingsModalShown" @modal:close="() => zoneEditorStore.toggleSettingsModal()" :modal-width="600" :modal-height="350">
<template #modalHeader>
<h3 class="m-0 font-medium shrink-0">Zone settings</h3>
<h3 class="m-0 font-medium shrink-0 text-gray-300">Zone settings</h3>
</template>
<template #modalBody>
@ -10,19 +10,19 @@
<div class="gap-2.5 flex flex-wrap">
<div class="form-field-full">
<label for="name">Name</label>
<input class="input-cyan" v-model="name" name="name" id="name" />
<input class="input-field" v-model="name" name="name" id="name" />
</div>
<div class="form-field-half">
<label for="name">Width</label>
<input class="input-cyan" v-model="width" name="name" id="name" type="number" />
<input class="input-field" v-model="width" name="name" id="name" type="number" />
</div>
<div class="form-field-half">
<label for="name">Height</label>
<input class="input-cyan" v-model="height" name="name" id="name" type="number" />
<input class="input-field" v-model="height" name="name" id="name" type="number" />
</div>
<div class="form-field-full">
<label for="pvp">PVP enabled</label>
<select v-model="pvp" class="input-cyan" name="pvp" id="pvp">
<select v-model="pvp" class="input-field" name="pvp" id="pvp">
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>

View File

@ -1,6 +1,6 @@
<template>
<div class="w-full md:min-w-[350px] max-w-[750px] flex flex-col">
<div ref="chatWindow" class="w-full overflow-auto h-32 mb-5 bg-gray-300/80 rounded-lg border-2 border-solid border-cyan-200" v-show="gameStore.isChatOpen">
<div 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">
<span class="text-ellipsis overflow-hidden whitespace-nowrap text-sm">{{ message.character.name }}</span>
<p class="text-gray-50 m-0">{{ message.message }}</p>
@ -8,7 +8,7 @@
</div>
<div class="w-full flex">
<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..."
v-model="message"
@keypress="handleKeyPress"
@ -59,61 +59,63 @@ const scrollToBottom = () => {
})
}
gameStore.connection?.on('chat:message', (data: ChatMessage) => {
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)
})
gameStore.connection?.on('chat:message', (data: ChatMessage) => {
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.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(() => {
gameStore.connection?.off('chat:message')
})

View File

@ -0,0 +1,9 @@
<template>
</template>
<script setup lang="ts">
import { useGameStore } from '@/stores/gameStore'
const gameStore = useGameStore()
</script>

View File

@ -1,46 +1,5 @@
<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>
<script setup lang="ts">

View File

@ -0,0 +1,9 @@
<template>
</template>
<script setup lang="ts">
import { useGameStore } from '@/stores/gameStore'
const gameStore = useGameStore()
</script>

View File

@ -0,0 +1,9 @@
<template>
</template>
<script setup lang="ts">
import { useGameStore } from '@/stores/gameStore'
const gameStore = useGameStore()
</script>

View File

@ -1,5 +1,5 @@
<template>
<div class="absolute z-50 w-full h-dvh top-0 left-0 bg-black/60" v-show="gameStore.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="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>

View File

@ -6,11 +6,11 @@
<form class="flex gap-2.5 flex-wrap">
<div class="form-field-half max-w-[300px]">
<label for="name">Name</label>
<input class="input-cyan" :class="{ inactive: !editCharacter }" type="text" name="name" placeholder="Ethereal" :disabled="!editCharacter" />
<input class="input-field" :class="{ inactive: !editCharacter }" type="text" name="name" placeholder="Ethereal" :disabled="!editCharacter" />
</div>
<div class="form-field-half max-w-[300px] relative">
<label for="class">Class</label>
<select class="input-cyan" v-model="characterClass" :class="{ inactive: !editCharacter }" name="class" :disabled="!editCharacter">
<select class="input-field" v-model="characterClass" :class="{ inactive: !editCharacter }" name="class" :disabled="!editCharacter">
<option value="Knight" :selected="characterClass == 'Knight'" :disabled="characterClass == 'Knight'">Knight</option>
<option value="Paladin" :selected="characterClass == 'Paladin'" :disabled="characterClass == 'Paladin'">Paladin</option>
</select>

View File

@ -1,58 +1,58 @@
<template>
<Container ref="charChatContainer" :depth="999" v-if="props.character" :x="currentX" :y="currentY">>
<RoundRectangle @create="createChatBubble" :origin-x="0.5" :origin-y="7.5" :fillColor="0xffffff" :width="194" :height="21" :radius="5" />
<Text @create="createChatText" :text="`This is a chat message 🤯👋`" :origin-x="0.5" :origin-y="10.9" :style="{ fontSize: 13, fontFamily: 'Arial', color: '#000' }" />
<!-- Chat bubble -->
<Container ref="charChatContainer" :depth="999" :x="currentX" :y="currentY">
<RoundRectangle @create="createChatBubble" :origin-x="0.5" :origin-y="7.5" :fillColor="0xffffff" :width="194" :height="21" :radius="20" />
<Text @create="createChatText" text="" :origin-x="0.5" :origin-y="10.9" :style="{ fontSize: 13, fontFamily: 'Arial', color: '#000' }" />
</Container>
<Container :depth="999" v-if="props.character" :x="currentX" :y="currentY">
<Text @create="createText" :text="props.character.name" :origin-x="0.5" :origin-y="9" />
<!-- Character name and health -->
<Container :depth="999" :x="currentX" :y="currentY">
<Text @create="createText" :text="character.name" :origin-x="0.5" :origin-y="9" />
<RoundRectangle :origin-x="0.5" :origin-y="18.5" :fillColor="0xffffff" :width="74" :height="6" :radius="5" />
<RoundRectangle :origin-x="0.5" :origin-y="36.4" :fillColor="0x00b3b3" :width="70" :height="3" :radius="5" />
</Container>
<Container :depth="isometricDepth" v-if="props.character" :x="currentX" :y="currentY" ref="charContainer">
<Image v-if="!props.character.characterType" texture="character" :origin-y="1" />
<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" />
<!-- Character sprite -->
<Container ref="charContainer" :depth="isometricDepth" :x="currentX" :y="currentY">
<Sprite ref="charSprite" :origin-y="1" :flipX="isFlippedX" :flipY="false" />
</Container>
</template>
<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 { type ExtendedCharacter } from '@/types'
import { useGameStore } from '@/stores/gameStore'
import { useZoneStore } from '@/stores/zoneStore'
import { watch, computed, ref, onMounted, onUnmounted } from 'vue'
import { Container, refObj, RoundRectangle, Sprite, Text, useGame, useScene } from 'phavuer'
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
enum Direction {
POSITIVE,
NEGATIVE,
NOCHANGE
UNCHANGED
}
const charChatContainer = refObj() as Ref<Phaser.GameObjects.Container>;
const charContainer = refObj() as Ref<Phaser.GameObjects.Container>;
interface Props {
const props = defineProps<{
layer: Phaser.Tilemaps.TilemapLayer
character?: CharacterT
}
character: ExtendedCharacter
}>()
const props = withDefaults(defineProps<Props>(), {
character: undefined
})
const charChatContainer = refObj<Phaser.GameObjects.Container>()
const charContainer = refObj<Phaser.GameObjects.Container>()
const charSprite = refObj<Phaser.GameObjects.Sprite>()
const game = useGame()
const gameStore = useGameStore()
const zoneStore = useZoneStore()
const scene = useScene()
const isometricDepth = ref(calculateIsometricDepth(props.character!.positionX, props.character!.positionY, 28, 94, true))
const currentX = ref(0)
const currentY = ref(0)
const tween = ref<Phaser.Tweens.Tween | null>(null)
const isometricDepth = ref(1)
const isInitialPosition = ref(true)
const tween = ref<Phaser.Tweens.Tween | null>(null)
const calculateLocalDepth = (x: number, y: number, width: number, height: number, isCharacter: boolean) => {
isometricDepth.value = calculateIsometricDepth(x, y, width, height, isCharacter)
const updateIsometricDepth = (x: number, y: number) => {
isometricDepth.value = calculateIsometricDepth(x, y, 28, 94, true)
}
const updatePosition = (x: number, y: number, direction: Direction) => {
@ -66,52 +66,93 @@ const updatePosition = (x: number, y: number, direction: Direction) => {
return
}
if (tween.value && tween.value.isPlaying()) {
if (tween.value?.isPlaying()) {
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))
/**
* 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
if (distance >= config.tile_size.x / 1.1) {
currentX.value = targetX
currentY.value = targetY
return
}
/**
* 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
const duration = distance * 6
tween.value = props.layer.scene.tweens.add({
targets: { x: currentX.value, y: currentY.value },
x: targetX,
y: targetY,
duration: duration,
ease: 'Linear',
onStart: () => {
if (direction === Direction.POSITIVE) {
calculateLocalDepth(x, y, 28, 94, true)
}
},
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)
}
tween.value = props.layer.scene.tweens.add({
targets: { x: currentX.value, y: currentY.value },
x: targetX,
y: targetY,
duration,
ease: 'Linear',
onStart: () => {
if (direction === Direction.POSITIVE) {
updateIsometricDepth(x, y)
}
})
},
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,
(newChar, oldChar) => {
if (!newChar) return
if (!oldChar || newChar.positionX !== oldChar.positionX || newChar.positionY !== oldChar.positionY) {
if (!oldChar) {
updatePosition(newChar.positionX, newChar.positionY, Direction.POSITIVE)
} else {
const direction = calcDirection(oldChar.positionX, oldChar.positionY, newChar.positionX, newChar.positionY)
updatePosition(newChar.positionX, newChar.positionY, direction)
}
const direction = !oldChar ? Direction.POSITIVE : 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) => {
if (newY < oldY || newX < oldX) {
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')
}
watch(() => props.character.isMoving, updateSprite)
watch(() => props.character.rotation, updateSprite)
onMounted(() => {
// Check if player is this character, then lock with camera
if (props.character && props.character.id === gameStore.character?.id) {
charChatContainer.value?.setName(props.character.name + '_chatContainer')
charChatContainer.value?.setVisible(false)
charChatContainer.value!.setName(`${props.character!.name}_chatContainer`)
charChatContainer.value!.setVisible(false)
charContainer.value!.setName(props.character!.name)
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) {
updatePosition(props.character.positionX, props.character.positionY, Direction.POSITIVE)
}
// Set sprite
charSprite.value!.setTexture(charTexture.value)
charSprite.value!.setFlipX(isFlippedX.value)
updatePosition(props.character.positionX, props.character.positionY, props.character.rotation)
})
onUnmounted(() => {
if (tween.value) {
tween.value.stop()
}
tween.value?.stop()
})
</script>

View File

@ -40,12 +40,12 @@ const modalOpened = ref(props.modalOpened)
<template>
<Modal :closable="false" :is-resizable="false" :isModalOpen="true" @modal:close="() => (modalOpened = !modalOpened)" :modal-width="300" :modal-height="190">
<template #modalHeader>
<div class="text-white">
<div class="text-gray-300">
<slot name="modalHeader"></slot>
</div>
</template>
<template #modalBody>
<div class="text-white h-full">
<div class="text-gray-300 h-full">
<div class="flex h-full flex-col justify-between">
<span class="p-2">
<slot name="modalBody"></slot>

View File

@ -8,21 +8,13 @@ import { onBeforeUnmount, ref } from 'vue'
import { usePointerHandlers } from '@/composables/usePointerHandlers'
import { useCameraControls } from '@/composables/useCameraControls'
interface Props {
layer: Phaser.Tilemaps.TilemapLayer
}
const props = defineProps<Props>()
const props = defineProps<{ layer: Phaser.Tilemaps.TilemapLayer }>()
const scene = useScene()
const waypoint = ref({
visible: false,
x: 0,
y: 0
})
const waypoint = ref({ visible: false, x: 0, y: 0 })
const { camera, isDragging } = useCameraControls(scene)
const { setupPointerHandlers, cleanupPointerHandlers } = usePointerHandlers(scene, props.layer, waypoint, camera, isDragging)
const { camera } = useCameraControls(scene)
const { setupPointerHandlers, cleanupPointerHandlers } = usePointerHandlers(scene, props.layer, waypoint, camera)
setupPointerHandlers()
onBeforeUnmount(cleanupPointerHandlers)

View File

@ -1,7 +1,7 @@
<template>
<Teleport to="body">
<div v-if="isModalOpenRef" class="fixed bg-gray-300/80 border-solid border-2 border-cyan-200 z-50 flex flex-col backdrop-blur-sm shadow-lg" :style="modalStyle">
<div @mousedown="startDrag" class="cursor-move p-2.5 flex justify-between items-center border-solid border-0 border-b border-cyan-200">
<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-gray-500">
<slot name="modalHeader" />
<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">
@ -16,7 +16,7 @@
<slot name="modalBody" />
<img v-if="isResizable && !isFullScreen" src="/assets/icons/resize-icon.svg" alt="resize" class="absolute bottom-0 right-0 w-5 h-5 cursor-nwse-resize invert-[60%]" @mousedown="startResize" />
</div>
<div v-if="$slots.modalFooter" class="px-5 min-h-12 flex justify-end gap-7.5 items-center border-solid border-t border-cyan-200">
<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" />
</div>
</div>

View File

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

View File

@ -1,5 +1,5 @@
<template>
<Image v-for="object in zoneStore.zone?.zoneObjects" :depth="calculateIsometricDepth(object.positionX, object.positionY, object.object.frameWidth, object.object.frameHeight)" :key="object.id" v-bind="getObjectImageProps(object)" />
<Image v-for="object in zoneStore.zone?.zoneObjects" :depth="calculateIsometricDepth(object.positionX, object.positionY, object.object.frameWidth, object.object.frameHeight)" :key="object.id" v-bind="getObjectImageProps(object)" :flipX="object.isRotated" />
<!-- <Text v-for="object in zoneStore.zone?.zoneObjects" :key="object.id" :depth="99999" :text="Math.ceil(calculateIsometricDepth(object.positionX, object.positionY, object.object.frameWidth, object.object.frameHeight))" v-bind="getObjectProps(object)" />-->
</template>

View File

@ -38,14 +38,15 @@ function createTileLayer() {
const uniqueTiles = new Set(tilesFromZone.flat().filter(Boolean))
const tilesetImages = Array.from(uniqueTiles).map((tile, index) => {
return zoneTilemap.addTilesetImage(tile, tile, config.tile_size.x, config.tile_size.y, 0, 0, index + 1, { x: 0, y: -config.tile_size.y })
return zoneTilemap.addTilesetImage(tile, tile, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y })
}) as any
// Add blank tile
tilesetImages.push(zoneTilemap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 0, 0, 0, { x: 0, y: -config.tile_size.y }))
tilesetImages.push(zoneTilemap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 1, 2, 0, { x: 0, y: -config.tile_size.y }))
const layer = zoneTilemap.createBlankLayer('tiles', tilesetImages, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
layer.setDepth(0)
layer.setCullPadding(2, 2)
return layer
}

View File

@ -8,7 +8,7 @@
import { useScene } from 'phavuer'
import { useGameStore } from '@/stores/gameStore'
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 Tiles from '@/components/zone/Tiles.vue'
import Objects from '@/components/zone/Objects.vue'
@ -26,16 +26,6 @@ type zoneLoadData = {
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
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) => {
// If data is from the current user, don't add it to the store
if (data.id === gameStore.character?.id) return
zoneStore.addCharacter(data)
})
@ -69,6 +61,18 @@ gameStore.connection!.on('character:move', (data: ExtendedCharacterT) => {
zoneStore.updateCharacter(data)
})
onBeforeMount(() => {
gameStore.connection!.emit('zone:character:join', async (response: zoneLoadData) => {
// Fetch assets for new zone
await gameStore.fetchZoneAssets(response.zone.id)
await loadAssets(scene)
// Set zone and characters
zoneStore.setZone(response.zone)
zoneStore.setCharacters(response.characters)
})
})
onBeforeUnmount(() => {
zoneStore.reset()
gameStore.connection!.off('zone:character:teleport')

View File

@ -1,72 +1,83 @@
import { type Ref } from 'vue'
import { type Ref, ref } from 'vue'
import { getTile, tileToWorldXY } from '@/composables/zoneComposable'
import { useGameStore } from '@/stores/gameStore'
import config from '@/config'
export function useGamePointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
const gameStore = useGameStore()
const pointerStartPosition = ref({ x: 0, y: 0 })
const dragThreshold = 5 // pixels
function updateWaypoint(pointer: Phaser.Input.Pointer) {
const { x: px, y: py } = camera.getWorldPoint(pointer.x, pointer.y)
const pointerTile = getTile(px, py, layer)
waypoint.value.visible = !!pointerTile
if (!pointerTile) {
return
function updateWaypoint(worldX: number, worldY: number) {
const pointerTile = getTile(worldX, worldY, layer)
if (pointerTile) {
const worldPoint = tileToWorldXY(layer, pointerTile.x, pointerTile.y)
waypoint.value = {
visible: true,
x: worldPoint.positionX,
y: worldPoint.positionY + config.tile_size.y + 15
}
} else {
waypoint.value.visible = false
}
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) {
if (!gameStore.isPlayerDraggingCamera) {
return
}
const { x, y, prevPosition } = pointer
const { scrollX, scrollY, zoom } = camera
camera.setScroll(scrollX - (x - prevPosition.x) / zoom, scrollY - (y - prevPosition.y) / zoom)
function handlePointerDown(pointer: Phaser.Input.Pointer) {
pointerStartPosition.value = { x: pointer.x, y: pointer.y }
gameStore.setPlayerDraggingCamera(true)
}
function handlePointerMove(pointer: Phaser.Input.Pointer) {
dragZone(pointer)
updateWaypoint(pointer)
const { worldX, worldY } = pointer
updateWaypoint(worldX, worldY)
if (gameStore.isPlayerDraggingCamera) {
const distance = Phaser.Math.Distance.Between(pointerStartPosition.value.x, pointerStartPosition.value.y, pointer.x, pointer.y)
if (distance > dragThreshold) {
const { x, y, prevPosition } = pointer
const { scrollX, scrollY, zoom } = camera
camera.setScroll(scrollX - (x - prevPosition.x) / zoom, scrollY - (y - prevPosition.y) / zoom)
}
}
}
function clickTile(pointer: Phaser.Input.Pointer) {
const { x: px, y: py } = camera.getWorldPoint(pointer.x, pointer.y)
const pointerTile = getTile(px, py, layer)
function handlePointerUp(pointer: Phaser.Input.Pointer) {
const distance = Phaser.Math.Distance.Between(pointerStartPosition.value.x, pointerStartPosition.value.y, pointer.x, pointer.y)
if (!pointerTile) {
return
if (distance <= dragThreshold) {
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', {
positionX: pointerTile.x,
positionY: pointerTile.y
})
gameStore.setPlayerDraggingCamera(false)
}
function handleZoom({ event, deltaY }: Phaser.Input.Pointer) {
if (event instanceof WheelEvent && event.shiftKey) {
return
function handleZoom(pointer: Phaser.Input.Pointer) {
if (pointer.event instanceof WheelEvent && pointer.event.shiftKey) {
const deltaY = pointer.event.deltaY
let zoomLevel = camera.zoom - deltaY * 0.005
zoomLevel = Phaser.Math.Clamp(zoomLevel, 1, 3)
camera.setZoom(zoomLevel)
}
scene.scale.setZoom(scene.scale.zoom - deltaY * 0.01)
camera = scene.cameras.main
}
const setupPointerHandlers = () => {
scene.input.on(Phaser.Input.Events.POINTER_UP, clickTile)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
scene.input.on(Phaser.Input.Events.POINTER_WHEEL, handleZoom)
}
const cleanupPointerHandlers = () => {
scene.input.off(Phaser.Input.Events.POINTER_UP, clickTile)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
scene.input.off(Phaser.Input.Events.POINTER_UP, handlePointerUp)
scene.input.off(Phaser.Input.Events.POINTER_WHEEL, handleZoom)
}

View File

@ -1,8 +1,8 @@
import { computed, type Ref, ref } from 'vue'
import { computed, type Ref } from 'vue'
import { getTile, tileToWorldXY } from '@/composables/zoneComposable'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import config from '@/config'
import { useGameStore } from '@/stores/gameStore'
import config from '@/config'
export function useZoneEditorPointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
const gameStore = useGameStore()
@ -13,14 +13,16 @@ export function useZoneEditorPointerHandlers(scene: Phaser.Scene, layer: Phaser.
const { x: px, y: py } = camera.getWorldPoint(pointer.x, pointer.y)
const pointerTile = getTile(px, py, layer)
waypoint.value.visible = !!pointerTile
if (!pointerTile) {
return
if (pointerTile) {
const worldPoint = tileToWorldXY(layer, pointerTile.x, pointerTile.y)
waypoint.value = {
visible: true,
x: worldPoint.positionX,
y: worldPoint.positionY + config.tile_size.y + 15
}
} else {
waypoint.value.visible = false
}
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) {
@ -38,13 +40,14 @@ export function useZoneEditorPointerHandlers(scene: Phaser.Scene, layer: Phaser.
updateWaypoint(pointer)
}
function handleZoom({ event, deltaY }: Phaser.Input.Pointer) {
if (event! instanceof WheelEvent && !event.shiftKey) {
return
function handleZoom(pointer: Phaser.Input.Pointer) {
if (pointer.event instanceof WheelEvent && pointer.event.shiftKey) {
const deltaY = pointer.event.deltaY
let zoomLevel = camera.zoom - deltaY * 0.005
if (zoomLevel > 0 && zoomLevel < 3) {
camera.setZoom(zoomLevel)
}
}
scene.scale.setZoom(scene.scale.zoom - deltaY * 0.01)
camera = scene.cameras.main
}
const setupPointerHandlers = () => {

View File

@ -1,36 +1,15 @@
import { useGameStore } from '@/stores/gameStore'
import { useScene } from 'phavuer'
import { watch } from 'vue'
import { useZoneStore } from '@/stores/zoneStore'
export function useCameraControls(scene: Phaser.Scene): any {
export function useCameraControls(scene: Phaser.Scene) {
const gameStore = useGameStore()
const zoneStore = useZoneStore()
const camera = scene.cameras.main
function onPointerDown(pointer: Phaser.Input.Pointer) {
if (pointer.event instanceof MouseEvent || pointer.event.shiftKey) {
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);
}
}
)
const onPointerDown = () => gameStore.setPlayerDraggingCamera(true)
const onPointerUp = () => gameStore.setPlayerDraggingCamera(false)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, onPointerDown)
scene.input.on(Phaser.Input.Events.POINTER_UP, onPointerUp)
return {
camera
}
return { camera }
}

View File

@ -1,33 +1,23 @@
import { computed, type Ref, watch } from 'vue'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { useGamePointerHandlers } from '@/composables/pointerHandlers/useGamePointerHandlers'
import { useZoneEditorPointerHandlers } from '@/composables/pointerHandlers/useZoneEditorPointerHandlers'
import { useGameStore } from '@/stores/gameStore'
import { useGamePointerHandlers } from './pointerHandlers/useGamePointerHandlers'
import { useZoneEditorPointerHandlers } from './pointerHandlers/useZoneEditorPointerHandlers'
export function usePointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Ref<Phaser.Cameras.Scene2D.Camera>, isDragging: Ref<boolean>) {
const gameStore = useGameStore()
export function usePointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
const zoneEditorStore = useZoneEditorStore()
const gameHandlers = useGamePointerHandlers(scene, layer, waypoint, camera, isDragging)
const zoneEditorHandlers = useZoneEditorPointerHandlers(scene, layer, waypoint, camera, isDragging)
const gameHandlers = useGamePointerHandlers(scene, layer, waypoint, camera)
const zoneEditorHandlers = useZoneEditorPointerHandlers(scene, layer, waypoint, camera)
const currentHandlers = computed(() => (zoneEditorStore.active ? zoneEditorHandlers : gameHandlers))
const setupPointerHandlers = () => {
currentHandlers.value.setupPointerHandlers()
}
const cleanupPointerHandlers = () => {
currentHandlers.value.cleanupPointerHandlers()
}
const setupPointerHandlers = () => currentHandlers.value.setupPointerHandlers()
const cleanupPointerHandlers = () => currentHandlers.value.cleanupPointerHandlers()
watch(
() => zoneEditorStore.active,
(newValue, oldValue) => {
if (newValue !== oldValue) {
cleanupPointerHandlers()
setupPointerHandlers()
}
() => {
cleanupPointerHandlers()
setupPointerHandlers()
}
)

View File

@ -1,12 +1,9 @@
const dev: boolean = true
export default {
name: 'New Quest',
development: dev,
server_endpoint: dev ? 'http://localhost:4000' : 'https://nq-server.cr-a.directonline.io',
tile_size: { x: 64, y: 32, z: 1 }
name: import.meta.env.VITE_NAME,
development: import.meta.env.VITE_DEVELOPMENT === 'true',
server_endpoint: import.meta.env.VITE_SERVER_ENDPOINT,
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
*/

View File

@ -1,5 +1,5 @@
<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="ui-wrapper h-dvh flex flex-col justify-center items-center gap-20 px-10 sm:px-20">
<div class="filler"></div>
@ -8,14 +8,16 @@
<div
v-for="character in characters"
:key="character.id"
class="group first:ml-auto last:mr-auto m-4 w-[170px] h-[275px] flex flex-col shrink-0 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 }"
>
<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" />
<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
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="
() => {
deletingCharacter = character
@ -28,15 +30,15 @@
<div class="sprite-container flex flex-col items-center m-auto">
<img class="drop-shadow-20" draggable="false" src="/assets/avatar/default/0.png" />
</div>
<span class="absolute bottom-5 w-full text-center translate-y-1/2 z-10 drop-shadow-text">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>
<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-gray-500 rounded-[3px] left-1/2 -bottom-4 -translate-x-1/2 transition-all ease-in-out duration-300"></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">
<div class="filler"></div>
<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>
</div>
</div>
@ -46,13 +48,13 @@
<div class="button-wrapper flex gap-8" v-if="!isLoading">
<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()"
>
<img class="h-8 drop-shadow-20 rotate-180" draggable="false" src="/assets/icons/arrow.svg" alt="Logout icon" />
</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"
@click="select_character()"
>
@ -66,7 +68,7 @@
<!-- CREATE CHARACTER MODAL -->
<Modal :isModalOpen="isModalOpen" @modal:close="isModalOpen = false">
<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 #modalBody>
@ -74,7 +76,7 @@
<form method="post" @submit.prevent="create" class="inline">
<div class="form-field-full">
<label for="name">Name</label>
<input class="input-cyan max-w-64" v-model="name" name="name" id="name" />
<input class="input-field max-w-64" v-model="name" name="name" id="name" />
</div>
<button class="btn-cyan py-1.5 px-4 mr-5 min-w-24 inline-block" type="submit">CREATE</button>
</form>
@ -86,7 +88,7 @@
<!-- 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">
<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 #modalBody>
You are about to delete <span class="font-extrabold">{{ deletingCharacter.name }}</span

View File

@ -1,25 +1,22 @@
<template>
<div class="flex justify-center items-center h-dvh relative">
<GmTools v-if="isLoaded && gameStore.character?.role === 'gm'" />
<GmPanel v-if="isLoaded && gameStore.character?.role === 'gm'" />
<Inventory />
<GmTools v-if="gameStore.character?.role === 'gm'" />
<GmPanel v-if="gameStore.character?.role === 'gm'" />
<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">
<div class="fixed inset-x-0 top-0 flex justify-start items-end p-10 pointer-events-none">
<div class="pointer-events-auto">
<Hud />
</div>
</div>
<Zone v-if="isLoaded" />
<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">
<div class="pointer-events-auto w-full">
<Chat />
</div>
<div class="pointer-events-auto max-xs:m-auto mr-auto">
<Menubar />
</div>
<div v-if="isLoaded">
<Menu />
<Hud />
<Keybindings />
<Minimap />
<Zone />
<Chat />
<Inventory />
<ExpBar />
<Effects />
</div>
</Scene>
</Game>
@ -27,7 +24,7 @@
<div v-if="zoneEditorStore.active">
<Game :config="gameConfig" @create="createGame">
<Scene name="main" @preload="preloadScene" @create="createScene">
<ZoneEditor v-if="isLoaded" :key="JSON.stringify(zoneEditorStore.zone)" />
<ZoneEditor v-if="isLoaded" :key="JSON.stringify(`${zoneEditorStore.zone?.id}_${zoneEditorStore.zone?.createdAt}_${zoneEditorStore.zone?.updatedAt}`)" />
</Scene>
</Game>
</div>
@ -40,15 +37,20 @@ import { ref, onBeforeUnmount } from 'vue'
import { Game, Scene } from 'phavuer'
import { useGameStore } from '@/stores/gameStore'
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 Zone from '@/components/zone/Zone.vue'
import Keybindings from '@/components/gui/Keybindings.vue'
import Chat from '@/components/gui/Chat.vue'
import Menubar from '@/components/gui/Menu.vue'
import GmTools from '@/components/gameMaster/GmTools.vue'
import ZoneEditor from '@/components/gameMaster/zoneEditor/ZoneEditor.vue'
import GmPanel from '@/components/gameMaster/GmPanel.vue'
import Inventory from '@/components/gui/UserPanel.vue'
import Effects from '@/components/Effects.vue'
import { loadAssets } from '@/composables/zoneComposable'
import Minimap from '@/components/gui/Minimap.vue'
const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore()
@ -59,14 +61,7 @@ const gameConfig = {
width: window.innerWidth,
height: window.innerHeight,
type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS
scale: {
mode: Phaser.Scale.RESIZE,
autoCenter: Phaser.Scale.CENTER_BOTH,
width: window.innerWidth,
height: window.innerHeight
},
resolution: 2,
pixelArt: true
resolution: 5
}
const createGame = (game: Phaser.Game) => {
@ -76,9 +71,20 @@ const createGame = (game: Phaser.Game) => {
addEventListener('resize', () => {
game.scale.resize(window.innerWidth, window.innerHeight)
})
// We don't support canvas mode, only WebGL
if (game.renderer.type === Phaser.CANVAS) {
gameStore.addNotification({
title: 'Warning',
message: 'Your browser does not support WebGL. Please use a modern browser like Chrome, Firefox, or Edge.'
})
gameStore.disconnectSocket()
}
}
const preloadScene = async (scene: Phaser.Scene) => {
isLoaded.value = false
/**
* Create loading bar
*/
@ -122,10 +128,6 @@ const preloadScene = async (scene: Phaser.Scene) => {
scene.load.image('blank_tile', '/assets/zone/blank_tile.png')
scene.load.image('blank_object', '/assets/zone/blank_tile.png')
scene.load.image('waypoint', '/assets/waypoint.png')
scene.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
@ -144,14 +146,22 @@ const createScene = async (scene: Phaser.Scene) => {
scene.anims.create({
key: asset.key,
frameRate: 7,
/** @TODO: Fix end, which is total amount of frames */
frames: scene.anims.generateFrameNumbers(asset.key, { start: 0, end: 4 }),
frames: scene.anims.generateFrameNumbers(asset.key, { start: 0, end: asset.frameCount! - 1 }),
repeat: -1
})
})
}
onBeforeUnmount(() => {
isLoaded.value = false
gameStore.disconnectSocket()
})
</script>
<style lang="scss">
canvas {
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-crisp-edges;
image-rendering: pixelated;
}
</style>

View File

@ -1,28 +1,66 @@
<template>
<div class="bg-gray-300">
<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="z-20 w-full h-dvh flex items-center justify-between flex-col relative">
<div class="filler"></div>
<h1 class="mt-28 text-center text-6xl">NEW QUEST</h1>
<form @submit.prevent="loginFunc">
<div class="my-20 mx-0 w-full flex flex-col gap-6">
<div class="w-full grid gap-4">
<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">
<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 />
<div class="relative max-lg:h-dvh">
<div class="lg:bg-gradient-to-r bg-gradient-to-b from-gray-900 to-transparent w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute right-0 max-lg:bottom-0 lg:top-0 z-10"></div>
<div class="bg-[url('/assets/login/login-bg.png')] w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute right-0 max-lg:bottom-0 lg:top-0 bg-no-repeat bg-cover bg-center"></div>
<div class="bg-gray-900 z-20 w-full lg:w-1/2 h-[65dvh] lg:h-dvh relative">
<div class="h-dvh flex items-center lg:justify-center flex-col px-8 max-lg:pt-20">
<img src="/assets/login/nq-logo-v1.png" class="mb-10" />
<div class="relative">
<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 top-2 w-[calc(100%_-_16px)] h-[calc(100%_-_16px)] max-lg:hidden" />
<!-- Login Form -->
<form v-show="switchForm === 'login'" @submit.prevent="loginFunc" class="relative px-6 py-11">
<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 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">
<label class="text-black bg-white/50 p-1 text-sm rounded-t-[3px]" for="password">Password</label>
<input class="p-1 text-sm focus-visible:outline-none" id="password" v-model="password" type="password" name="password" required />
<div class="pt-8">
<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>
</div>
</div>
<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>
<button class="btn-cyan py-2 px-0 min-w-24" type="button" @click.prevent="registerFunc"><span class="m-auto">REGISTER</span></button>
<button class="btn-cyan py-2 px-0 min-w-24"><span class="m-auto">CREDITS</span></button>
</div>
</form>
<!-- Register Form -->
<form v-show="switchForm === 'register'" @submit.prevent="registerFunc" class="relative px-6 py-11">
<div class="flex flex-col gap-5 p-2 mb-8 relative">
<div class="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>
</form>
</div>
</div>
</div>
</template>
@ -36,7 +74,8 @@ import { useCookies } from '@vueuse/integrations/useCookies'
const gameStore = useGameStore()
const username = ref('')
const password = ref('')
const switchForm = ref('login')
const loginError = ref('')
// automatic login because of development
onMounted(async () => {
const token = useCookies().get('token')
@ -49,7 +88,7 @@ onMounted(async () => {
async function loginFunc() {
// check if username and password are valid
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
}
@ -57,10 +96,9 @@ async function loginFunc() {
const response = await login(username.value, password.value)
if (response.success === undefined) {
gameStore.addNotification({ message: response.error })
loginError.value = response.error
return
}
gameStore.setToken(response.token)
gameStore.initConnection()
return true // Indicate success
@ -69,7 +107,7 @@ async function loginFunc() {
async function registerFunc() {
// check if username and password are valid
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
}
@ -77,13 +115,14 @@ async function registerFunc() {
const response = await register(username.value, password.value)
if (response.success === undefined) {
gameStore.addNotification({ message: response.error })
loginError.value = response.error
return
}
const loginSuccess = await loginFunc()
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>

View File

@ -5,19 +5,26 @@ import config from '@/config'
import { useCookies } from '@vueuse/integrations/useCookies'
export const useGameStore = defineStore('game', {
state: () => ({
notifications: [] as Notification[],
assets: [] as Asset[],
token: '' as string | null,
connection: null as Socket | null,
user: null as User | null,
character: null as Character | null,
isGmPanelOpen: false,
isPlayerDraggingCamera: false,
isCameraFollowingCharacter: false,
isChatOpen: false,
isUserPanelOpen: false
}),
state: () => {
return {
loginMessage: null as string | null,
notifications: [] as Notification[],
assets: [] as Asset[],
token: '' as string | null,
connection: null as Socket | null,
user: null as User | null,
character: null as Character | null,
isPlayerDraggingCamera: false,
gameSettings: {
isCameraFollowingCharacter: false
},
uiSettings: {
isChatOpen: false,
isUserPanelOpen: false,
isGmPanelOpen: false
}
}
},
getters: {
getNotifications: (state: any) => state.notifications,
getAssetByKey: (state) => {
@ -92,7 +99,7 @@ export const useGameStore = defineStore('game', {
this.character = character
},
toggleGmPanel() {
this.isGmPanelOpen = !this.isGmPanelOpen
this.uiSettings.isGmPanelOpen = !this.uiSettings.isGmPanelOpen
},
togglePlayerDraggingCamera() {
this.isPlayerDraggingCamera = !this.isPlayerDraggingCamera
@ -101,16 +108,16 @@ export const useGameStore = defineStore('game', {
this.isPlayerDraggingCamera = moving
},
toggleCameraFollowingCharacter() {
this.isCameraFollowingCharacter = !this.isCameraFollowingCharacter
this.gameSettings.isCameraFollowingCharacter = !this.gameSettings.isCameraFollowingCharacter
},
setCameraFollowingCharacter(following: boolean) {
this.isCameraFollowingCharacter = following
this.gameSettings.isCameraFollowingCharacter = following
},
toggleChat() {
this.isChatOpen = !this.isChatOpen
this.uiSettings.isChatOpen = !this.uiSettings.isChatOpen
},
toggleUserPanel() {
this.isUserPanelOpen = !this.isUserPanelOpen
this.uiSettings.isUserPanelOpen = !this.uiSettings.isUserPanelOpen
},
initConnection() {
this.connection = io(config.server_endpoint, {
@ -151,10 +158,11 @@ export const useGameStore = defineStore('game', {
this.token = null
this.user = null
this.character = null
this.isGmPanelOpen = false
this.uiSettings.isGmPanelOpen = false
this.isPlayerDraggingCamera = false
this.isChatOpen = false
this.isUserPanelOpen = false
this.gameSettings.isCameraFollowingCharacter = false
this.uiSettings.isChatOpen = false
this.uiSettings.isUserPanelOpen = false
}
}
})

View File

@ -1,8 +1,8 @@
import { defineStore } from 'pinia'
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
toPositionX: number
toPositionY: number
@ -10,37 +10,40 @@ type TeleportSettings = {
}
export const useZoneEditorStore = defineStore('zoneEditor', {
state: () => ({
active: false,
zone: null as Zone | null,
tool: 'move',
drawMode: 'tile',
eraserMode: 'tile',
zoneList: [] as Zone[],
tileList: [] as Tile[],
objectList: [] as Object[],
selectedTile: null as Tile | null,
selectedObject: null as Object | null,
selectedZoneObject: null as ZoneObject | null,
objectDepth: 0,
isTileListModalShown: false,
isObjectListModalShown: false,
isZoneListModalShown: false,
isCreateZoneModalShown: false,
isSettingsModalShown: false,
zoneSettings: {
name: '',
width: 0,
height: 0,
pvp: false
},
teleportSettings: {
toZoneId: 0,
toPositionX: 0,
toPositionY: 0,
toRotation: 0
state: () => {
return {
active: false,
zone: null as Zone | null,
tool: 'move',
drawMode: 'tile',
eraserMode: 'tile',
zoneList: [] as Zone[],
tileList: [] as Tile[],
objectList: [] as Object[],
selectedTile: null as Tile | null,
selectedObject: null as Object | null,
selectedZoneObject: null as ZoneObject | null,
objectDepth: 0,
isTileListModalShown: false,
isObjectListModalShown: false,
isZoneListModalShown: false,
isCreateZoneModalShown: false,
isSettingsModalShown: false,
zoneSettings: {
name: '',
width: 0,
height: 0,
pvp: false,
effects: [] as ZoneEffects[]
},
teleportSettings: {
toZoneId: 0,
toPositionX: 0,
toPositionY: 0,
toRotation: 0
} as TeleportSettings
}
}),
},
actions: {
toggleActive() {
const gameStore = useGameStore()
@ -64,6 +67,10 @@ export const useZoneEditorStore = defineStore('zoneEditor', {
if (!this.zone) return
this.zone.pvp = pvp
},
setZoneEffects(zoneEffects: ZoneEffects) {
if (!this.zone) return
this.zone.zoneEffects = zoneEffects
},
setTool(tool: string) {
this.tool = tool
},

View File

@ -2,11 +2,13 @@ import { defineStore } from 'pinia'
import type { ExtendedCharacter, Zone } from '@/types'
export const useZoneStore = defineStore('zone', {
state: () => ({
zone: null as Zone | null,
characters: [] as ExtendedCharacter[],
characterLoaded: false,
}),
state: () => {
return {
zone: null as Zone | null,
characters: [] as ExtendedCharacter[],
characterLoaded: false
}
},
getters: {
getCharacterById: (state) => {
return (id: number) => state.characters.find((char) => char.id === id)
@ -37,9 +39,13 @@ export const useZoneStore = defineStore('zone', {
removeCharacter(character_id: number) {
this.characters = this.characters.filter((char) => char.id !== character_id)
},
setCharacterLoaded(loaded: boolean) {
this.characterLoaded = loaded
},
reset() {
this.zone = null
this.characters = []
this.characterLoaded = false
}
}
})

View File

@ -1,5 +1,6 @@
export type Notification = {
id?: string
title?: string
message?: string
}
@ -7,6 +8,7 @@ export type Asset = {
key: string
url: string
group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
frameCount?: number
frameWidth?: number
frameHeight?: number
}
@ -51,6 +53,7 @@ export type Zone = {
height: number
tiles: any | null
pvp: boolean
zoneEffects: ZoneEffects
zoneEventTiles: ZoneEventTile[]
zoneObjects: ZoneObject[]
characters: Character[]
@ -59,6 +62,14 @@ export type Zone = {
updatedAt: Date
}
export type ZoneEffects = {
id: string
zoneId: number
zone: Zone
effect: string
strength: number
}
export type ZoneObject = {
id: string
zoneId: number
@ -66,6 +77,7 @@ export type ZoneObject = {
objectId: string
object: Object
depth: number
isRotated: boolean
positionX: number
positionY: number
}

View File

@ -4,9 +4,11 @@ export default {
theme: {
fontFamily: {
'titles': ['Poppins', 'serif'],
'default': ['Inter', 'serif']
'default': ['Quicksand', 'serif']
},
backgroundSize: {
'cover': 'cover',
'contain': 'contain',
'30px': '30px'
},
screens: {
@ -47,16 +49,23 @@ export default {
'character': '0 4px 30px rgba(0, 0, 0, 0.1)',
},
colors: {
red: {
DEFAULT: '#d50000',
50: '#d50000',
100: '#b30000'
cyan: {
DEFAULT: '#0D6d69',
50: '#f3faf8',
100: '#D7F0EC',
200: '#b1dfd9',
300: '#80c8c1',
600: '#0D6D69',
800: '#244b4c',
900: '#204040',
950: '#0f2324'
},
bordeaux: {
DEFAULT: '#800020',
50: '#cc0033',
100: '#800020',
200: '#4c0000'
red: {
DEFAULT: '#e15970',
100: '#ffefef',
200: '#e15970',
300: '#b73f54',
400: '#332426'
},
blue: {
DEFAULT: '#00c2ff'
@ -67,20 +76,20 @@ export default {
purple: {
DEFAULT: '#9841e6'
},
cyan: {
DEFAULT: '#368f8b',
50: '#00b3b3',
100: '#368f8b',
200: '#376362'
},
gray: {
DEFAULT: '#7f7f7f',
50: '#d3d3d3',
100: '#7f7f7f',
200: '#696969',
300: '#313638',
500: '#778899',
600: '#B1B2B5'
DEFAULT: '#262626',
50: '#fcfcfd',
100: '#f7f7f8',
150: '#f1f1f3',
175: '#e4e4e7',
200: '#999999',
300: '#808080',
400: '#666666',
500: '#4d4d4d',
600: '#333333',
700: '#262626',
800: '#1a1a1a',
900: '#0d0d0d'
}
}
},