Compare commits

...

72 Commits

Author SHA1 Message Date
8b98fc5c4e More WIP ingame UI
Temporary skill icons, adjusted borders globally
2024-10-13 20:27:44 +02:00
934ae50d8e Updated global btn + input styling 2024-10-13 17:25:46 +02:00
7504e3719e WIP ingame ui updated design
Cleaned up some old icons and styling
2024-10-13 17:21:45 +02:00
34bd103ec2 Reorderered components 2024-10-13 12:27:16 +02:00
295ce98e33 Merge remote-tracking branch 'origin/feature/137-zone-effect' into feature/new-design-FE 2024-10-13 12:25:28 +02:00
3c7e96ea7f Setup skeleton for new HUD, updated fog img, updated effects 2024-10-13 12:25:13 +02:00
1cfdf1857e Updated close icon, more styling changes 2024-10-12 22:20:25 +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
93 changed files with 1918 additions and 1144 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 ./ COPY package*.json ./
RUN npm ci RUN npm ci
COPY . . COPY . .
# Set environment variables
ARG VITE_NAME=${VITE_NAME}
ENV VITE_NAME=${VITE_NAME}
ARG VITE_DEVELOPMENT=${VITE_DEVELOPMENT}
ENV VITE_DEVELOPMENT=${VITE_DEVELOPMENT}
ARG VITE_SERVER_ENDPOINT=${VITE_SERVER_ENDPOINT}
ENV VITE_SERVER_ENDPOINT=${VITE_SERVER_ENDPOINT}
ARG VITE_TILE_SIZE_X=${VITE_TILE_SIZE_X}
ENV VITE_TILE_SIZE_X=${VITE_TILE_SIZE_X}
ARG VITE_TILE_SIZE_Y=${VITE_TILE_SIZE_Y}
ENV VITE_TILE_SIZE_Y=${VITE_TILE_SIZE_Y}
# Build the application
RUN npm run build-ntc RUN npm run build-ntc
# Production stage # Production stage

1137
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

BIN
public/assets/fog.png Normal file

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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 597 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" id="screenshot-7904a66d-0c2c-80b0-8004-7aa8e4dbc6e0" viewBox="5853.755 6642.086 23.788 23.788" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-7904a66d-0c2c-80b0-8004-7aa8e4dbc6e0"><g class="fills" id="fills-7904a66d-0c2c-80b0-8004-7aa8e4dbc6e0"><path d="M5867.837,6654.000L5877.045,6644.792C5877.199,6644.649,5877.322,6644.476,5877.408,6644.284C5877.493,6644.093,5877.539,6643.886,5877.543,6643.676C5877.546,6643.466,5877.508,6643.258,5877.429,6643.063C5877.350,6642.869,5877.234,6642.692,5877.085,6642.544C5876.937,6642.395,5876.760,6642.278,5876.565,6642.200C5876.371,6642.121,5876.163,6642.082,5875.953,6642.086C5875.743,6642.090,5875.536,6642.136,5875.344,6642.221C5875.153,6642.307,5874.980,6642.430,5874.837,6642.583L5865.629,6651.792L5856.420,6642.583C5856.124,6642.307,5855.732,6642.157,5855.328,6642.164C5854.923,6642.171,5854.537,6642.335,5854.250,6642.622C5853.964,6642.908,5853.800,6643.294,5853.793,6643.699C5853.786,6644.104,5853.936,6644.495,5854.212,6644.792L5863.420,6654.000L5854.212,6663.208C5853.919,6663.501,5853.755,6663.898,5853.755,6664.312C5853.755,6664.726,5853.919,6665.124,5854.212,6665.417C5854.505,6665.709,5854.902,6665.874,5855.316,6665.874C5855.730,6665.874,5856.127,6665.709,5856.420,6665.417L5865.629,6656.208L5874.837,6665.417C5875.130,6665.709,5875.527,6665.874,5875.941,6665.874C5876.355,6665.874,5876.753,6665.709,5877.045,6665.417C5877.338,6665.124,5877.502,6664.726,5877.502,6664.312C5877.502,6663.898,5877.338,6663.501,5877.045,6663.208L5867.837,6654.000ZZ" style="fill: rgb(255, 255, 255);"/></g></g></svg> <svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.91481 9.08504C7.71955 9.2803 7.40297 9.2803 7.20771 9.08504L5.55709 7.43442C5.36183 7.23916 5.04524 7.23916 4.84998 7.43442L2.99578 9.28862C2.80052 9.48389 2.48393 9.48389 2.28867 9.28863L1.00416 8.00412C0.808899 7.80885 0.808899 7.49227 1.00416 7.29701L2.85837 5.4428C3.05363 5.24754 3.05363 4.93096 2.85837 4.7357L0.914865 2.7922C0.719603 2.59693 0.719603 2.28035 0.914865 2.08509L2.07053 0.929423C2.26579 0.734161 2.58238 0.734162 2.77764 0.929424L4.72114 2.87293C4.9164 3.06819 5.23298 3.06819 5.42825 2.87293L7.297 1.00417C7.49226 0.808906 7.80885 0.808906 8.00411 1.00417L9.28862 2.28868C9.48388 2.48394 9.48388 2.80052 9.28862 2.99578L7.41986 4.86454C7.2246 5.0598 7.2246 5.37639 7.41986 5.57165L9.07048 7.22227C9.26574 7.41753 9.26574 7.73411 9.07048 7.92937L7.91481 9.08504Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 915 B

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: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 600 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 600 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 599 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

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 +0,0 @@
<svg width="290" xmlns="http://www.w3.org/2000/svg" height="87" id="screenshot-e9942e24-155b-8096-8004-7eb5ea5d2669" viewBox="0 0 290 87" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-e9942e24-155b-8096-8004-7eb5ea5d2669"><g class="fills" id="fills-e9942e24-155b-8096-8004-7eb5ea5d2669"><path d="M286.515,78.278C282.911,83.543,276.856,87.000,270.000,87.000L20.000,87.000C8.962,87.000,0.000,78.038,0.000,67.000L0.000,20.000C0.000,8.962,8.962,0.000,20.000,0.000L270.000,0.000C276.834,0.000,282.872,3.435,286.480,8.671C268.843,10.411,255.000,25.352,255.000,43.500C255.000,61.610,268.784,76.525,286.515,78.278ZM290.000,20.000L290.000,67.000" style="fill: #fff; fill-opacity: 1;"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 726 B

View File

@ -1 +0,0 @@
<svg width="290" xmlns="http://www.w3.org/2000/svg" height="87" id="screenshot-e38d8c7f-bba0-801b-8004-7d6eeffceb00" viewBox="4058.354 6110 290 87" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-e38d8c7f-bba0-801b-8004-7d6eeffceb00"><g class="fills" id="fills-e38d8c7f-bba0-801b-8004-7d6eeffceb00"><path d="M4061.840,6118.722C4065.444,6113.457,4071.499,6110.000,4078.354,6110.000L4328.354,6110.000C4339.393,6110.000,4348.354,6118.962,4348.354,6130.000L4348.354,6177.000C4348.354,6188.038,4339.393,6197.000,4328.354,6197.000L4078.354,6197.000C4071.521,6197.000,4065.483,6193.565,4061.875,6188.329C4079.512,6186.589,4093.354,6171.648,4093.354,6153.500C4093.354,6135.390,4079.571,6120.475,4061.840,6118.722ZM4058.354,6177.000L4058.354,6130.000" style="fill: #fff; fill-opacity: 1;"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 829 B

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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 598 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 471 KiB

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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 470 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 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 return 'login' // default fallback
}) })
// Disable right click
addEventListener('contextmenu', (event) => event.preventDefault())
</script> </script>

Binary file not shown.

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,15 +1,15 @@
<template> <template>
<div class="flex flex-col items-center py-5 px-3 fixed bottom-14 right-0"> <div class="flex flex-col items-center py-5 px-3 fixed bottom-14 right-0" v-if="zoneEditorStore.selectedZoneObject">
<div class="self-end mt-2 flex gap-2"> <div class="self-end mt-2 flex gap-2">
<div> <div>
<label class="mb-1.5 font-titles block text-sm text-gray-700 hidden" for="depth">Depth</label> <label class="mb-1.5 font-titles block text-sm text-gray-700 hidden" for="depth">Depth</label>
<input v-model="objectDepth" @mousedown.stop @input="handleDepthInput" class="input-cyan max-w-24 px-2 py-1 border rounded" type="number" name="depth" placeholder="Depth" :disabled="!isObjectSelected" /> <input v-model="objectDepth" @mousedown.stop @input="handleDepthInput" class="input-field max-w-24 px-2 py-1 border rounded" type="number" name="depth" placeholder="Depth" />
</div> </div>
<button @mousedown.stop @click="handleDelete" class="btn-bordeaux py-1.5 px-4" :disabled="!isObjectSelected"> <button @mousedown.stop @click="handleDelete" class="btn-red py-1.5 px-4">
<img src="/assets/icons/trashcan.svg" class="w-4 h-4" alt="Delete" /> <img src="/assets/icons/trashcan.svg" class="w-4 h-4" alt="Delete" />
</button> </button>
<button @mousedown.stop @click="zoneEditorStore.setSelectedObject(zoneEditorStore.selectedZoneObject?.object)" class="btn-cyan py-1.5 px-4" :disabled="!isObjectSelected">S</button> <button @mousedown.stop @click="handleRotate" class="btn-cyan py-1.5 px-4">Rotate</button>
<button @mousedown.stop @click="handleMove" class="btn-cyan py-1.5 px-4 min-w-24" :disabled="!isObjectSelected">Move</button> <button @mousedown.stop @click="handleMove" class="btn-cyan py-1.5 px-4 min-w-24">Move</button>
</div> </div>
</div> </div>
</template> </template>
@ -18,13 +18,11 @@
import { useZoneEditorStore } from '@/stores/zoneEditorStore' import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { ref, computed, watch } from 'vue' import { ref, computed, watch } from 'vue'
const emit = defineEmits(['update_depth', 'move', 'delete']) const emit = defineEmits(['update_depth', 'move', 'delete', 'rotate'])
const zoneEditorStore = useZoneEditorStore() const zoneEditorStore = useZoneEditorStore()
const objectDepth = ref(zoneEditorStore.objectDepth) const objectDepth = ref(zoneEditorStore.objectDepth)
const isObjectSelected = computed(() => !!zoneEditorStore.selectedZoneObject)
watch( watch(
() => zoneEditorStore.selectedZoneObject, () => zoneEditorStore.selectedZoneObject,
(selectedZoneObject) => { (selectedZoneObject) => {
@ -39,6 +37,10 @@ const handleDepthInput = () => {
} }
} }
const handleRotate = () => {
emit('rotate', zoneEditorStore.selectedZoneObject?.id)
}
const handleMove = () => { const handleMove = () => {
emit('move') emit('move')
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,14 +1,15 @@
<template> <template>
<div class="w-full md:min-w-[350px] max-w-[750px] flex flex-col"> <div class="w-full md:min-w-[350px] max-w-[750px] flex flex-col absolute left-1/2 -translate-x-1/2 bottom-5">
<div ref="chatWindow" class="w-full overflow-auto h-32 mb-5 bg-gray-300/80 rounded-lg border-2 border-solid border-cyan-200" v-show="gameStore.isChatOpen"> <div ref="chatWindow" class="w-full overflow-auto h-32 mb-5 bg-gray rounded-md border-2 border-solid border-gray-500 text-gray-300" v-show="gameStore.uiSettings.isChatOpen">
<div v-for="message in chats" class="flex-col py-2 items-center p-3"> <div v-for="message in chats" class="flex-col py-2 items-center p-3">
<span class="text-ellipsis overflow-hidden whitespace-nowrap text-sm">{{ message.character.name }}</span> <span class="text-ellipsis overflow-hidden whitespace-nowrap text-sm" :class="{'text-cyan-300': gameStore.character?.role == 'gm'}">{{ message.character.name }}</span>
<p class="text-gray-50 m-0">{{ message.message }}</p> <p class="text-gray-50 m-0">{{ message.message }}</p>
</div> </div>
</div> </div>
<div class="w-full flex"> <div class="w-64 mx-auto relative">
<img src="/assets/icons/chat-icon.svg" class="absolute top-1/2 -translate-y-1/2 left-1.5 h-4 w-4 opacity-50" />
<input <input
class="w-full h-12 rounded-lg text-lg px-4 py-0 bg-gray-300/80 border-2 border-solid border-cyan-200 text-gray-50 bg-[url('/assets/icons/submit-icon.svg')] bg-no-repeat bg-[right_25px_center] bg-[length:30px] focus:outline-none focus:ring-0 focus:border-cyan-800" class="w-[204px] h-6 rounded-sm text-xs font-default pl-8 pr-4 py-0 bg-gray-600 border-2 border-solid border-gray-500 text-gray-300 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover focus:outline-none focus:ring-0 focus:border-cyan-800"
placeholder="Type something..." placeholder="Type something..."
v-model="message" v-model="message"
@keypress="handleKeyPress" @keypress="handleKeyPress"
@ -59,61 +60,63 @@ const scrollToBottom = () => {
}) })
} }
gameStore.connection?.on('chat:message', (data: ChatMessage) => { gameStore.connection?.on('chat:message', (data: ChatMessage) => {
chats.value.push(data) chats.value.push(data)
scrollToBottom()
if (!zoneStore.characterLoaded) return
const charChatContainer = scene.children.getByName(data.character.name + '_chatContainer') as Phaser.GameObjects.Container
if (!charChatContainer) return
const chatBubble = charChatContainer.getByName(data.character.name + '_chatBubble') as Phaser.GameObjects.Container
const chatText = charChatContainer.getByName(data.character.name + '_chatText') as Phaser.GameObjects.Text
if (!chatText || !chatBubble) return
function calculateTextWidth(text: string, font: string, fontSize: number): number {
// Create a canvas element
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (!context) {
throw new Error('Unable to create canvas context');
}
// Set the font
context.font = `${fontSize}px ${font}`;
// Measure the text width
const metrics = context.measureText(text);
return metrics.width;
}
chatBubble.width = calculateTextWidth(data.message, 'Arial', 13) + 10
chatText.setText(data.message)
charChatContainer.setVisible(true)
/**
* Hide chat bubble after a few seconds
*/
// Clear any existing hide timer
if (charChatContainer.getData('hideTimer')) {
clearTimeout(charChatContainer.getData('hideTimer'))
}
// Set a new hide timer
const hideTimer = setTimeout(() => {
charChatContainer.setVisible(false)
}, 3000)
// Store the timer on the container itself
charChatContainer.setData('hideTimer', hideTimer)
})
scrollToBottom() scrollToBottom()
if (!zoneStore.characterLoaded) return
const charChatContainer = scene.children.getByName(data.character.name + '_chatContainer') as Phaser.GameObjects.Container
if (!charChatContainer) return
const chatBubble = charChatContainer.getByName(data.character.name + '_chatBubble') as Phaser.GameObjects.Container
const chatText = charChatContainer.getByName(data.character.name + '_chatText') as Phaser.GameObjects.Text
if (!chatText || !chatBubble) return
function calculateTextWidth(text: string, font: string, fontSize: number): number {
// Create a canvas element
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
if (!context) {
throw new Error('Unable to create canvas context')
}
// Set the font
context.font = `${fontSize}px ${font}`
// Measure the text width
const metrics = context.measureText(text)
return metrics.width
}
chatBubble.width = calculateTextWidth(data.message.substring(0, 90), 'Arial', 13) + 30
// setText but with max. char limit of 90
chatText.setText(data.message.substring(0, 90))
charChatContainer.setVisible(true)
/**
* Hide chat bubble after a few seconds
*/
// Clear any existing hide timer
if (charChatContainer.getData('hideTimer')) {
clearTimeout(charChatContainer.getData('hideTimer'))
}
// Set a new hide timer
const hideTimer = setTimeout(() => {
charChatContainer.setVisible(false)
}, 3000)
// Store the timer on the container itself
charChatContainer.setData('hideTimer', hideTimer)
})
scrollToBottom()
onBeforeUnmount(() => { onBeforeUnmount(() => {
gameStore.connection?.off('chat:message') gameStore.connection?.off('chat:message')
}) })

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,16 @@
<template> <template>
<div class="hud-wrapper relative left-0 w-[310px] h-[84px]"> <div class="absolute left-[66px] top-4 bg-[url('/assets/ui-rect-border-4-corners.svg')] bg-no-repeat px-4 py-2 w-[181px] h-[26px] flex flex-col justify-between">
<div class="absolute 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"> <div class="w-full flex items-center gap-2">
<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" /> <label class="text-xs leading-3 text-pixel" for="hp">HP</label>
<progress class="h-2 rounded-sm w-full max-w-44 appearance-none accent-green" id="hp" :value="gameStore.character?.hitpoints" max="100">{{ gameStore.character?.hitpoints }}%</progress>
<span class="text-xs leading-3 text-pixel">{{ gameStore.character?.hitpoints }}%</span>
</div> </div>
<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="w-full flex items-center gap-2">
<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"> <label class="text-xs leading-3 text-pixel" for="sp">SP</label>
<div class="h-16 flex flex-col items-end py-2.5 pl-12 pr-5"> <progress class="h-2 rounded-sm w-full max-w-44 appearance-none accent-blue" id="sp" :value="gameStore.character?.mana" max="100">{{ gameStore.character?.mana }}%</progress>
<div class="w-full flex items-center justify-between mb-1.5"> <span class="text-xs leading-3 text-pixel">{{ gameStore.character?.mana }}%</span>
<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>
</div> </div>
<!-- TODO: Replace gameStore.character with other (selected) player's -->
<!-- <div class="hud-wrapper other-player relative right-0 w-[310px] h-[84px]">-->
<!-- <div class="absolute w-14 h-14 bg-white/80 rounded-full border-3 border-solid border-white top-1/2 -translate-y-1/2 right-0 z-20">-->
<!-- <img class="w-7 absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 -scale-x-100" draggable="false" src="/assets/avatar/default/head.png" />-->
<!-- </div>-->
<!-- <div class="hud-bg absolute top-0 right-8 w-[280px] h-[84px] z-10 bg-[url('/assets/bg-hud-2.png')] bg-center bg-[length:cover] bg-no-repeat mask-[url('/assets/shapes/hud-image-shape.svg')] mask-center mask-[length:cover] mask-no-repeat"></div>-->
<!-- <div class="absolute top-0 right-8 w-[280px] h-[84px] z-10 -scale-x-100 bg-[url('/assets/shapes/hud-shape-empty.svg')] bg-center bg-[length:cover] bg-no-repeat">-->
<!-- <div class="h-16 flex flex-col items-end -scale-x-100 py-2.5 pr-12 pl-5">-->
<!-- <div class="w-full flex items-center justify-between mb-1.5">-->
<!-- <span class="text-ellipsis overflow-hidden whitespace-nowrap max-w-32 text-sm">{{ gameStore.character.name }}</span>-->
<!-- <span class="text-sm">lvl. {{ gameStore.character.level }}</span>-->
<!-- </div>-->
<!-- <div class="w-full flex items-center justify-between">-->
<!-- <label class="text-sm" for="hp">HP</label>-->
<!-- <progress class="h-2 rounded-lg w-full max-w-44 appearance-none accent-red" id="hp" :value="gameStore.character.hitpoints" max="100">{{ gameStore.character.hitpoints }}%</progress>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -50,41 +20,30 @@ const gameStore = useGameStore()
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.hud-wrapper { #hp {
.hud-bg { // Chrome, Safari, Edge, Opera
mask: url('/assets/shapes/hud-image-shape.svg') center/cover no-repeat; &::-webkit-progress-value {
@apply bg-gradient-to-r from-green from-75% to-green-200 rounded-sm;
} }
#hp { &::-webkit-progress-bar {
// Chrome, Safari, Edge, Opera @apply bg-white rounded-sm border border-solid border-black;
&::-webkit-progress-value {
@apply bg-red rounded-lg;
}
&::-webkit-progress-bar {
@apply bg-white rounded-lg border-2 border-solid border-white;
}
// Firefox
&::-moz-progress-bar {
@apply bg-red rounded-lg border-2 border-solid border-white;
}
} }
#mp { // Firefox
// Chrome, Safari, Edge, Opera &::-moz-progress-bar {
&::-webkit-progress-value { @apply bg-gradient-to-r from-green from-75% to-green-200 rounded-sm border border-solid border-black;
@apply bg-blue rounded-lg;
}
&::-webkit-progress-bar {
@apply bg-white rounded-lg border-2 border-solid border-white;
}
// Firefox
&::-moz-progress-bar {
@apply bg-blue rounded-lg border-2 border-solid border-white;
}
} }
}
&.other-player { #sp {
.hud-bg { // Chrome, Safari, Edge, Opera
mask: url('/assets/shapes/hud-image-shape-flipped.svg') center/cover no-repeat; &::-webkit-progress-value {
} @apply bg-gradient-to-r from-blue from-75% to-blue-200 rounded-sm;
}
&::-webkit-progress-bar {
@apply bg-white rounded-sm border border-solid border-black;
}
// Firefox
&::-moz-progress-bar {
@apply bg-gradient-to-r from-blue from-75% to-blue-200 rounded-sm border border-solid border-black;
} }
} }
</style> </style>

View File

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

View File

@ -1,39 +1,49 @@
<template> <template>
<ul class="list-none flex gap-2.5 items-center m-0 max-md:pl-0"> <ul class="list-none flex flex-col gap-2.5 items-center m-0 pl-0 absolute left-4 top-4">
<li class="menu-item group relative" @click="gameStore.toggleChat">
<div class="group-hover:block absolute bottom-16 left-1/2 -translate-x-1/2 w-20 h-6 text-center bg-gray-300 border-2 border-solid border-cyan rounded-3xl hidden">
<p class="absolute w-full bottom-0 m-0 text-xs leading-6">Chat</p>
<div class="group-hover:block absolute -bottom-2.5 bg-cyan h-2 w-3.5 [clip-path:polygon(100%_0,_0_0,_50%_100%)] left-1/2 -translate-x-1/2 hidden"></div>
</div>
<a class="group-hover:bg-gray/70 group-hover:cursor-pointer p-1.5 bg-gray-300/70 border-2 border-solid border-cyan-200 hover:border-cyan rounded-lg block w-11 h-9">
<img class="group-hover:drop-shadow-default w-11 h-9 object-contain" draggable="false" src="/assets/icons/chat.png" />
</a>
</li>
<li class="menu-item group relative"> <li class="menu-item group relative">
<div class="group-hover:block absolute bottom-16 left-1/2 -translate-x-1/2 w-20 h-6 text-center bg-gray-300 border-2 border-solid border-cyan rounded-3xl hidden"> <div class="group-hover:block absolute top-1/2 left-14 -translate-y-1/2 w-20 h-6 text-center bg-gray-800 border-2 border-solid border-gray-500 rounded-3xl hidden">
<p class="absolute w-full bottom-0 m-0 text-xs leading-6">World</p> <p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">Open menu</p>
<div class="group-hover:block absolute -bottom-2.5 bg-cyan h-2 w-3.5 [clip-path:polygon(100%_0,_0_0,_50%_100%)] left-1/2 -translate-x-1/2 hidden"></div> <div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
</div> </div>
<a class="group-hover:bg-gray/70 group-hover:cursor-pointer p-1.5 bg-gray-300/70 border-2 border-solid border-cyan-200 hover:border-cyan rounded-lg block w-11 h-9"> <a class="group-hover:cursor-pointer bg-[url('/assets/ui-border-4-corners.svg')] bg-no-repeat block w-[42px] h-[42px] relative">
<img class="group-hover:drop-shadow-default w-11 h-9 object-contain" draggable="false" src="/assets/icons/world.png" /> <img class="group-hover:drop-shadow-default w-6 h-5 mx-[9px] my-[11px] object-contain" draggable="false" src="/assets/icons/menu-icon.svg" />
</a>
</li>
<li class="menu-item group relative">
<div class="group-hover:block absolute bottom-16 left-1/2 -translate-x-1/2 w-20 h-6 text-center bg-gray-300 border-2 border-solid border-cyan rounded-3xl hidden">
<p class="absolute w-full bottom-0 m-0 text-xs leading-6">Users</p>
<div class="group-hover:block absolute -bottom-2.5 bg-cyan h-2 w-3.5 [clip-path:polygon(100%_0,_0_0,_50%_100%)] left-1/2 -translate-x-1/2 hidden"></div>
</div>
<a class="group-hover:bg-gray/70 group-hover:cursor-pointer p-1.5 bg-gray-300/70 border-2 border-solid border-cyan-200 hover:border-cyan rounded-lg block w-11 h-9">
<img class="group-hover:drop-shadow-default w-11 h-9 object-contain" draggable="false" src="/assets/icons/users.png" />
</a> </a>
</li> </li>
<li class="menu-item group relative" @click="gameStore.toggleUserPanel"> <li class="menu-item group relative" @click="gameStore.toggleUserPanel">
<div class="group-hover:block absolute bottom-16 left-1/2 -translate-x-1/2 w-20 h-6 text-center bg-gray-300 border-2 border-solid border-cyan rounded-3xl hidden"> <div class="group-hover:block absolute top-1/2 left-14 -translate-y-1/2 w-20 h-6 text-center bg-gray-800 border-2 border-solid border-gray-500 rounded-3xl hidden">
<p class="absolute w-full bottom-0 m-0 text-xs leading-6">Inventory</p> <p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">User Profile</p>
<div class="group-hover:block absolute -bottom-2.5 bg-cyan h-2 w-3.5 [clip-path:polygon(100%_0,_0_0,_50%_100%)] left-1/2 -translate-x-1/2 hidden"></div> <div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
</div> </div>
<a class="group-hover:bg-gray/70 group-hover:cursor-pointer p-1.5 bg-gray-300/70 border-2 border-solid border-cyan-200 hover:border-cyan rounded-lg block w-11 h-9"> <a class="group-hover:cursor-pointer bg-[url('/assets/ui-border-4-corners.svg')] bg-no-repeat block w-[42px] h-[42px] relative">
<img class="group-hover:drop-shadow-default w-11 h-9 object-contain" draggable="false" src="/assets/icons/treasure-chest.png" /> <img class="group-hover:drop-shadow-default w-8 h-8 m-[5px] object-contain" draggable="false" src="/assets/avatar/default/head.png" />
<p class="absolute bottom-0 -right-1.5 m-0 max-w-4 font-ui z-10 text-white text-[12px] leading-[6px] drop-shadow-pixel"><span class="font-ui text-white text-[8px] ml-0.5">LVL</span> {{ characterLevel }}</p>
</a>
</li>
<li class="menu-item group relative" @click="gameStore.toggleChat">
<div class="group-hover:block absolute top-1/2 left-14 -translate-y-1/2 w-20 h-6 text-center bg-gray-800 border-2 border-solid border-gray-500 rounded-3xl hidden">
<p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">Open Chat</p>
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
</div>
<a class="group-hover:bg-gray-800 group-hover:cursor-pointer border border-b-4 border-solid rounded border-gray-500 block w-[34px] h-[31px]">
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/chat-icon.svg" />
</a>
</li>
<li class="menu-item group relative">
<div class="group-hover:block absolute top-1/2 left-14 -translate-y-1/2 w-20 h-6 text-center bg-gray-800 border-2 border-solid border-gray-500 rounded-3xl hidden">
<p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">World map</p>
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
</div>
<a class="group-hover:bg-gray-800 group-hover:cursor-pointer border border-b-4 border-solid rounded border-gray-500 block w-[34px] h-[31px]">
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/map-icon.svg" />
</a>
</li>
<li class="menu-item group relative">
<div class="group-hover:block absolute top-1/2 left-14 -translate-y-1/2 w-20 h-6 text-center bg-gray-800 border-2 border-solid border-gray-500 rounded-3xl hidden">
<p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">Users</p>
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
</div>
<a class="group-hover:bg-gray-800 group-hover:cursor-pointer border border-b-4 border-solid rounded border-gray-500 block w-[34px] h-[31px]">
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/socials-icon.svg" />
</a> </a>
</li> </li>
</ul> </ul>
@ -43,4 +53,5 @@
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
const gameStore = useGameStore() const gameStore = useGameStore()
let characterLevel = gameStore.character?.level.toString().padStart(2, '0');
</script> </script>

View File

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

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="absolute z-50 w-full h-dvh top-0 left-0 bg-black/60" v-show="gameStore.isUserPanelOpen"> <div class="absolute z-50 w-full h-dvh top-0 left-0 bg-black/60" v-show="gameStore.uiSettings.isUserPanelOpen">
<div class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 max-w-[875px] max-h-[600px] h-full w-[80%] bg-gray-300/80 border-solid border-2 border-cyan-200 rounded-lg z-50 flex flex-col backdrop-blur-sm shadow-lg"> <div class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 max-w-[875px] max-h-[600px] h-full w-[80%] bg-gray-300/80 border-solid border-2 border-cyan-200 rounded-md z-50 flex flex-col backdrop-blur-sm shadow-lg">
<div class="p-2.5 flex max-sm:flex-wrap justify-between items-center gap-5 border-solid border-0 border-b border-cyan-200"> <div class="p-2.5 flex max-sm:flex-wrap justify-between items-center gap-5 border-solid border-0 border-b border-cyan-200">
<h3 class="m-0 font-medium shrink-0">Game menu</h3> <h3 class="m-0 font-medium shrink-0">Game menu</h3>
<div class="hidden sm:flex gap-1.5 flex-wrap"> <div class="hidden sm:flex gap-1.5 flex-wrap">
@ -10,7 +10,7 @@
<button @click.stop="userPanelScreen = 'settings'" :class="{ active: userPanelScreen === 'settings' }" class="btn-cyan py-1.5 px-4 min-w-24">Settings</button> <button @click.stop="userPanelScreen = 'settings'" :class="{ active: userPanelScreen === 'settings' }" class="btn-cyan py-1.5 px-4 min-w-24">Settings</button>
</div> </div>
<div class="flex gap-2.5"> <div class="flex gap-2.5">
<button class="w-5 h-5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out" @click="gameStore.toggleUserPanel"> <button class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out" @click="gameStore.toggleUserPanel">
<img alt="close" draggable="false" src="/assets/icons/close-button-white.svg" class="w-full h-full" /> <img alt="close" draggable="false" src="/assets/icons/close-button-white.svg" class="w-full h-full" />
</button> </button>
</div> </div>

View File

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

View File

@ -1,58 +1,58 @@
<template> <template>
<Container ref="charChatContainer" :depth="999" v-if="props.character" :x="currentX" :y="currentY">> <!-- Chat bubble -->
<RoundRectangle @create="createChatBubble" :origin-x="0.5" :origin-y="7.5" :fillColor="0xffffff" :width="194" :height="21" :radius="5" /> <Container ref="charChatContainer" :depth="999" :x="currentX" :y="currentY">
<Text @create="createChatText" :text="`This is a chat message 🤯👋`" :origin-x="0.5" :origin-y="10.9" :style="{ fontSize: 13, fontFamily: 'Arial', color: '#000' }" /> <RoundRectangle @create="createChatBubble" :origin-x="0.5" :origin-y="7.5" :fillColor="0xffffff" :width="194" :height="21" :radius="20" />
<Text @create="createChatText" text="" :origin-x="0.5" :origin-y="10.9" :style="{ fontSize: 13, fontFamily: 'Arial', color: '#000' }" />
</Container> </Container>
<Container :depth="999" v-if="props.character" :x="currentX" :y="currentY"> <!-- Character name and health -->
<Text @create="createText" :text="props.character.name" :origin-x="0.5" :origin-y="9" /> <Container :depth="999" :x="currentX" :y="currentY">
<Text @create="createText" :text="character.name" :origin-x="0.5" :origin-y="9" />
<RoundRectangle :origin-x="0.5" :origin-y="18.5" :fillColor="0xffffff" :width="74" :height="6" :radius="5" /> <RoundRectangle :origin-x="0.5" :origin-y="18.5" :fillColor="0xffffff" :width="74" :height="6" :radius="5" />
<RoundRectangle :origin-x="0.5" :origin-y="36.4" :fillColor="0x00b3b3" :width="70" :height="3" :radius="5" /> <RoundRectangle :origin-x="0.5" :origin-y="36.4" :fillColor="0x00b3b3" :width="70" :height="3" :radius="5" />
</Container> </Container>
<Container :depth="isometricDepth" v-if="props.character" :x="currentX" :y="currentY" ref="charContainer"> <!-- Character sprite -->
<Image v-if="!props.character.characterType" texture="character" :origin-y="1" /> <Container ref="charContainer" :depth="isometricDepth" :x="currentX" :y="currentY">
<Sprite v-else :texture="charTexture" :play="props.character.isMoving ? charTexture : undefined" :origin-y="1" :flipX="props.character.rotation === 6 || props.character.rotation === 4" :flipY="false" /> <Sprite ref="charSprite" :origin-y="1" :flipX="isFlippedX" :flipY="false" />
</Container> </Container>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { Container, Image, refObj, RoundRectangle, Sprite, Text, useScene } from 'phavuer'
import { type ExtendedCharacter as CharacterT } from '@/types'
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
import { watch, computed, ref, onMounted, onUnmounted, type Ref } from 'vue'
import config from '@/config' import config from '@/config'
import { type ExtendedCharacter } from '@/types'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useZoneStore } from '@/stores/zoneStore' import { useZoneStore } from '@/stores/zoneStore'
import { watch, computed, ref, onMounted, onUnmounted } from 'vue'
import { Container, refObj, RoundRectangle, Sprite, Text, useGame, useScene } from 'phavuer'
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
enum Direction { enum Direction {
POSITIVE, POSITIVE,
NEGATIVE, NEGATIVE,
NOCHANGE UNCHANGED
} }
const charChatContainer = refObj() as Ref<Phaser.GameObjects.Container>; const props = defineProps<{
const charContainer = refObj() as Ref<Phaser.GameObjects.Container>;
interface Props {
layer: Phaser.Tilemaps.TilemapLayer layer: Phaser.Tilemaps.TilemapLayer
character?: CharacterT character: ExtendedCharacter
} }>()
const props = withDefaults(defineProps<Props>(), { const charChatContainer = refObj<Phaser.GameObjects.Container>()
character: undefined const charContainer = refObj<Phaser.GameObjects.Container>()
}) const charSprite = refObj<Phaser.GameObjects.Sprite>()
const game = useGame()
const gameStore = useGameStore() const gameStore = useGameStore()
const zoneStore = useZoneStore() const zoneStore = useZoneStore()
const scene = useScene() const scene = useScene()
const isometricDepth = ref(calculateIsometricDepth(props.character!.positionX, props.character!.positionY, 28, 94, true))
const currentX = ref(0) const currentX = ref(0)
const currentY = ref(0) const currentY = ref(0)
const tween = ref<Phaser.Tweens.Tween | null>(null) const isometricDepth = ref(1)
const isInitialPosition = ref(true) const isInitialPosition = ref(true)
const tween = ref<Phaser.Tweens.Tween | null>(null)
const calculateLocalDepth = (x: number, y: number, width: number, height: number, isCharacter: boolean) => { const updateIsometricDepth = (x: number, y: number) => {
isometricDepth.value = calculateIsometricDepth(x, y, width, height, isCharacter) isometricDepth.value = calculateIsometricDepth(x, y, 28, 94, true)
} }
const updatePosition = (x: number, y: number, direction: Direction) => { const updatePosition = (x: number, y: number, direction: Direction) => {
@ -66,52 +66,93 @@ const updatePosition = (x: number, y: number, direction: Direction) => {
return return
} }
if (tween.value && tween.value.isPlaying()) { if (tween.value?.isPlaying()) {
tween.value.stop() tween.value.stop()
} }
/**
* Calculate the distance between the current and target positions
*/
const distance = Math.sqrt(Math.pow(targetX - currentX.value, 2) + Math.pow(targetY - currentY.value, 2)) const distance = Math.sqrt(Math.pow(targetX - currentX.value, 2) + Math.pow(targetY - currentY.value, 2))
/** if (distance >= config.tile_size.x / 1.1) {
* Teleport: No animation, only if the distance is greater than the tile size / 1.5
*/
if (distance >= config.tile_size.x / 1.2) {
// Teleport: No animation
currentX.value = targetX currentX.value = targetX
currentY.value = targetY currentY.value = targetY
return
} }
/** const duration = distance * 6
* Normal movement: Animate
*/
if (distance <= config.tile_size.x / 1.2) {
// Normal movement: Animate
const duration = distance * 6 // Adjust this multiplier to control overall speed
tween.value = props.layer.scene.tweens.add({ tween.value = props.layer.scene.tweens.add({
targets: { x: currentX.value, y: currentY.value }, targets: { x: currentX.value, y: currentY.value },
x: targetX, x: targetX,
y: targetY, y: targetY,
duration: duration, duration,
ease: 'Linear', ease: 'Linear',
onStart: () => { onStart: () => {
if (direction === Direction.POSITIVE) { if (direction === Direction.POSITIVE) {
calculateLocalDepth(x, y, 28, 94, true) updateIsometricDepth(x, y)
}
},
onUpdate: (tween) => {
currentX.value = tween.targets[0].x ?? 0
currentY.value = tween.targets[0].y ?? 0
},
onComplete: () => {
if (direction === Direction.NEGATIVE) {
calculateLocalDepth(x, y, 28, 94, true)
}
} }
}) },
onUpdate: (tween) => {
currentX.value = tween.targets[0].x
currentY.value = tween.targets[0].y
},
onComplete: () => {
if (direction === Direction.NEGATIVE) {
updateIsometricDepth(x, y)
}
}
})
}
const calcDirection = (oldX: number, oldY: number, newX: number, newY: number): Direction => {
if (newY < oldY || newX < oldX) return Direction.NEGATIVE
if (newX > oldX || newY > oldY) return Direction.POSITIVE
return Direction.UNCHANGED
}
const isFlippedX = computed(() => [6, 4].includes(props.character.rotation ?? 0))
const charTexture = computed(() => {
const { rotation, characterType, isMoving } = props.character
const spriteId = characterType?.sprite.id ?? 'idle_right_down'
const action = isMoving ? 'walk' : 'idle'
const direction = [0, 6].includes(rotation) ? 'left_up' : 'right_down'
return `${spriteId}-${action}_${direction}`
})
const updateSprite = () => {
if (props.character.isMoving) {
charSprite.value!.anims.play(charTexture.value, true)
return
}
charSprite.value!.anims.stop()
charSprite.value!.setFrame(0)
charSprite.value!.setTexture(charTexture.value)
}
const createChatBubble = (container: Phaser.GameObjects.Container) => {
container.setName(`${props.character.name}_chatBubble`)
}
const createChatText = (text: Phaser.GameObjects.Text) => {
text.setName(`${props.character.name}_chatText`)
text.setFontSize(13)
text.setFontFamily('Arial')
// Fix text alignment on Windows and Android
if (game.device.os.windows || game.device.os.android) {
text.setOrigin(0.5, 9.75)
}
}
const createText = (text: Phaser.GameObjects.Text) => {
text.setFontSize(13)
text.setFontFamily('Arial')
// Fix text alignment on Windows and Android
if (game.device.os.windows || game.device.os.android) {
text.setOrigin(0.5, 8)
} }
} }
@ -119,80 +160,38 @@ watch(
() => props.character, () => props.character,
(newChar, oldChar) => { (newChar, oldChar) => {
if (!newChar) return if (!newChar) return
if (!oldChar || newChar.positionX !== oldChar.positionX || newChar.positionY !== oldChar.positionY) { if (!oldChar || newChar.positionX !== oldChar.positionX || newChar.positionY !== oldChar.positionY) {
if (!oldChar) { const direction = !oldChar ? Direction.POSITIVE : calcDirection(oldChar.positionX, oldChar.positionY, newChar.positionX, newChar.positionY)
updatePosition(newChar.positionX, newChar.positionY, Direction.POSITIVE) updatePosition(newChar.positionX, newChar.positionY, direction)
} else {
const direction = calcDirection(oldChar.positionX, oldChar.positionY, newChar.positionX, newChar.positionY)
updatePosition(newChar.positionX, newChar.positionY, direction)
}
} }
}, }
{ immediate: true, deep: true }
) )
const calcDirection = (oldX: number, oldY: number, newX: number, newY: number) => { watch(() => props.character.isMoving, updateSprite)
if (newY < oldY || newX < oldX) { watch(() => props.character.rotation, updateSprite)
return Direction.NEGATIVE
}
if (newX > oldX || newY > oldY) {
return Direction.POSITIVE
}
return Direction.NOCHANGE
}
const charTexture = computed(() => {
if (!props.character?.characterType?.sprite) {
return 'idle_right_down'
}
const rotation = props.character.rotation
const spriteId = props.character.characterType.sprite.id
const action = props.character.isMoving ? 'walk' : 'idle'
if (rotation === 0 || rotation === 6) {
return `${spriteId}-${action}_left_up`
} else if (rotation === 2 || rotation === 4) {
return `${spriteId}-${action}_right_down`
}
return `${spriteId}-${action}_left_up`
})
const createChatBubble = (container: Phaser.GameObjects.Container) => {
container.setName(props.character?.name + '_chatBubble')
}
const createChatText = (text: Phaser.GameObjects.Text) => {
text.setName(props.character?.name + '_chatText')
text.setFontSize(13)
text.setFontFamily('Arial')
}
const createText = (text: Phaser.GameObjects.Text) => {
text.setFontSize(13)
text.setFontFamily('Arial')
}
onMounted(() => { onMounted(() => {
// Check if player is this character, then lock with camera charChatContainer.value!.setName(`${props.character!.name}_chatContainer`)
if (props.character && props.character.id === gameStore.character?.id) { charChatContainer.value!.setVisible(false)
charChatContainer.value?.setName(props.character.name + '_chatContainer') charContainer.value!.setName(props.character!.name)
charChatContainer.value?.setVisible(false)
charContainer.value?.setName(props.character.name) if (props.character.id === gameStore.character!.id) {
zoneStore.setCharacterLoaded(true)
zoneStore.characterLoaded = true; // #146 : Set camera position to character, need to be improved still
scene.cameras.main.startFollow(charContainer.value as Phaser.GameObjects.Container)
scene.cameras.main.stopFollow()
} }
if (props.character) { // Set sprite
updatePosition(props.character.positionX, props.character.positionY, Direction.POSITIVE) charSprite.value!.setTexture(charTexture.value)
} charSprite.value!.setFlipX(isFlippedX.value)
updatePosition(props.character.positionX, props.character.positionY, props.character.rotation)
}) })
onUnmounted(() => { onUnmounted(() => {
if (tween.value) { tween.value?.stop()
tween.value.stop()
}
}) })
</script> </script>

View File

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

View File

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

View File

@ -1,13 +1,13 @@
<template> <template>
<Teleport to="body"> <Teleport to="body">
<div v-if="isModalOpenRef" class="fixed bg-gray-300/80 border-solid border-2 border-cyan-200 z-50 flex flex-col backdrop-blur-sm shadow-lg" :style="modalStyle"> <div v-if="isModalOpenRef" class="fixed bg-gray border-solid border-2 border-gray-500 z-50 flex flex-col backdrop-blur-sm shadow-lg" :style="modalStyle">
<div @mousedown="startDrag" class="cursor-move p-2.5 flex justify-between items-center border-solid border-0 border-b border-cyan-200"> <div @mousedown="startDrag" class="cursor-move p-2.5 flex justify-between items-center border-solid border-0 border-b border-gray-500">
<slot name="modalHeader" /> <slot name="modalHeader" />
<div class="flex gap-2.5"> <div class="flex gap-2.5">
<button @click="toggleFullScreen" class="w-5 h-5 m-0 p-0 relative hover:scale-110 transition-transform duration-300 ease-in-out" v-if="canFullScreen"> <button @click="toggleFullScreen" class="w-5 h-5 m-0 p-0 relative hover:scale-110 transition-transform duration-300 ease-in-out" v-if="canFullScreen">
<img :alt="isFullScreen ? 'exit full-screen' : 'full-screen'" draggable="false" :src="isFullScreen ? '/assets/icons/minimize.svg' : '/assets/icons/full-screen.svg'" class="w-full h-full invert" /> <img :alt="isFullScreen ? 'exit full-screen' : 'full-screen'" draggable="false" :src="isFullScreen ? '/assets/icons/minimize.svg' : '/assets/icons/full-screen.svg'" class="w-full h-full invert" />
</button> </button>
<button @click="close" v-if="closable" class="w-5 h-5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out"> <button @click="close" v-if="closable" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
<img alt="close" draggable="false" src="/assets/icons/close-button-white.svg" class="w-full h-full" /> <img alt="close" draggable="false" src="/assets/icons/close-button-white.svg" class="w-full h-full" />
</button> </button>
</div> </div>
@ -16,7 +16,7 @@
<slot name="modalBody" /> <slot name="modalBody" />
<img v-if="isResizable && !isFullScreen" src="/assets/icons/resize-icon.svg" alt="resize" class="absolute bottom-0 right-0 w-5 h-5 cursor-nwse-resize invert-[60%]" @mousedown="startResize" /> <img v-if="isResizable && !isFullScreen" src="/assets/icons/resize-icon.svg" alt="resize" class="absolute bottom-0 right-0 w-5 h-5 cursor-nwse-resize invert-[60%]" @mousedown="startResize" />
</div> </div>
<div v-if="$slots.modalFooter" class="px-5 min-h-12 flex justify-end gap-7.5 items-center border-solid border-t border-cyan-200"> <div v-if="$slots.modalFooter" class="px-5 min-h-12 flex justify-end gap-7.5 items-center border-solid border-t border-gray-500">
<slot name="modalFooter" /> <slot name="modalFooter" />
</div> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@
import { useScene } from 'phavuer' import { useScene } from 'phavuer'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useZoneStore } from '@/stores/zoneStore' import { useZoneStore } from '@/stores/zoneStore'
import { onBeforeUnmount, ref } from 'vue' import { onBeforeMount, onBeforeUnmount, ref } from 'vue'
import type { Character as CharacterT, Zone as ZoneT, ExtendedCharacter as ExtendedCharacterT } from '@/types' import type { Character as CharacterT, Zone as ZoneT, ExtendedCharacter as ExtendedCharacterT } from '@/types'
import Tiles from '@/components/zone/Tiles.vue' import Tiles from '@/components/zone/Tiles.vue'
import Objects from '@/components/zone/Objects.vue' import Objects from '@/components/zone/Objects.vue'
@ -26,16 +26,6 @@ type zoneLoadData = {
characters: CharacterT[] characters: CharacterT[]
} }
gameStore.connection!.emit('zone:character:join', async (response: zoneLoadData) => {
// Fetch assets for new zone
await gameStore.fetchZoneAssets(response.zone.id)
await loadAssets(scene)
// Set zone and characters
zoneStore.setZone(response.zone)
zoneStore.setCharacters(response.characters)
})
// Event listeners // Event listeners
gameStore.connection!.on('zone:character:teleport', async (data: zoneLoadData) => { gameStore.connection!.on('zone:character:teleport', async (data: zoneLoadData) => {
/** /**
@ -58,6 +48,8 @@ gameStore.connection!.on('zone:character:teleport', async (data: zoneLoadData) =
}) })
gameStore.connection!.on('zone:character:join', async (data: ExtendedCharacterT) => { gameStore.connection!.on('zone:character:join', async (data: ExtendedCharacterT) => {
// If data is from the current user, don't add it to the store
if (data.id === gameStore.character?.id) return
zoneStore.addCharacter(data) zoneStore.addCharacter(data)
}) })
@ -69,6 +61,18 @@ gameStore.connection!.on('character:move', (data: ExtendedCharacterT) => {
zoneStore.updateCharacter(data) zoneStore.updateCharacter(data)
}) })
onBeforeMount(() => {
gameStore.connection!.emit('zone:character:join', async (response: zoneLoadData) => {
// Fetch assets for new zone
await gameStore.fetchZoneAssets(response.zone.id)
await loadAssets(scene)
// Set zone and characters
zoneStore.setZone(response.zone)
zoneStore.setCharacters(response.characters)
})
})
onBeforeUnmount(() => { onBeforeUnmount(() => {
zoneStore.reset() zoneStore.reset()
gameStore.connection!.off('zone:character:teleport') gameStore.connection!.off('zone:character:teleport')

View File

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

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

View File

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

View File

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

View File

@ -1,12 +1,9 @@
const dev: boolean = true
export default { export default {
name: 'New Quest', name: import.meta.env.VITE_NAME,
development: dev, development: import.meta.env.VITE_DEVELOPMENT === 'true',
server_endpoint: dev ? 'http://localhost:4000' : 'https://nq-server.cr-a.directonline.io', server_endpoint: import.meta.env.VITE_SERVER_ENDPOINT,
tile_size: { x: 64, y: 32, z: 1 } tile_size: {
x: Number(import.meta.env.VITE_TILE_SIZE_X),
y: Number(import.meta.env.VITE_TILE_SIZE_Y)
}
} }
/**
* @TODO: Implement .env like server has
*/

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="bg-gray-300 relative"> <div class="bg-gray-900 relative">
<div class="absolute bg-[url('/assets/shapes/select-screen-bg-shape.svg')] bg-no-repeat bg-center w-full h-full"></div> <div class="absolute bg-[url('/assets/shapes/select-screen-bg-shape.svg')] bg-no-repeat bg-center w-full h-full"></div>
<div class="ui-wrapper h-dvh flex flex-col justify-center items-center gap-20 px-10 sm:px-20"> <div class="ui-wrapper h-dvh flex flex-col justify-center items-center gap-20 px-10 sm:px-20">
<div class="filler"></div> <div class="filler"></div>
@ -8,14 +8,16 @@
<div <div
v-for="character in characters" v-for="character in characters"
:key="character.id" :key="character.id"
class="group first:ml-auto last:mr-auto m-4 w-[170px] h-[275px] flex flex-col shrink-0 rounded-2xl relative bg-[url('/assets/shapes/character-select-shape.svg')] bg-no-repeat shadow-character" class="group first:ml-auto last:mr-auto m-4 w-[170px] h-[275px] flex flex-col shrink-0 relative shadow-character"
:class="{ active: selected_character == character.id }" :class="{ active: selected_character == character.id }"
> >
<img src="/assets/ui-box-outer.svg" class="absolute w-full h-full max-lg:hidden" />
<img src="/assets/ui-box-inner.svg" class="absolute left-2 bottom-2 w-[calc(100%_-_16px)] h-[calc(100%_-_40px)] max-lg:hidden" />
<input class="opacity-0 h-full w-full absolute m-0 z-10" type="radio" :id="character.id" name="character" :value="character.id" v-model="selected_character" /> <input class="opacity-0 h-full w-full absolute m-0 z-10" type="radio" :id="character.id" name="character" :value="character.id" v-model="selected_character" />
<label class="font-bold absolute left-1/2 top-5 max-w-32 -translate-x-1/2 -translate-y-1/2 text-center text-ellipsis overflow-hidden whitespace-nowrap drop-shadow-text" :for="character.id">{{ character.name }}</label> <label class="font-bold absolute left-1/2 top-4 max-w-32 -translate-x-1/2 -translate-y-1/2 text-center text-ellipsis overflow-hidden whitespace-nowrap drop-shadow-text" :for="character.id">{{ character.name }}</label>
<button <button
class="delete bg-red w-8 h-8 p-[3px] rounded-full absolute -right-4 top-0 -translate-y-1/2 z-10 border-2 border-solid border-white hover:bg-red-100" class="delete bg-red w-8 h-8 p-[3px] rounded-full absolute -right-4 top-0 -translate-y-1/2 z-10 border-2 border-solid border-white hover:bg-red-300"
@click=" @click="
() => { () => {
deletingCharacter = character deletingCharacter = character
@ -28,15 +30,15 @@
<div class="sprite-container flex flex-col items-center m-auto"> <div class="sprite-container flex flex-col items-center m-auto">
<img class="drop-shadow-20" draggable="false" src="/assets/avatar/default/0.png" /> <img class="drop-shadow-20" draggable="false" src="/assets/avatar/default/0.png" />
</div> </div>
<span class="absolute bottom-5 w-full text-center translate-y-1/2 z-10 drop-shadow-text">Lvl. {{ character.level }}</span> <span class="absolute bottom-6 w-full text-center translate-y-1/2 z-10">Lvl. {{ character.level }}</span>
<div class="selected-character group-[.active]:max-w-[170px] absolute max-w-0 w-4/6 h-[3px] bg-white rounded-[3px] left-1/2 -bottom-4 -translate-x-1/2 transition-all ease-in-out duration-300"></div> <div class="selected-character group-[.active]:max-w-[170px] absolute max-w-0 w-4/6 h-[3px] bg-gray-500 rounded-[3px] left-1/2 -bottom-4 -translate-x-1/2 transition-all ease-in-out duration-300"></div>
</div> </div>
<div class="character new-character first:ml-auto mr-auto m-4 w-[170px] h-[275px] flex flex-col shrink-0 rounded-2xl relative bg-gray-50/50 bg-no-repeat shadow-character" v-if="characters.length < 4"> <div class="character new-character first:ml-auto mr-auto m-4 w-[170px] h-[275px] flex flex-col shrink-0 rounded-2xl relative bg-gray-500/50 bg-no-repeat shadow-character" v-if="characters.length < 4">
<button class="h-full w-full py-10 flex flex-col justify-between" @click="isModalOpen = true"> <button class="h-full w-full py-10 flex flex-col justify-between" @click="isModalOpen = true">
<div class="filler"></div> <div class="filler"></div>
<img class="w-24 h-24 m-auto" draggable="false" src="/assets/icons/plus-icon.svg" /> <img class="w-24 h-24 m-auto" draggable="false" src="/assets/icons/plus-icon.svg" />
<span class="self-center text-base absolute bottom-5 w-full text-center translate-y-1/2 z-10 drop-shadow-text">Create new</span> <span class="self-center text-base absolute bottom-5 w-full text-center translate-y-1/2 z-10">Create new</span>
</button> </button>
</div> </div>
</div> </div>
@ -46,13 +48,13 @@
<div class="button-wrapper flex gap-8" v-if="!isLoading"> <div class="button-wrapper flex gap-8" v-if="!isLoading">
<button <button
class="btn-bordeaux py-2 pr-2.5 pl-8 min-w-24 relative rounded text-xl flex gap-4 items-center transition-all ease-in-out duration-200 hover:gap-5 disabled:bg-cyan/50 disabled:hover:bg-opacity-50 disabled:cursor-not-allowed disabled:hover:gap-[15px]" class="btn-red py-2 pr-2.5 pl-8 min-w-24 relative rounded text-xl flex gap-4 items-center transition-all ease-in-out duration-200 hover:gap-5 disabled:bg-red/50 disabled:hover:bg-opacity-50 disabled:cursor-not-allowed disabled:hover:gap-[15px]"
@click.stop="gameStore.disconnectSocket()" @click.stop="gameStore.disconnectSocket()"
> >
<img class="h-8 drop-shadow-20 rotate-180" draggable="false" src="/assets/icons/arrow.svg" alt="Logout icon" /> <img class="h-8 drop-shadow-20 rotate-180" draggable="false" src="/assets/icons/arrow.svg" alt="Logout icon" />
</button> </button>
<button <button
class="btn-cyan py-2 pr-2.5 pl-8 min-w-24 relative rounded text-xl flex gap-4 items-center transition-all ease-in-out duration-200 hover:gap-5 disabled:bg-cyan/50 disabled:hover:bg-opacity-50 disabled:cursor-not-allowed disabled:hover:gap-[15px]" class="btn-cyan py-2 px-2.5 pl-8 min-w-24 relative rounded text-xl flex gap-4 items-center transition-all ease-in-out duration-200 hover:gap-5 disabled:bg-cyan-800 disabled:hover:bg-opacity-50 disabled:cursor-not-allowed disabled:hover:gap-[15px]"
:disabled="!selected_character" :disabled="!selected_character"
@click="select_character()" @click="select_character()"
> >
@ -66,7 +68,7 @@
<!-- CREATE CHARACTER MODAL --> <!-- CREATE CHARACTER MODAL -->
<Modal :isModalOpen="isModalOpen" @modal:close="isModalOpen = false"> <Modal :isModalOpen="isModalOpen" @modal:close="isModalOpen = false">
<template #modalHeader> <template #modalHeader>
<h3 class="m-0 font-medium">Create your character</h3> <h3 class="m-0 font-medium text-gray-300">Create your character</h3>
</template> </template>
<template #modalBody> <template #modalBody>
@ -74,7 +76,7 @@
<form method="post" @submit.prevent="create" class="inline"> <form method="post" @submit.prevent="create" class="inline">
<div class="form-field-full"> <div class="form-field-full">
<label for="name">Name</label> <label for="name">Name</label>
<input class="input-cyan max-w-64" v-model="name" name="name" id="name" /> <input class="input-field max-w-64" v-model="name" name="name" id="name" />
</div> </div>
<button class="btn-cyan py-1.5 px-4 mr-5 min-w-24 inline-block" type="submit">CREATE</button> <button class="btn-cyan py-1.5 px-4 mr-5 min-w-24 inline-block" type="submit">CREATE</button>
</form> </form>
@ -86,7 +88,7 @@
<!-- DELETE CHARACTER MODAL --> <!-- DELETE CHARACTER MODAL -->
<ConfirmationModal v-if="deletingCharacter != null" :confirm-function="delete_character.bind(this, deletingCharacter.id)" :cancel-function="(() => (deletingCharacter = null)).bind(this)" confirm-button-text="Delete"> <ConfirmationModal v-if="deletingCharacter != null" :confirm-function="delete_character.bind(this, deletingCharacter.id)" :cancel-function="(() => (deletingCharacter = null)).bind(this)" confirm-button-text="Delete">
<template #modalHeader> <template #modalHeader>
<h3 class="m-0 font-medium">Deleting character</h3> <h3 class="m-0 font-medium text-gray-300">Deleting character</h3>
</template> </template>
<template #modalBody> <template #modalBody>
You are about to delete <span class="font-extrabold">{{ deletingCharacter.name }}</span You are about to delete <span class="font-extrabold">{{ deletingCharacter.name }}</span

View File

@ -1,25 +1,22 @@
<template> <template>
<div class="flex justify-center items-center h-dvh relative"> <div class="flex justify-center items-center h-dvh relative">
<GmTools v-if="isLoaded && gameStore.character?.role === 'gm'" /> <GmTools v-if="gameStore.character?.role === 'gm'" />
<GmPanel v-if="isLoaded && gameStore.character?.role === 'gm'" /> <GmPanel v-if="gameStore.character?.role === 'gm'" />
<Inventory />
<div v-if="!zoneEditorStore.active"> <div v-if="!zoneEditorStore.active">
<Game :config="gameConfig" @create="createGame" class="111mt-[-60px]"> <Game :config="gameConfig" @create="createGame">
<Scene name="main" @preload="preloadScene" @create="createScene"> <Scene name="main" @preload="preloadScene" @create="createScene">
<div class="fixed inset-x-0 top-0 flex justify-start items-end p-10 pointer-events-none"> <div v-if="isLoaded">
<div class="pointer-events-auto"> <Menu />
<Hud /> <Hud />
</div> <Keybindings />
</div> <Minimap />
<Zone v-if="isLoaded" /> <Zone />
<div class="fixed inset-x-0 bottom-0 flex justify-between gap-5 items-end py-10 px-5 xxs:p-10 pointer-events-none max-md:flex-wrap max-md:flex-col-reverse"> <Chat />
<div class="pointer-events-auto w-full"> <ExpBar />
<Chat />
</div> <Inventory />
<div class="pointer-events-auto max-xs:m-auto mr-auto"> <Effects />
<Menubar />
</div>
</div> </div>
</Scene> </Scene>
</Game> </Game>
@ -27,7 +24,7 @@
<div v-if="zoneEditorStore.active"> <div v-if="zoneEditorStore.active">
<Game :config="gameConfig" @create="createGame"> <Game :config="gameConfig" @create="createGame">
<Scene name="main" @preload="preloadScene" @create="createScene"> <Scene name="main" @preload="preloadScene" @create="createScene">
<ZoneEditor v-if="isLoaded" :key="JSON.stringify(zoneEditorStore.zone)" /> <ZoneEditor v-if="isLoaded" :key="JSON.stringify(`${zoneEditorStore.zone?.id}_${zoneEditorStore.zone?.createdAt}_${zoneEditorStore.zone?.updatedAt}`)" />
</Scene> </Scene>
</Game> </Game>
</div> </div>
@ -40,15 +37,20 @@ import { ref, onBeforeUnmount } from 'vue'
import { Game, Scene } from 'phavuer' import { Game, Scene } from 'phavuer'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore' import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import Menu from '@/components/gui/Menu.vue'
import ExpBar from '@/components/gui/ExpBar.vue'
import Hud from '@/components/gui/Hud.vue' import Hud from '@/components/gui/Hud.vue'
import Zone from '@/components/zone/Zone.vue' import Zone from '@/components/zone/Zone.vue'
import Keybindings from '@/components/gui/Keybindings.vue'
import Chat from '@/components/gui/Chat.vue' import Chat from '@/components/gui/Chat.vue'
import Menubar from '@/components/gui/Menu.vue'
import GmTools from '@/components/gameMaster/GmTools.vue' import GmTools from '@/components/gameMaster/GmTools.vue'
import ZoneEditor from '@/components/gameMaster/zoneEditor/ZoneEditor.vue' import ZoneEditor from '@/components/gameMaster/zoneEditor/ZoneEditor.vue'
import GmPanel from '@/components/gameMaster/GmPanel.vue' import GmPanel from '@/components/gameMaster/GmPanel.vue'
import Inventory from '@/components/gui/UserPanel.vue' import Inventory from '@/components/gui/UserPanel.vue'
import Effects from '@/components/Effects.vue'
import { loadAssets } from '@/composables/zoneComposable' import { loadAssets } from '@/composables/zoneComposable'
import Minimap from '@/components/gui/Minimap.vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore() const zoneEditorStore = useZoneEditorStore()
@ -59,14 +61,7 @@ const gameConfig = {
width: window.innerWidth, width: window.innerWidth,
height: window.innerHeight, height: window.innerHeight,
type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS
scale: { resolution: 5
mode: Phaser.Scale.RESIZE,
autoCenter: Phaser.Scale.CENTER_BOTH,
width: window.innerWidth,
height: window.innerHeight
},
resolution: 2,
pixelArt: true
} }
const createGame = (game: Phaser.Game) => { const createGame = (game: Phaser.Game) => {
@ -76,9 +71,20 @@ const createGame = (game: Phaser.Game) => {
addEventListener('resize', () => { addEventListener('resize', () => {
game.scale.resize(window.innerWidth, window.innerHeight) game.scale.resize(window.innerWidth, window.innerHeight)
}) })
// We don't support canvas mode, only WebGL
if (game.renderer.type === Phaser.CANVAS) {
gameStore.addNotification({
title: 'Warning',
message: 'Your browser does not support WebGL. Please use a modern browser like Chrome, Firefox, or Edge.'
})
gameStore.disconnectSocket()
}
} }
const preloadScene = async (scene: Phaser.Scene) => { const preloadScene = async (scene: Phaser.Scene) => {
isLoaded.value = false
/** /**
* Create loading bar * Create loading bar
*/ */
@ -122,10 +128,6 @@ const preloadScene = async (scene: Phaser.Scene) => {
scene.load.image('blank_tile', '/assets/zone/blank_tile.png') scene.load.image('blank_tile', '/assets/zone/blank_tile.png')
scene.load.image('blank_object', '/assets/zone/blank_tile.png') scene.load.image('blank_object', '/assets/zone/blank_tile.png')
scene.load.image('waypoint', '/assets/waypoint.png') scene.load.image('waypoint', '/assets/waypoint.png')
scene.textures.addBase64(
'character',
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAABeCAYAAAAwnXTzAAAHWUlEQVR4nLVaQUhcRxj+DCuILruHXdbnYkBimkdFdouwFBdWKCWHHkoChragmAo9pKG3ltQGWrGHkEpzS0t7Mgn2EiIYcvQiLurBIF0RYYtbFiK6Lhqyiy4BBXt4+8/OzHvz3ryN/eCx782bN9/8//z/P//MbMvHP/wJXXTMTJ45lR+PT7XothHwQzQ8OIBUpt/2fmLaeq9D7EnYMTN5Njw4ALM3iFC8B0Y6ifZYWKjzKJ1EaSWHienJMy/SCzpkqUy/QNZqJISrPRaGkU7i/p0xpdq1CAHA7A0CAIx0UlnHD6mSkKQLxXu8+iTArWOuhCrUyhWclDZc67hJqWWlTqTt2LCV6aApQj8EMjxVWt0tAgBOD8tNEcjwlDBfOAIKm/jE7NJq8Mvr374b4affjwAAXvzyF7uXG300/4Ddz++8wkj3RRwr2vNUaWklBwBIZfqZWud3XmF+5xU66vd+oCQ8Hp9qmVtdx1p2k5Ee5PdweljGm5dPWb2R7ovCd29ePsWPd8aUhFpWupbdZLH0IL+HqGk1/BunSjm+quCqUpISqBtPHQf5PUZCF8HLXTzHkCddy26y8tPDMmrliu06PSxjYvqJcqrSDm0yKY2nfJH0KmhHmuHBAUZKSDnUc5POFyGRAg1p5Q4A3rO+L0JKL1KZflR3i8gXjpj1eklGaHFLovhchhpWobpbxL3ZJfasIneUkCciEBmlGbVyhQUEI52EgSTux3uY5HMzzvmNjZBmegCOUhEJNQwA5m6R1QvFe5CKW3WdSAVCPmkCgEvXh5gkcupgIInQSg5VjozqlFZyVi60Kovj4IeUNF26PsSSI0I4kWEXJU2yqsOJDMxb3yAU78Hw4IAt1WCEfNJkpJNoNRIArIwsWp8L+VyG3vOgMqpHnedhG8Oo2SVIxZM45TI8qC7FU0v6JaGOoFKzN4hAJCb0tFausDBGjfFxk1BaybFyoJGayGoVCGWLlBtVxU1qvLSSw+lhGaWVHPKFI8cc1aZSUiep5yC/h7XsJlIAG0sqB7gZpFD/zW5ibnUd9++MoT0WtlmrMrSRdGv1BlKZfmEmICLZMPh508mwAoDo7ABQ2cgyKeZW1zE8OCDMhYAVT6NSJneQ32tIqoBSQpKOwEedqNmFQCQGACzMWeUApI7JlsoIzd4gomYXUyUvXePDBhkt2wAgbNQtmiMiQ5LBrFS2UKcPeMnk8XEaL6dFj+AW1NhBfk8YfBmqDI0vzxeOUCtXYKSTgi8yQjnC6OKktGGLMG4QJHRSixcZRReybN5NaCrjy3wvSAF/SzVZa4yQxo8wt7ou5KR+iem7ViMhGKRNwtPDstKk6T0gWiCpToZTO4ywGYNxa5hwUtoQgrgt0ni5BFBP87nnKzeusXIvMEK/FqrTeL5whCvliqA9bSut7hY91w0q1MoV5vwX5JnCrbdEqiLmh0O2cPJF2xhWd4vMJQDgqK8PjytvrZfrb4H1A9wMtwHcIpW+yxeOrLp9faBk9J9nz3HlxjV8cHsMmF2yCPlI4GQwV69eFZ4fLywIHZDrLtB7au/Zc6R+/rUhoc5+WmdnJ7sfHR1V1tvf3xeeP39wG68Xl7H203cNQn7mdsqYebLLly8DALq7u21kOzs7trLXi8vo/OJrtG1kgdklu5W6SetGRuVUh7DwIoeT0gbCiQwAzi0oVMnz17uALPXv35+w9m0ShhMZtuOkgqy6xcVFdr+8vGwjvTe7hM/SX+F4fKolAFj+E4jEEDaA2ZFbglsAliF0dnZie3ubqYxIt7e3Gen+/j4WFhYQ3NrC8YciKd0HAMuHDFgBViYLbm2BjFx2D75D5Ao3w22Yc9FOALB85dJhWbnTezPcBoDzP0UdszfoGfgDx+NTLXMzk2epTD9C77+nrGj2BvEw3jizoJyVUslGYrzquBAlMKORM2uZLBTvQdTsYhd/YEI5rQ4uAA3zdUsZqMFAJIZAJCYQ+Nn5F9xClSoQKOMOJzIIRGKOK1xtwuPxqRbV5g4vQauRUBqXnIi5EhKpfl/9k9kIdeB1SHLuhEDzZxZNEXqReTm+L0J5g6EZ+D4KciNqj4VtsViGloRyiuhkkfyK2A2+JSTnr0Ecr3/nl2CkvY3JFyG/tpe3wKq7RVSfFT3b8GU0tL1FC1GnTQfPTvshBKzEFnA2f6fNPBm+/VCWxG8A9yWhMOdJO06+5kMv0OTLuwMvmfzODdoS8u4ANLdsA5oM3u+CcyM8l/PD/wPnRqi7R+CL0K1R3Uzg3CTUzQLOhZCOGXR2ObT+wENwU5vuloq2hHTI7JVa3B0dQsfM5JlqQav9jyEi9UIo3oO7o0PKVbRWaNMJzHydqNmFUHwPZm8Q96QzRF+zhXz0I4PPCAKRrLXInRXnx6anJ1re5QtHMFGsvwdqANph7VrQ3oxvQuv/F43zishHwzBvAak/HmJi+kl9Aeo807seyTqBl8BIW3PeSWlDiDrn9ldBWo4PYwAmrKyskWIsue4Eq+B6jk9w2yTyu8T7D0vv92u9uVoPAAAAAElFTkSuQmCC'
)
/** /**
* Load the assets into the Phaser scene * Load the assets into the Phaser scene
@ -144,14 +146,22 @@ const createScene = async (scene: Phaser.Scene) => {
scene.anims.create({ scene.anims.create({
key: asset.key, key: asset.key,
frameRate: 7, frameRate: 7,
/** @TODO: Fix end, which is total amount of frames */ frames: scene.anims.generateFrameNumbers(asset.key, { start: 0, end: asset.frameCount! - 1 }),
frames: scene.anims.generateFrameNumbers(asset.key, { start: 0, end: 4 }),
repeat: -1 repeat: -1
}) })
}) })
} }
onBeforeUnmount(() => { onBeforeUnmount(() => {
isLoaded.value = false
gameStore.disconnectSocket() gameStore.disconnectSocket()
}) })
</script> </script>
<style lang="scss">
canvas {
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-crisp-edges;
image-rendering: pixelated;
}
</style>

View File

@ -1,28 +1,66 @@
<template> <template>
<div class="bg-gray-300"> <div class="relative max-lg:h-dvh">
<div class="absolute bg-[url('/assets/shapes/select-screen-bg-shape.svg')] bg-no-repeat bg-center w-full h-full z-10 pointer-events-none"></div> <div class="lg:bg-gradient-to-r bg-gradient-to-b from-gray-900 to-transparent w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute right-0 max-lg:bottom-0 lg:top-0 z-10"></div>
<div class="z-20 w-full h-dvh flex items-center justify-between flex-col relative"> <div class="bg-[url('/assets/login/login-bg.png')] w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute right-0 max-lg:bottom-0 lg:top-0 bg-no-repeat bg-cover bg-center"></div>
<div class="filler"></div> <div class="bg-gray-900 z-20 w-full lg:w-1/2 h-[65dvh] lg:h-dvh relative">
<h1 class="mt-28 text-center text-6xl">NEW QUEST</h1> <div class="h-dvh flex items-center lg:justify-center flex-col px-8 max-lg:pt-20">
<form @submit.prevent="loginFunc"> <img src="/assets/login/nq-logo-v1.png" class="mb-10" />
<div class="my-20 mx-0 w-full flex flex-col gap-6"> <div class="relative">
<div class="w-full grid gap-4"> <img src="/assets/ui-box-outer.svg" class="absolute w-full h-full" />
<div class="flex flex-col bg-white/50 rounded-[3px] border border-solid border-gray-50 sm:min-w-[500px] sm:w-unset w-full my-0 mx-auto"> <img src="/assets/ui-box-inner.svg" class="absolute left-2 top-2 w-[calc(100%_-_16px)] h-[calc(100%_-_16px)] max-lg:hidden" />
<label class="text-black bg-white/50 p-1 text-sm rounded-t-[3px]" for="username">Username</label>
<input class="p-1 text-sm focus-visible:outline-none" id="username" v-model="username" type="text" name="username" required autofocus /> <!-- Login Form -->
<form v-show="switchForm === 'login'" @submit.prevent="loginFunc" class="relative px-6 py-11">
<div class="flex flex-col gap-5 p-2 mb-8 relative">
<div class="w-full grid gap-3 relative">
<input class="input-field xs:min-w-[350px] min-w-64" id="username-login" 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-login" v-model="password" type="password" name="password" placeholder="Password" required />
<button class="p-0 absolute right-3 w-4 h-3 top-1/2 -translate-y-1/2 bg-[url('/assets/icons/eye.svg')] bg-no-repeat"></button>
</div>
<span v-if="loginError" class="text-red-200 text-xs absolute top-full mt-1">{{ loginError }}</span>
</div>
<button class="text-right text-cyan-300 text-base">Forgot password?</button>
<button class="btn-cyan px-0 xs:w-full" type="submit">Play now</button>
<!-- Divider shape -->
<div class="absolute w-40 h-0.5 -bottom-8 left-1/2 -translate-x-1/2 flex justify-between">
<div class="w-0.5 h-full bg-gray-300"></div>
<div class="w-36 h-full bg-gray-300"></div>
<div class="w-0.5 h-full bg-gray-300"></div>
</div>
</div> </div>
<div class="flex flex-col bg-white/50 rounded-[3px] border border-solid border-gray-50 sm:min-w-[500px] sm:w-unset w-full my-0 mx-auto"> <div class="pt-8">
<label class="text-black bg-white/50 p-1 text-sm rounded-t-[3px]" for="password">Password</label> <p class="m-0 text-center">Don't have an account? <button class="text-cyan-300 text-base p-0" @click.prevent="switchForm = 'register'">Sign up</button></p>
<input class="p-1 text-sm focus-visible:outline-none" id="password" v-model="password" type="password" name="password" required />
</div> </div>
</div> </form>
<div class="flex justify-center sm:gap-4 gap-2">
<button class="btn-cyan py-2 px-0 min-w-24" type="submit"><span class="m-auto">PLAY</span></button> <!-- Register Form -->
<button class="btn-cyan py-2 px-0 min-w-24" type="button" @click.prevent="registerFunc"><span class="m-auto">REGISTER</span></button> <form v-show="switchForm === 'register'" @submit.prevent="registerFunc" class="relative px-6 py-11">
<button class="btn-cyan py-2 px-0 min-w-24"><span class="m-auto">CREDITS</span></button> <div class="flex flex-col gap-5 p-2 mb-8 relative">
</div> <div class="w-full grid gap-3 relative">
<input class="input-field xs:min-w-[350px] min-w-64" id="username-register" 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-register" v-model="password" type="password" name="password" placeholder="Password" required />
<button class="p-0 absolute right-3 w-4 h-3 top-1/2 -translate-y-1/2 bg-[url('/assets/icons/eye.svg')] bg-no-repeat"></button>
</div>
<span v-if="loginError" class="text-red-200 text-xs absolute top-full mt-1">{{ loginError }}</span>
</div>
<button class="btn-cyan xs:w-full" type="submit">Register now</button>
<!-- Divider shape -->
<div class="absolute w-40 h-0.5 -bottom-8 left-1/2 -translate-x-1/2 flex justify-between">
<div class="w-0.5 h-full bg-gray-300"></div>
<div class="w-36 h-full bg-gray-300"></div>
<div class="w-0.5 h-full bg-gray-300"></div>
</div>
</div>
<div class="pt-8">
<p class="m-0 text-center">Already have an account? <button class="text-cyan-300 text-base p-0" @click.prevent="switchForm = 'login'">Log in</button></p>
</div>
</form>
</div> </div>
</form> </div>
</div> </div>
</div> </div>
</template> </template>
@ -36,7 +74,8 @@ import { useCookies } from '@vueuse/integrations/useCookies'
const gameStore = useGameStore() const gameStore = useGameStore()
const username = ref('') const username = ref('')
const password = ref('') const password = ref('')
const switchForm = ref('login')
const loginError = ref('')
// automatic login because of development // automatic login because of development
onMounted(async () => { onMounted(async () => {
const token = useCookies().get('token') const token = useCookies().get('token')
@ -49,7 +88,7 @@ onMounted(async () => {
async function loginFunc() { async function loginFunc() {
// check if username and password are valid // check if username and password are valid
if (username.value === '' || password.value === '') { if (username.value === '' || password.value === '') {
gameStore.addNotification({ message: 'Please enter a valid username and password' }) loginError.value = 'Please enter a valid username and password'
return return
} }
@ -57,10 +96,9 @@ async function loginFunc() {
const response = await login(username.value, password.value) const response = await login(username.value, password.value)
if (response.success === undefined) { if (response.success === undefined) {
gameStore.addNotification({ message: response.error }) loginError.value = response.error
return return
} }
gameStore.setToken(response.token) gameStore.setToken(response.token)
gameStore.initConnection() gameStore.initConnection()
return true // Indicate success return true // Indicate success
@ -69,7 +107,7 @@ async function loginFunc() {
async function registerFunc() { async function registerFunc() {
// check if username and password are valid // check if username and password are valid
if (username.value === '' || password.value === '') { if (username.value === '' || password.value === '') {
gameStore.addNotification({ message: 'Please enter a valid username and password' }) loginError.value = 'Please enter a valid username and password'
return return
} }
@ -77,13 +115,14 @@ async function registerFunc() {
const response = await register(username.value, password.value) const response = await register(username.value, password.value)
if (response.success === undefined) { if (response.success === undefined) {
gameStore.addNotification({ message: response.error }) loginError.value = response.error
return return
} }
const loginSuccess = await loginFunc() const loginSuccess = await loginFunc()
if (!loginSuccess) { if (!loginSuccess) {
gameStore.addNotification({ message: 'Login after registration failed. Please try logging in manually.' }) loginError.value = 'Login after registration failed. Please try logging in manually.'
return
} }
} }
</script> </script>

View File

@ -1 +0,0 @@
<template></template>

View File

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

View File

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

View File

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

View File

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

View File

@ -4,9 +4,12 @@ export default {
theme: { theme: {
fontFamily: { fontFamily: {
'titles': ['Poppins', 'serif'], 'titles': ['Poppins', 'serif'],
'default': ['Inter', 'serif'] 'default': ['Quicksand', 'serif'],
'ui': ['Upheaval', 'serif'],
}, },
backgroundSize: { backgroundSize: {
'cover': 'cover',
'contain': 'contain',
'30px': '30px' '30px': '30px'
}, },
screens: { screens: {
@ -41,46 +44,59 @@ export default {
dropShadow: { dropShadow: {
'default': '0 3px 6px rgb(0, 0, 0)', 'default': '0 3px 6px rgb(0, 0, 0)',
'text': '1px 1px 5px rgba(0, 0, 0, 0.25)', 'text': '1px 1px 5px rgba(0, 0, 0, 0.25)',
'pixel': '2px 2px 0px rgb(77, 77, 77)',
'pixel-black': '2px 2px 0px rgb(0, 0, 0)',
'20': '0 3px 6px rgba(0, 0, 0, 0.2)' '20': '0 3px 6px rgba(0, 0, 0, 0.2)'
}, },
boxShadow: { boxShadow: {
'character': '0 4px 30px rgba(0, 0, 0, 0.1)', 'character': '0 4px 30px rgba(0, 0, 0, 0.1)',
}, },
colors: { colors: {
red: { cyan: {
DEFAULT: '#d50000', DEFAULT: '#0D6d69',
50: '#d50000', 50: '#f3faf8',
100: '#b30000' 100: '#D7F0EC',
200: '#b1dfd9',
300: '#80c8c1',
600: '#0D6D69',
800: '#244b4c',
900: '#204040',
950: '#0f2324'
}, },
bordeaux: { red: {
DEFAULT: '#800020', DEFAULT: '#e15970',
50: '#cc0033', 100: '#ffefef',
100: '#800020', 200: '#e15970',
200: '#4c0000' 300: '#b73f54',
400: '#332426'
}, },
blue: { blue: {
DEFAULT: '#00c2ff' DEFAULT: '#4F5FF0',
100: '#4F5FF0',
200: '#2E378A'
}, },
green: { green: {
DEFAULT: '#09ad19' DEFAULT: '#05E300',
100: '#05E300',
200: '#027D00'
}, },
purple: { purple: {
DEFAULT: '#9841e6' DEFAULT: '#9841e6'
}, },
cyan: {
DEFAULT: '#368f8b',
50: '#00b3b3',
100: '#368f8b',
200: '#376362'
},
gray: { gray: {
DEFAULT: '#7f7f7f', DEFAULT: '#262626',
50: '#d3d3d3', 50: '#fcfcfd',
100: '#7f7f7f', 100: '#f7f7f8',
200: '#696969', 150: '#f1f1f3',
300: '#313638', 175: '#e4e4e7',
500: '#778899', 200: '#999999',
600: '#B1B2B5' 300: '#808080',
400: '#666666',
500: '#4d4d4d',
600: '#333333',
700: '#262626',
800: '#1a1a1a',
900: '#0d0d0d'
} }
} }
}, },