Compare commits

...

96 Commits

Author SHA1 Message Date
aff32c33c7 npm update, mobile scroll fix, zone editor object refactor work, removed redundant div 2024-10-18 00:20:30 +02:00
774871510e Reordered func. params getTile(), npm run format, refactor zone object part in zone editor, other improvements 2024-10-17 19:26:45 +02:00
e61b705031 Smaller GmTools modal height 2024-10-17 18:41:14 +02:00
3902c611fa Fixed placing tiles 2024-10-17 18:39:18 +02:00
3765cfe5e9 Started refactoring zone editor 2024-10-17 02:32:39 +02:00
2fad54fd26 More styling tweaks, updated confirmation modal 2024-10-16 22:23:00 +02:00
be3cbf77bf Added global empty btn styling, adjusted and WIP modal styling 2024-10-16 20:47:29 +02:00
f24a498246 Re-added Vue Dev Tools. Problem fixed
https://github.com/vuejs/devtools-next/issues/635
2024-10-16 18:50:38 +02:00
9686381745 Removed unused impots, use config.name instead of hardcoded name 2024-10-16 16:07:35 +02:00
e42c530685 Bug fix for toggling zoneEditor 2024-10-16 16:06:18 +02:00
e56e078042 Separated Game and ZoneEditor screens 2024-10-16 16:04:43 +02:00
13be1a38fa Refactor App.vue 2024-10-16 16:04:29 +02:00
27e857b9a6 npm update, removed vue dev tools bc bug 2024-10-16 15:45:41 +02:00
2b2c290db0 Removed unused const. 2024-10-16 14:15:30 +02:00
245b50c1fd Moved component attributes to properties function for more streamlined experience 2024-10-16 14:15:01 +02:00
32dc7a2963 Removed register, it's in the same screen now 2024-10-16 14:11:43 +02:00
a9c2b209d9 Works partially 💩 2024-10-15 23:48:50 +02:00
a6c22df528 Fixed some bugs in login form, updated logo name and icons, wip of minimap 2024-10-15 20:27:54 +02:00
7dd2d70eca npm update 2024-10-15 15:49:12 +02:00
5fc3547d9c Replaced logo 2024-10-14 23:25:41 +02:00
b5c222cc05 Made eye icon on login/register functional 2024-10-14 22:05:12 +02:00
8b1efca7b8 Added world settings type and added it into gameStore 2024-10-14 20:11:30 +02:00
1bdd2bc75a npm update 2024-10-14 19:14:53 +02:00
24dff8d920 New Quest > Sylvan Quest 2024-10-14 15:46:02 +02:00
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
112 changed files with 2564 additions and 1664 deletions

5
.env.example Normal file
View File

@ -0,0 +1,5 @@
VITE_NAME=New Quest
VITE_DEVELOPMENT=true
VITE_SERVER_ENDPOINT=http://localhost:4000
VITE_TILE_SIZE_X=64
VITE_TILE_SIZE_Y=32

View File

@ -4,6 +4,24 @@ WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci
COPY . .
# Set environment variables
ARG VITE_NAME=${VITE_NAME}
ENV VITE_NAME=${VITE_NAME}
ARG VITE_DEVELOPMENT=${VITE_DEVELOPMENT}
ENV VITE_DEVELOPMENT=${VITE_DEVELOPMENT}
ARG VITE_SERVER_ENDPOINT=${VITE_SERVER_ENDPOINT}
ENV VITE_SERVER_ENDPOINT=${VITE_SERVER_ENDPOINT}
ARG VITE_TILE_SIZE_X=${VITE_TILE_SIZE_X}
ENV VITE_TILE_SIZE_X=${VITE_TILE_SIZE_X}
ARG VITE_TILE_SIZE_Y=${VITE_TILE_SIZE_Y}
ENV VITE_TILE_SIZE_Y=${VITE_TILE_SIZE_Y}
# Build the application
RUN npm run build-ntc
# Production stage

View File

@ -3,8 +3,8 @@
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>New Quest - Play</title>
<meta name="viewport" content="width=device-width">
<title>Sylvan Quest - Play</title>
</head>
<body>
<div id="app"></div>

1219
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

BIN
public/assets/fog.png Normal file

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,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="20" height="16" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M17.882 19.297A10.949 10.949 0 0 1 12 21c-5.392 0-9.878-3.88-10.819-9a10.982 10.982 0 0 1 3.34-6.066L1.392 2.808l1.415-1.415 19.799 19.8-1.415 1.414-3.31-3.31zM5.935 7.35A8.965 8.965 0 0 0 3.223 12a9.005 9.005 0 0 0 13.201 5.838l-2.028-2.028A4.5 4.5 0 0 1 8.19 9.604L5.935 7.35zm6.979 6.978l-3.242-3.242a2.5 2.5 0 0 0 3.241 3.241zm7.893 2.264l-1.431-1.43A8.935 8.935 0 0 0 20.777 12 9.005 9.005 0 0 0 9.552 5.338L7.974 3.76C9.221 3.27 10.58 3 12 3c5.392 0 9.878 3.88 10.819 9a10.947 10.947 0 0 1-2.012 4.592zm-9.084-9.084a4.5 4.5 0 0 1 4.769 4.769l-4.77-4.769z" fill="#808080"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 788 B

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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 325 B

After

Width:  |  Height:  |  Size: 597 KiB

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 15L15 21M21 8L8 21" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21 15L15 21M21 8L8 21" stroke="#4d4d4d" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 346 B

After

Width:  |  Height:  |  Size: 346 B

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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 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

@ -1,33 +1,29 @@
<template>
<div class="overflow-hidden">
<Notifications />
<Login v-if="screen === 'login'" />
<!-- <Register v-if="screen === 'register'" />-->
<Characters v-if="screen === 'characters'" />
<Game v-if="screen === 'game'" />
</div>
<component :is="currentScreen" />
</template>
<script setup lang="ts">
import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import Notifications from '@/components/utilities/Notifications.vue'
import Login from '@/screens/Login.vue'
// import Register from '@/screens/Register.vue'
import Characters from '@/screens/Characters.vue'
import Game from '@/screens/Game.vue'
import ZoneEditor from '@/screens/ZoneEditor.vue'
import { computed } from 'vue'
const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore()
const screen = computed(() => {
if (!gameStore.connection) {
return 'login'
} else if (gameStore.token && gameStore.connection) {
if (gameStore.character) {
return 'game'
}
return 'characters'
}
return 'login' // default fallback
const currentScreen = computed(() => {
if (!gameStore.connection) return Login
if (!gameStore.token) return Login
if (!gameStore.character) return Characters
if (zoneEditorStore.active) return ZoneEditor
return Game
})
// Disable right click
addEventListener('contextmenu', (event) => event.preventDefault())
</script>

Binary file not shown.

View File

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

110
src/components/Effects.vue Normal file
View File

@ -0,0 +1,110 @@
<template>
<Scene name="effects" @preload="preloadScene" @create="createScene" @update="updateScene"> </Scene>
</template>
<script setup lang="ts">
import { Scene } from 'phavuer'
import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { onBeforeMount, onBeforeUnmount, ref } from 'vue'
const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore()
// See if there's a dat
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)
fogSprite.value.setDepth(950)
}
const updateFogEffect = () => {
if (fogSprite.value) {
// Example: Oscillate fog opacity
const fogOpacity = ((Math.sin(Date.now() / 5000) + 1) / 2) * 0.3
fogSprite.value.setAlpha(fogOpacity)
}
}
// Expose methods to control effects
const controlEffects = {
toggleRain,
setFogDensity: (density: number) => {
if (fogSprite.value) {
fogSprite.value.setAlpha(density)
}
}
}
// Make control methods available to parent components
defineExpose(controlEffects)
onBeforeUnmount(() => {
if (sceneRef.value) sceneRef.value.scene.remove('effects')
})
</script>

View File

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

View File

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

View File

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

View File

@ -4,38 +4,38 @@
<!-- Asset Categories -->
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'tiles' }" @click="() => (selectedCategory = 'tiles')">
<span>Tiles</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'objects' }" @click="() => (selectedCategory = 'objects')">
<span>Objects</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'sprites' }" @click="() => (selectedCategory = 'sprites')">
<span>Sprites</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
<a class="relative p-2.5 hover:cursor-pointer">
<span>Items</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
<a class="relative p-2.5 hover:cursor-pointer">
<span>NPC's</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
<a class="relative p-2.5 hover:cursor-pointer">
<span>Characters</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
<a class="relative p-2.5 hover:cursor-pointer">
<span>Mounts</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
<a class="relative p-2.5 hover:cursor-pointer">
<span>Pets</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
</div>
<div class="absolute w-px bg-cyan-200 h-full top-0 left-1/6"></div>
<div class="absolute w-px bg-gray-500 h-full top-0 left-1/6"></div>
<!-- Assets list -->
<div class="overflow-auto h-full w-4/12 flex flex-col relative">
@ -44,7 +44,7 @@
<SpriteList v-if="selectedCategory === 'sprites'" />
</div>
<div class="absolute w-px bg-cyan-200 h-full top-0 left-1/2"></div>
<div class="absolute w-px bg-gray-500 h-full top-0 left-1/2"></div>
<!-- Asset details -->
<div class="flex w-1/2 after:hidden flex-col relative overflow-auto">

View File

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

View File

@ -1,13 +1,13 @@
<template>
<div class="relative p-2.5 flex items-center gap-x-2.5">
<input v-model="searchQuery" class="input-cyan flex-grow" placeholder="Search..." @input="handleSearch" />
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
<label for="upload-asset" class="bg-cyan/50 border border-solid border-white/25 rounded drop-shadow-20 p-2 inline-flex items-center justify-center hover:bg-cyan hover:cursor-pointer">
<input class="hidden" id="upload-asset" ref="objectUploadField" type="file" accept="image/png" multiple @change="handleFileUpload" />
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
</label>
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</div>
<div v-bind="containerProps" class="overflow-y-auto relative" @scroll="onScroll">
<div v-bind="wrapperProps" ref="elementToScroll">
@ -18,10 +18,10 @@
</div>
<span>{{ object.name }}</span>
</div>
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
</div>
<button class="left-[calc(50%_-_60px)] fixed bottom-2.5 min-w-[unset] w-12 h-12 rounded-lg bg-cyan/50 p-0 hover:bg-cyan" v-show="hasScrolled" @click="toTop">
<button class="left-[calc(50%_-_60px)] fixed bottom-2.5 min-w-[unset] w-12 h-12 rounded-md bg-cyan/50 p-0 hover:bg-cyan" v-show="hasScrolled" @click="toTop">
<img class="absolute invert w-8 h-8 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
</button>
</div>

View File

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

View File

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

View File

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

View File

@ -1,13 +1,13 @@
<template>
<div class="relative p-2.5 flex items-center gap-x-2.5">
<input v-model="searchQuery" class="input-cyan flex-grow" placeholder="Search..." @input="handleSearch" />
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
<label for="upload-asset" class="bg-cyan/50 border border-solid border-white/25 rounded drop-shadow-20 p-2 inline-flex items-center justify-center hover:bg-cyan hover:cursor-pointer">
<input class="hidden" id="upload-asset" ref="tileUploadField" type="file" accept="image/png" multiple @change="handleFileUpload" />
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
</label>
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</div>
<div v-bind="containerProps" class="overflow-y-auto relative" @scroll="onScroll">
<div v-bind="wrapperProps" ref="elementToScroll">
@ -18,10 +18,10 @@
</div>
<span>{{ tile.name }}</span>
</div>
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
</div>
<button class="left-[calc(50%_-_60px)] fixed bottom-2.5 min-w-[unset] w-12 h-12 rounded-lg bg-cyan/50 p-0 hover:bg-cyan" v-show="hasScrolled" @click="toTop">
<button class="left-[calc(50%_-_60px)] fixed bottom-2.5 min-w-[unset] w-12 h-12 rounded-md bg-cyan/50 p-0 hover:bg-cyan" v-show="hasScrolled" @click="toTop">
<img class="absolute invert w-8 h-8 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
</button>
</div>

View File

@ -0,0 +1,14 @@
<template></template>
<script setup lang="ts">
import type { ZoneEventTile } from '@/types'
import { tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
// function getEventTileImageProps(tile: ZoneEventTile) {
// return {
// x: tileToWorldX(zoneTilemap as any, tile.positionX, tile.positionY),
// y: tileToWorldY(zoneTilemap as any, tile.positionX, tile.positionY),
// texture: tile.type
// }
// }
</script>

View File

@ -0,0 +1,126 @@
<template>
<SelectedZoneObject v-if="selectedZoneObject" :zoneObject="selectedZoneObject" />
<Image
v-for="object in zoneEditorStore.zone?.zoneObjects"
v-bind="getObjectImageProps(object)"
@pointerup="() => selectedZoneObject = object"
/>
</template>
<script setup lang="ts">
import { uuidv4 } from '@/utilities'
import { calculateIsometricDepth, getTile, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
import { Image, useScene } from 'phavuer'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import type { ZoneObject } from '@/types'
import SelectedZoneObject from '@/components/gameMaster/zoneEditor/partials/SelectedZoneObject.vue'
import { onBeforeMount, onBeforeUnmount, ref, watch } from 'vue'
const scene = useScene()
const zoneEditorStore = useZoneEditorStore()
const selectedZoneObject = ref<ZoneObject | null>(null)
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
}>()
function getObjectImageProps(object: ZoneObject) {
return {
// alpha: object.id === movingZoneObject.value?.id ? .5 : 1,
depth: calculateIsometricDepth(object.positionX, object.positionY, object.object.frameWidth, object.object.frameHeight),
tint: selectedZoneObject.value?.id === object.id ? 0x00ff00 : 0xffffff,
x: tileToWorldX(props.tilemap as any, object.positionX, object.positionY),
y: tileToWorldY(props.tilemap as any, object.positionX, object.positionY),
flipX: object.isRotated,
texture: object.object.id,
originY: Number(object.object.originX),
originX: Number(object.object.originY)
}
}
function addZoneObject(pointer: Phaser.Input.Pointer) {
if (!zoneEditorStore.zone) return
// Check if tool is pencil
if (zoneEditorStore.tool !== 'pencil') return
// Check if draw mode is object
if (zoneEditorStore.drawMode !== 'object') return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if there is a tile @TODO chekc if props.tilemap words
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if there is a selected object
if (!zoneEditorStore.selectedObject) return
// Check if object already exists on position
const existingObject = zoneEditorStore.zone?.zoneObjects.find((object) => object.positionX === tile.x && object.positionY === tile.y)
if (existingObject) return
const newObject = {
id: uuidv4(),
zoneId: zoneEditorStore.zone.id,
zone: zoneEditorStore.zone,
object: zoneEditorStore.selectedObject,
depth: 0,
isRotated: false,
positionX: tile.x,
positionY: tile.y
}
// Add new object to zoneObjects
zoneEditorStore.zone.zoneObjects = zoneEditorStore.zone.zoneObjects.concat(newObject as ZoneObject)
}
onBeforeMount(() => {
scene.input.on(Phaser.Input.Events.POINTER_DOWN, addZoneObject)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, addZoneObject)
})
onBeforeUnmount(() => {
scene.input.off(Phaser.Input.Events.POINTER_DOWN, addZoneObject)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, addZoneObject)
})
// watch zoneEditorStore.objectList and update originX and originY of objects in zoneObjects
watch(
zoneEditorStore.objectList,
(newObjects) => {
// Check if zoneEditorStore.zone is set
if (!zoneEditorStore.zone) return
// Update zoneObjects
zoneEditorStore.zone.zoneObjects = zoneEditorStore.zone.zoneObjects.map((zoneObject) => {
const updatedObject = newObjects.find((obj) => obj.id === zoneObject.objectId)
if (updatedObject) {
return {
...zoneObject,
object: {
...zoneObject.object,
originX: updatedObject.originX,
originY: updatedObject.originY
}
}
}
return zoneObject
})
// Update selectedObject if it's set
if (zoneEditorStore.selectedObject) {
const updatedObject = newObjects.find((obj) => obj.id === zoneEditorStore.selectedObject?.id)
if (updatedObject) {
zoneEditorStore.setSelectedObject({
...zoneEditorStore.selectedObject,
originX: updatedObject.originX,
originY: updatedObject.originY
})
}
}
},
{ deep: true }
)
</script>

View File

@ -0,0 +1,91 @@
<template>
<Controls :layer="tiles" :depth="0" />
</template>
<script setup lang="ts">
import config from '@/config'
import { useScene } from 'phavuer'
import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { onBeforeMount, onBeforeUnmount } from 'vue'
import { getTile, placeTile, setAllTiles } from '@/composables/zoneComposable'
import Controls from '@/components/utilities/Controls.vue'
const emit = defineEmits(['tilemap:create'])
const scene = useScene()
const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore()
const zoneTilemap = createTilemap()
const tiles = createTileLayer()
let tileArray = createTileArray()
function createTilemap() {
const zoneData = new Phaser.Tilemaps.MapData({
width: zoneEditorStore.zone?.width,
height: zoneEditorStore.zone?.height,
tileWidth: config.tile_size.x,
tileHeight: config.tile_size.y,
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
format: Phaser.Tilemaps.Formats.ARRAY_2D
})
const tilemap = new Phaser.Tilemaps.Tilemap(scene, zoneData)
emit('tilemap:create', tilemap)
return tilemap
}
function createTileLayer() {
const tilesetImages = gameStore.assets.filter((asset) => asset.group === 'tiles').map((asset, index) => zoneTilemap.addTilesetImage(asset.key, asset.key, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y }))
tilesetImages.push(zoneTilemap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 1, 2, 0, { x: 0, y: -config.tile_size.y }))
const layer = zoneTilemap.createBlankLayer('tiles', tilesetImages as any, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
layer.setDepth(0)
layer.setCullPadding(2, 2)
return layer
}
function createTileArray() {
return Array.from({ length: zoneTilemap.height || 0 }, () => Array.from({ length: zoneTilemap.width || 0 }, () => 'blank_tile'))
}
function handleTileClick(pointer: Phaser.Input.Pointer) {
// Check if tool is pencil
if (zoneEditorStore.tool !== 'pencil') return
// Check if draw mode is tile
if (zoneEditorStore.drawMode !== 'tile') return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if there is a tile
const tile = getTile(tiles, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if there is a selected tile
if (!zoneEditorStore.selectedTile) return
placeTile(zoneTilemap, tiles, tile.x, tile.y, zoneEditorStore.selectedTile.id)
}
onBeforeMount(() => {
if (!zoneEditorStore.zone?.tiles) {
return
}
setAllTiles(zoneTilemap, tiles, zoneEditorStore.zone.tiles)
tileArray = zoneEditorStore.zone.tiles.map((row) => row.map((tileId) => tileId || 'blank_tile'))
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handleTileClick)
})
onBeforeUnmount(() => {
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handleTileClick)
zoneTilemap.destroyLayer('tiles')
zoneTilemap.removeAllLayers()
zoneTilemap.destroy()
})
</script>

View File

@ -1,214 +1,58 @@
<template>
<Toolbar :layer="tiles" @eraser="eraser" @pencil="pencil" @paint="paint" @clear="clear" @save="save" />
<ZoneList v-if="zoneEditorStore.isZoneListModalShown" />
<Tiles @tilemap:create="tileMap = $event" />
<Objects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
<EventTiles v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
<template v-if="zoneEditorStore.zone">
<Controls :layer="tiles as TilemapLayer" />
<Toolbar @save="save" />
<Tiles />
<Objects />
<ZoneList />
<TileList />
<ObjectList />
<ZoneSettings />
<TeleportModal v-if="shouldShowTeleportModal" />
<Container :depth="2">
<Image v-for="object in zoneObjects" :depth="calculateIsometricDepth(object.positionX, object.positionY, 0)" :key="object.id" v-bind="getObjectImageProps(object)" @pointerup="() => setSelectedZoneObject(object)" />
</Container>
<Container :depth="3">
<Image v-for="tile in zoneEventTiles" :key="tile.id" v-bind="getEventTileImageProps(tile)" />
</Container>
<SelectedZoneObject v-if="zoneEditorStore.selectedZoneObject" @update_depth="updateZoneObjectDepth" @delete="deleteZoneObject" @move="handleMove" />
</template>
<TeleportModal />
</template>
<script setup lang="ts">
import { computed, onBeforeMount, onMounted, onUnmounted, ref, watch } from 'vue'
import { Container, Image, useScene } from 'phavuer'
import { storeToRefs } from 'pinia'
import { onBeforeMount, onUnmounted, ref } from 'vue'
import { useScene } from 'phavuer'
import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { calculateIsometricDepth, loadAssets, placeTile, setAllTiles, sortByIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
import { ZoneEventTileType, type ZoneObject, type ZoneEventTile, type Zone } from '@/types'
import { uuidv4 } from '@/utilities'
import config from '@/config'
import { loadAssets } from '@/composables/zoneComposable'
import { type ZoneObject, type ZoneEventTile, type Zone } from '@/types'
// Components
import Controls from '@/components/utilities/Controls.vue'
import Toolbar from '@/components/gameMaster/zoneEditor/partials/Toolbar.vue'
import Tiles from '@/components/gameMaster/zoneEditor/partials/TileList.vue'
import SelectedZoneObject from '@/components/gameMaster/zoneEditor/partials/SelectedZoneObject.vue'
import TileList from '@/components/gameMaster/zoneEditor/partials/TileList.vue'
import ObjectList from '@/components/gameMaster/zoneEditor/partials/ObjectList.vue'
import ZoneSettings from '@/components/gameMaster/zoneEditor/partials/ZoneSettings.vue'
import Objects from '@/components/gameMaster/zoneEditor/partials/ObjectList.vue'
import ZoneList from '@/components/gameMaster/zoneEditor/partials/ZoneList.vue'
import TeleportModal from '@/components/gameMaster/zoneEditor/partials/TeleportModal.vue'
import Tilemap = Phaser.Tilemaps.Tilemap
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
/**
* @TODO:
* Clean all the code in this file
*/
import Tiles from '@/components/gameMaster/zoneEditor/Tiles.vue'
import Objects from '@/components/gameMaster/zoneEditor/Objects.vue'
import EventTiles from '@/components/gameMaster/zoneEditor/EventTiles.vue'
const scene = useScene()
const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore()
const { objectList, zone, selectedTile, selectedObject, selectedZoneObject, eraserMode, drawMode } = storeToRefs(zoneEditorStore)
const zoneTilemap = createTilemap()
const tiles = createTileLayer()
const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null)
const tileArray = ref<string[][]>([])
const zoneObjects = ref<ZoneObject[]>([])
const zoneEventTiles = ref<ZoneEventTile[]>([])
let tileArray = createTileArray()
const shouldShowTeleportModal = computed(() => zoneEditorStore.tool === 'pencil' && drawMode.value === 'teleport')
function createTilemap() {
const zoneData = new Phaser.Tilemaps.MapData({
width: zone.value?.width ?? 10,
height: zone.value?.height ?? 10,
tileWidth: config.tile_size.x,
tileHeight: config.tile_size.y,
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
format: Phaser.Tilemaps.Formats.ARRAY_2D
})
const tilemap = new Phaser.Tilemaps.Tilemap(scene, zoneData)
return tilemap
}
function createTileLayer() {
const tilesetImages = gameStore.assets.filter((asset) => asset.group === 'tiles').map((asset, index) => zoneTilemap.addTilesetImage(asset.key, asset.key, config.tile_size.x, config.tile_size.y, 0, 0, index + 1, { x: 0, y: -config.tile_size.y }))
tilesetImages.push(zoneTilemap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 0, 0, 0, { x: 0, y: -config.tile_size.y }))
const layer = zoneTilemap.createBlankLayer('tiles', tilesetImages as any, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
layer.setDepth(0)
return layer
}
function createTileArray() {
return Array.from({ length: zoneTilemap.height || 0 }, () => Array.from({ length: zoneTilemap.width || 0 }, () => 'blank_tile'))
}
function getObjectImageProps(object: ZoneObject) {
return {
tint: selectedZoneObject.value?.id === object.id ? 0x00ff00 : 0xffffff,
x: tileToWorldX(zoneTilemap as any, object.positionX, object.positionY),
y: tileToWorldY(zoneTilemap as any, object.positionX, object.positionY),
texture: object.object.id,
originY: Number(object.object.originX),
originX: Number(object.object.originY)
}
}
function getEventTileImageProps(tile: ZoneEventTile) {
return {
x: tileToWorldX(zoneTilemap as any, tile.positionX, tile.positionY),
y: tileToWorldY(zoneTilemap as any, tile.positionX, tile.positionY),
texture: tile.type
}
}
function eraser(tile: Phaser.Tilemaps.Tile) {
if (eraserMode.value === 'tile') {
placeTile(zoneTilemap as Tilemap, tiles as TilemapLayer, tile.x, tile.y, 'blank_tile')
tileArray[tile.y][tile.x] = 'blank_tile'
} else if (eraserMode.value === 'object') {
zoneObjects.value = zoneObjects.value.filter((object) => object.positionX !== tile.x || object.positionY !== tile.y)
} else if (eraserMode.value === 'blocking tile' || eraserMode.value === 'teleport') {
zoneEventTiles.value = zoneEventTiles.value.filter((eventTile) => eventTile.positionX !== tile.x || eventTile.positionY !== tile.y || (eraserMode.value === 'teleport' && eventTile.type !== ZoneEventTileType.TELEPORT))
}
}
function pencil(tile: Phaser.Tilemaps.Tile) {
if (drawMode.value === 'tile' && selectedTile.value) {
placeTile(zoneTilemap as Tilemap, tiles as TilemapLayer, tile.x, tile.y, selectedTile.value.id)
tileArray[tile.y][tile.x] = selectedTile.value.id
} else if (drawMode.value === 'object' && selectedObject.value) {
addZoneObject(tile)
} else if (drawMode.value === 'blocking tile' || drawMode.value === 'teleport') {
addZoneEventTile(tile)
}
}
function addZoneObject(tile: Phaser.Tilemaps.Tile) {
// Check if object already exists on position
const existingObject = zoneObjects.value.find((object) => object.positionX === tile.x && object.positionY === tile.y)
if (existingObject) return
const newObject = {
id: uuidv4(),
zoneId: zone.value!.id,
zone: zone.value!,
objectId: selectedObject.value!.id,
object: selectedObject.value!,
depth: 0,
positionX: tile.x,
positionY: tile.y
}
// Add new object to zoneObjects
zoneObjects.value = zoneObjects.value.concat(newObject)
}
function addZoneEventTile(tile: Phaser.Tilemaps.Tile) {
// Check if event tile already exists on position
const existingEventTile = zoneEventTiles.value.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y)
if (existingEventTile) return
const newEventTile = {
id: uuidv4(),
zoneId: zone.value!.id,
zone: zone.value!,
type: drawMode.value === 'blocking tile' ? ZoneEventTileType.BLOCK : ZoneEventTileType.TELEPORT,
positionX: tile.x,
positionY: tile.y,
teleport:
drawMode.value === 'teleport'
? {
toZoneId: zoneEditorStore.teleportSettings.toZoneId,
toPositionX: zoneEditorStore.teleportSettings.toPositionX,
toPositionY: zoneEditorStore.teleportSettings.toPositionY,
toRotation: zoneEditorStore.teleportSettings.toRotation
}
: undefined
}
zoneEventTiles.value = zoneEventTiles.value.concat(newEventTile as any)
}
function paint() {
if (!selectedTile.value) return
// Ensure tileArray is initialized with correct dimensions
if (!tileArray || tileArray.length !== zoneTilemap.height) {
tileArray = Array.from({ length: zoneTilemap.height }, () => Array.from({ length: zoneTilemap.width }, () => 'blank_tile'))
}
// Set all tiles in the tilemap to the selected tile's id
for (let y = 0; y < zoneTilemap.height; y++) {
if (!tileArray[y]) {
tileArray[y] = Array(zoneTilemap.width).fill('blank_tile')
}
for (let x = 0; x < zoneTilemap.width; x++) {
placeTile(zoneTilemap as Tilemap, tiles as TilemapLayer, x, y, selectedTile.value.id)
tileArray[y][x] = selectedTile.value.id
}
}
}
function save() {
if (!zone.value) return
if (!zoneEditorStore.zone) return
const data = {
zoneId: zone.value.id,
zoneId: zoneEditorStore.zone.id,
name: zoneEditorStore.zoneSettings.name,
width: zoneEditorStore.zoneSettings.width,
height: zoneEditorStore.zoneSettings.height,
tiles: tileArray,
pvp: zone.value.pvp,
pvp: zoneEditorStore.zone.pvp,
zoneEventTiles: zoneEventTiles.value.map(({ id, zoneId, type, positionX, positionY, teleport }) => ({ id, zoneId, type, positionX, positionY, teleport })),
zoneObjects: zoneObjects.value.map(({ id, zoneId, objectId, depth, positionX, positionY }) => ({ id, zoneId, objectId, depth, positionX, positionY }))
zoneObjects: zoneObjects.value.map(({ id, zoneId, objectId, depth, isRotated, positionX, positionY }) => ({ id, zoneId, objectId, depth, isRotated, positionX, positionY }))
}
if (zoneEditorStore.isSettingsModalShown) {
@ -216,106 +60,16 @@ function save() {
}
gameStore.connection?.emit('gm:zone_editor:zone:update', data, (response: Zone) => {
console.log('zone updated')
zoneEditorStore.setZone(response)
})
}
function clear() {
for (let y = 0; y < zoneTilemap.height; y++) {
if (!tileArray[y]) {
tileArray[y] = Array(zoneTilemap.width).fill('blank_tile')
}
for (let x = 0; x < zoneTilemap.width; x++) {
placeTile(zoneTilemap as Tilemap, tiles as TilemapLayer, x, y, 'blank_tile')
tileArray[y][x] = 'blank_tile'
}
}
zoneEventTiles.value = []
zoneObjects.value = []
}
function updateZoneObjectDepth(depth: number) {
zoneObjects.value = zoneObjects.value.map((object) => (object.id === selectedZoneObject.value?.id ? { ...object, depth } : object))
}
function deleteZoneObject(objectId: string) {
zoneObjects.value = zoneObjects.value.filter((object) => object.id !== objectId)
}
function handleMove() {
console.log('move btn clicked')
}
onBeforeMount(async () => {
tileArray.forEach((row, y) => row.forEach((_, x) => placeTile(zoneTilemap, tiles, x, y, 'blank_tile')))
if (zone.value?.tiles) {
setAllTiles(zoneTilemap, tiles, zone.value.tiles)
tileArray = zone.value.tiles.map((row) => row.map((tileId) => tileId || 'blank_tile'))
}
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(
objectList,
(newObjects) => {
zoneObjects.value = zoneObjects.value.map((zoneObject) => {
const updatedObject = newObjects.find((obj) => obj.id === zoneObject.objectId)
if (updatedObject) {
return {
...zoneObject,
object: {
...zoneObject.object,
originX: updatedObject.originX,
originY: updatedObject.originY
}
}
}
return zoneObject
})
// Update selectedObject if it exists
if (zoneEditorStore.selectedObject) {
const updatedObject = newObjects.find((obj) => obj.id === zoneEditorStore.selectedObject?.id)
if (updatedObject) {
zoneEditorStore.setSelectedObject({
...zoneEditorStore.selectedObject,
originX: updatedObject.originX,
originY: updatedObject.originY
})
}
}
},
{ deep: true }
)
const setSelectedZoneObject = (zoneObject: ZoneObject | null) => {
if (!zoneObject) return
if (zoneEditorStore.tool !== 'move') return
zoneEditorStore.setSelectedZoneObject(zoneObject)
}
onBeforeMount(async () => {
await gameStore.fetchAllZoneAssets()
await loadAssets(scene)
})
onUnmounted(() => {
zoneEditorStore.reset()
})
</script>

View File

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

View File

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

View File

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

View File

@ -1,24 +1,23 @@
<template>
<Modal :is-modal-open="true" @modal:close="() => zoneEditorStore.setTool('move')" :modal-width="300" :modal-height="350" :is-resizable="false">
<Modal :is-modal-open="showTeleportModal" @modal:close="() => zoneEditorStore.setTool('move')" :modal-width="300" :modal-height="350" :is-resizable="false">
<template #modalHeader>
<h3 class="m-0 font-medium shrink-0">Teleport settings</h3>
<h3 class="m-0 font-medium shrink-0 text-white">Teleport settings</h3>
</template>
<template #modalBody>
<div class="m-4">
<form method="post" @submit.prevent="" class="inline">
<div class="gap-2.5 flex flex-wrap">
<div class="form-field-half">
<label for="positionX">Position X</label>
<input class="input-cyan" v-model="toPositionX" name="positionX" id="positionX" type="number" />
<input class="input-field" v-model="toPositionX" name="positionX" id="positionX" type="number" />
</div>
<div class="form-field-half">
<label for="positionY">Position Y</label>
<input class="input-cyan" v-model="toPositionY" name="positionY" id="positionY" type="number" />
<input class="input-field" v-model="toPositionY" name="positionY" id="positionY" type="number" />
</div>
<div class="form-field-full">
<label for="rotation">Rotation</label>
<select v-model="toRotation" class="input-cyan" name="rotation" id="rotation">
<select v-model="toRotation" class="input-field" name="rotation" id="rotation">
<option :value="0">North</option>
<option :value="2">East</option>
<option :value="4">South</option>
@ -27,7 +26,7 @@
</div>
<div class="form-field-full">
<label for="toZoneId">Zone to teleport to</label>
<select v-model="toZoneId" class="input-cyan" name="toZoneId" id="toZoneId">
<select v-model="toZoneId" class="input-field" name="toZoneId" id="toZoneId">
<option :value="0">Select zone</option>
<option v-for="zone in zoneEditorStore.zoneList" :key="zone.id" :value="zone.id">{{ zone.name }}</option>
</select>
@ -40,12 +39,13 @@
</template>
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import Modal from '@/components/utilities/Modal.vue'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { useGameStore } from '@/stores/gameStore'
import type { Zone } from '@/types'
const showTeleportModal = computed(() => zoneEditorStore.tool === 'pencil' && zoneEditorStore.drawMode === 'teleport')
const zoneEditorStore = useZoneEditorStore()
const gameStore = useGameStore()

View File

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

View File

@ -1,21 +1,21 @@
<template>
<div class="flex justify-center p-5">
<div class="toolbar fixed bottom-0 m-3 rounded left-0 right-0 flex bg-gray-300/80 solid border-solid border-2 border-cyan ext-gray-50 p-1.5 px-3 p min-w-11/12 h-10">
<div ref="clickOutsideElement" class="tools flex gap-2.5" v-if="zoneEditorStore.zone">
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan-50 gap-2.5': zoneEditorStore.tool === 'move' }" @click="handleClick('move')">
<div class="toolbar fixed bottom-0 left-0 m-3 rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10">
<div ref="toolbar" class="tools flex gap-2.5" v-if="zoneEditorStore.zone">
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': zoneEditorStore.tool === 'move' }" @click="handleClick('move')">
<img class="invert w-5 h-5" src="/assets/icons/zoneEditor/move.svg" alt="Move camera" /> <span :class="{ 'ml-2.5': zoneEditorStore.tool !== 'move' }">(M)</span>
</button>
<div class="w-px bg-cyan"></div>
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan-50 gap-2.5': zoneEditorStore.tool === 'pencil' }" @click="handleClick('pencil')">
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': zoneEditorStore.tool === 'pencil' }" @click="handleClick('pencil')">
<img class="invert w-5 h-5" src="/assets/icons/zoneEditor/pencil.svg" alt="Pencil" /> <span :class="{ 'ml-2.5': zoneEditorStore.tool !== 'pencil' }">(P)</span>
<div class="select" v-if="zoneEditorStore.tool === 'pencil'">
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectPencilOpen }">
{{ zoneEditorStore.drawMode }}
<img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/zoneEditor/chevron.svg" />
</div>
<div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray-300/80 rounded min-w-28 border border-cyan border-solid text-left" v-show="selectPencilOpen && zoneEditorStore.tool === 'pencil'">
<div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectPencilOpen && zoneEditorStore.tool === 'pencil'">
<span class="py-2 px-2.5 relative hover:bg-cyan/50" @click="setDrawMode('tile')">
Tile
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
@ -35,14 +35,14 @@
<div class="w-px bg-cyan"></div>
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan-50 gap-2.5': zoneEditorStore.tool === 'eraser' }" @click="handleClick('eraser')">
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': zoneEditorStore.tool === 'eraser' }" @click="handleClick('eraser')">
<img class="invert w-5 h-5" src="/assets/icons/zoneEditor/eraser.svg" alt="Eraser" /> <span :class="{ 'ml-2.5': zoneEditorStore.tool !== 'eraser' }">(E)</span>
<div class="select" v-if="zoneEditorStore.tool === 'eraser'">
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectEraserOpen }">
{{ zoneEditorStore.eraserMode }}
<img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/zoneEditor/chevron.svg" />
</div>
<div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray-300/80 rounded min-w-28 border border-cyan border-solid text-left" v-show="selectEraserOpen">
<div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectEraserOpen">
<span class="py-2 px-2.5 relative hover:bg-cyan/50" @click="setEraserMode('tile')">
Tile
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
@ -62,7 +62,7 @@
<div class="w-px bg-cyan"></div>
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan-50 gap-2.5': zoneEditorStore.tool === 'paint' }" @click="handleClick('paint')">
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': zoneEditorStore.tool === 'paint' }" @click="handleClick('paint')">
<img class="invert w-5 h-5" src="/assets/icons/zoneEditor/paint.svg" alt="Paint bucket" /> <span :class="{ 'ml-2.5': zoneEditorStore.tool !== 'paint' }">(B)</span>
</button>
@ -71,7 +71,7 @@
<button class="flex justify-center items-center min-w-10 p-0 relative" @click="handleClick('settings')" v-if="zoneEditorStore.zone"><img class="invert w-5 h-5" src="/assets/icons/zoneEditor/gear.svg" alt="Zone settings" /> <span class="ml-2.5">(Z)</span></button>
</div>
<div class="flex gap-2.5 ml-auto">
<div class="toolbar fixed bottom-0 right-0 m-3 rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10 space-x-2">
<button class="btn-cyan px-3.5" @click="() => zoneEditorStore.toggleZoneListModal()">Load</button>
<button class="btn-cyan px-3.5" @click="() => emit('save')" v-if="zoneEditorStore.zone">Save</button>
<button class="btn-cyan px-3.5" @click="() => emit('clear')" v-if="zoneEditorStore.zone">Clear</button>
@ -83,22 +83,15 @@
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue'
import { useScene } from 'phavuer'
import { getTile } from '@/composables/zoneComposable'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { onClickOutside } from '@vueuse/core'
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
const zoneEditorStore = useZoneEditorStore()
const props = defineProps({
layer: Phaser.Tilemaps.TilemapLayer
})
const scene = useScene()
const emit = defineEmits(['move', 'eraser', 'pencil', 'paint', 'save', 'clear'])
const emit = defineEmits(['save', 'clear'])
// track when clicked outside of toolbar items
const clickOutsideElement = ref(null)
const toolbar = ref(null)
// track select state
let selectPencilOpen = ref(false)
@ -119,47 +112,6 @@ function setEraserMode(value: string) {
selectEraserOpen.value = false
}
function clickTile(pointer: Phaser.Input.Pointer) {
if (zoneEditorStore.tool !== 'eraser' && zoneEditorStore.tool !== 'pencil' && zoneEditorStore.tool !== 'paint') return
if (pointer.event.shiftKey) return
const px = scene.cameras.main.worldView.x + pointer.x
const py = scene.cameras.main.worldView.y + pointer.y
const pointer_tile = getTile(px, py, props.layer as TilemapLayer) as Phaser.Tilemaps.Tile
if (!pointer_tile) return
if (zoneEditorStore.tool === 'eraser') {
emit('eraser', pointer_tile)
}
if (zoneEditorStore.tool === 'pencil') {
emit('pencil', pointer_tile)
}
if (zoneEditorStore.tool === 'paint') {
emit('paint', pointer_tile)
}
}
function drawTiles(pointer: Phaser.Input.Pointer) {
if (!pointer.isDown) return
clickTile(pointer)
}
scene.input.on(Phaser.Input.Events.POINTER_UP, clickTile)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, drawTiles)
onMounted(() => {
addEventListener('keydown', initKeyShortcuts)
})
onBeforeUnmount(() => {
scene.input.off(Phaser.Input.Events.POINTER_UP, clickTile)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, drawTiles)
removeEventListener('keydown', initKeyShortcuts)
})
function handleClick(tool: string) {
if (tool === 'settings') {
zoneEditorStore.toggleSettingsModal()
@ -171,13 +123,26 @@ function handleClick(tool: string) {
selectEraserOpen.value = tool === 'eraser' ? !selectEraserOpen.value : false
}
// Key bindings
function cycleToolMode(tool: 'pencil' | 'eraser') {
const modes = ['tile', 'object', 'teleport', 'blocking tile']
const currentMode = tool === 'pencil' ? zoneEditorStore.drawMode : zoneEditorStore.eraserMode
const currentIndex = modes.indexOf(currentMode)
const nextIndex = (currentIndex + 1) % modes.length
const nextMode = modes[nextIndex]
if (tool === 'pencil') {
setDrawMode(nextMode)
} else {
setEraserMode(nextMode)
}
}
function initKeyShortcuts(event: KeyboardEvent) {
if (!zoneEditorStore.zone) return
// prevent if focused on composables
if (document.activeElement?.tagName === 'INPUT') return
const keyActions: any = {
const keyActions: { [key: string]: string } = {
m: 'move',
p: 'pencil',
e: 'eraser',
@ -186,14 +151,26 @@ function initKeyShortcuts(event: KeyboardEvent) {
}
if (keyActions.hasOwnProperty(event.key)) {
handleClick(keyActions[event.key])
const tool = keyActions[event.key]
if ((tool === 'pencil' || tool === 'eraser') && zoneEditorStore.tool === tool) {
cycleToolMode(tool)
} else {
handleClick(tool)
}
}
}
onClickOutside(clickOutsideElement, handleClickOutside)
function handleClickOutside() {
selectPencilOpen.value = false
selectEraserOpen.value = false
}
onClickOutside(toolbar, handleClickOutside)
onMounted(() => {
addEventListener('keydown', initKeyShortcuts)
})
onBeforeUnmount(() => {
removeEventListener('keydown', initKeyShortcuts)
})
</script>

View File

@ -1,10 +1,8 @@
<template>
<CreateZone v-if="zoneEditorStore.isCreateZoneModalShown" />
<Teleport to="body">
<Modal @modal:close="() => zoneEditorStore.toggleZoneListModal()" :is-resizable="false" :is-modal-open="true" :modal-width="300" :modal-height="360">
<Modal :is-modal-open="zoneEditorStore.isZoneListModalShown" @modal:close="() => zoneEditorStore.toggleZoneListModal()" :is-resizable="false" :modal-width="300" :modal-height="360">
<template #modalHeader>
<h3 class="text-lg">Zones</h3>
<h3 class="text-lg text-white">Zones</h3>
</template>
<template #modalBody>
<div class="my-4 mx-auto">
@ -13,19 +11,18 @@
<button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="() => zoneEditorStore.toggleCreateZoneModal()">New</button>
</div>
<div class="relative p-2.5 cursor-pointer flex gap-y-2.5 gap-x-5 flex-wrap" v-for="(zone, index) in zoneEditorStore.zoneList" :key="zone.id">
<div class="absolute left-0 top-0 w-full h-px bg-cyan-200" v-if="index === 0"></div>
<div class="absolute left-0 top-0 w-full h-px bg-gray-500" v-if="index === 0"></div>
<div class="flex gap-3 items-center w-full" @click="() => loadZone(zone.id)">
<span>{{ zone.name }}</span>
<span class="ml-auto gap-1 flex">
<button class="btn-bordeaux py-0.5 px-2.5 z-50" @click.stop="() => deleteZone(zone.id)">X</button>
<button class="btn-red w-11 h-11 z-50" @click.stop="() => deleteZone(zone.id)">X</button>
</span>
</div>
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</div>
</div>
</template>
</Modal>
</Teleport>
</template>
<script setup lang="ts">

View File

@ -1,7 +1,7 @@
<template>
<Modal :is-modal-open="zoneEditorStore.isSettingsModalShown" @modal:close="() => zoneEditorStore.toggleSettingsModal()" :modal-width="300" :modal-height="350" :is-resizable="false">
<Modal :is-modal-open="zoneEditorStore.isSettingsModalShown" @modal:close="() => zoneEditorStore.toggleSettingsModal()" :modal-width="600" :modal-height="350">
<template #modalHeader>
<h3 class="m-0 font-medium shrink-0">Zone settings</h3>
<h3 class="m-0 font-medium shrink-0 text-white">Zone settings</h3>
</template>
<template #modalBody>
@ -10,19 +10,19 @@
<div class="gap-2.5 flex flex-wrap">
<div class="form-field-full">
<label for="name">Name</label>
<input class="input-cyan" v-model="name" name="name" id="name" />
<input class="input-field" v-model="name" name="name" id="name" />
</div>
<div class="form-field-half">
<label for="name">Width</label>
<input class="input-cyan" v-model="width" name="name" id="name" type="number" />
<input class="input-field" v-model="width" name="name" id="name" type="number" />
</div>
<div class="form-field-half">
<label for="name">Height</label>
<input class="input-cyan" v-model="height" name="name" id="name" type="number" />
<input class="input-field" v-model="height" name="name" id="name" type="number" />
</div>
<div class="form-field-full">
<label for="pvp">PVP enabled</label>
<select v-model="pvp" class="input-cyan" name="pvp" id="pvp">
<select v-model="pvp" class="input-field" name="pvp" id="pvp">
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>
@ -41,15 +41,15 @@ import { useZoneEditorStore } from '@/stores/zoneEditorStore'
const zoneEditorStore = useZoneEditorStore()
zoneEditorStore.setZoneName(zoneEditorStore.zone.name)
zoneEditorStore.setZoneWidth(zoneEditorStore.zone.width)
zoneEditorStore.setZoneHeight(zoneEditorStore.zone.height)
zoneEditorStore.setZonePvp(zoneEditorStore.zone.pvp)
zoneEditorStore.setZoneName(zoneEditorStore.zone?.name)
zoneEditorStore.setZoneWidth(zoneEditorStore.zone?.width)
zoneEditorStore.setZoneHeight(zoneEditorStore.zone?.height)
zoneEditorStore.setZonePvp(zoneEditorStore.zone?.pvp)
const name = ref(zoneEditorStore.zoneSettings.name)
const width = ref(zoneEditorStore.zoneSettings.width)
const height = ref(zoneEditorStore.zoneSettings.height)
const pvp = ref(zoneEditorStore.zoneSettings.pvp)
const name = ref(zoneEditorStore.zoneSettings?.name)
const width = ref(zoneEditorStore.zoneSettings?.width)
const height = ref(zoneEditorStore.zoneSettings?.height)
const pvp = ref(zoneEditorStore.zoneSettings?.pvp)
watch(name, (value) => {
zoneEditorStore.setZoneName(value)

View File

@ -1,14 +1,15 @@
<template>
<div class="w-full md:min-w-[350px] max-w-[750px] flex flex-col">
<div ref="chatWindow" class="w-full overflow-auto h-32 mb-5 bg-gray-300/80 rounded-lg border-2 border-solid border-cyan-200" v-show="gameStore.isChatOpen">
<div class="w-full md:min-w-[350px] max-w-[750px] flex flex-col absolute left-1/2 -translate-x-1/2 bottom-5">
<div ref="chatWindow" class="w-full overflow-auto h-32 mb-5 bg-gray rounded-md border-2 border-solid border-gray-500 text-gray-300" v-show="gameStore.uiSettings.isChatOpen">
<div v-for="message in chats" class="flex-col py-2 items-center p-3">
<span class="text-ellipsis overflow-hidden whitespace-nowrap text-sm">{{ message.character.name }}</span>
<span class="text-ellipsis overflow-hidden whitespace-nowrap text-sm" :class="{ 'text-cyan-300': gameStore.character?.role == 'gm' }">{{ message.character.name }}</span>
<p class="text-gray-50 m-0">{{ message.message }}</p>
</div>
</div>
<div class="w-full flex">
<div class="w-96 mx-auto relative">
<img src="/assets/icons/chat-icon.svg" class="absolute top-1/2 -translate-y-1/2 left-2.5 h-4 w-4 opacity-50" />
<input
class="w-full h-12 rounded-lg text-lg px-4 py-0 bg-gray-300/80 border-2 border-solid border-cyan-200 text-gray-50 bg-[url('/assets/icons/submit-icon.svg')] bg-no-repeat bg-[right_25px_center] bg-[length:30px] focus:outline-none focus:ring-0 focus:border-cyan-800"
class="w-[332px] h-8 rounded-sm text-xs font-default pl-8 pr-4 py-0 bg-gray-600 border-2 border-solid border-gray-500 text-gray-300 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover focus:outline-none focus:ring-0 focus:border-cyan-800"
placeholder="Type something..."
v-model="message"
@keypress="handleKeyPress"
@ -59,7 +60,7 @@ const scrollToBottom = () => {
})
}
gameStore.connection?.on('chat:message', (data: ChatMessage) => {
gameStore.connection?.on('chat:message', (data: ChatMessage) => {
chats.value.push(data)
scrollToBottom()
@ -74,24 +75,26 @@ const scrollToBottom = () => {
function calculateTextWidth(text: string, font: string, fontSize: number): number {
// Create a canvas element
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
if (!context) {
throw new Error('Unable to create canvas context');
throw new Error('Unable to create canvas context')
}
// Set the font
context.font = `${fontSize}px ${font}`;
context.font = `${fontSize}px ${font}`
// Measure the text width
const metrics = context.measureText(text);
const metrics = context.measureText(text)
return metrics.width;
return metrics.width
}
chatBubble.width = calculateTextWidth(data.message, 'Arial', 13) + 10
chatText.setText(data.message)
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)
@ -111,8 +114,8 @@ const scrollToBottom = () => {
// Store the timer on the container itself
charChatContainer.setData('hideTimer', hideTimer)
})
scrollToBottom()
})
scrollToBottom()
onBeforeUnmount(() => {
gameStore.connection?.off('chat:message')

View File

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

View File

@ -1,46 +1,16 @@
<template>
<div class="hud-wrapper relative left-0 w-[310px] h-[84px]">
<div class="absolute w-14 h-14 bg-white/80 rounded-full border-3 border-solid border-white top-1/2 -translate-y-1/2 left-0 z-20">
<img class="w-7 absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" draggable="false" src="/assets/avatar/default/head.png" />
<div class="absolute left-[66px] top-4 bg-[url('/assets/ui-rect-border-4-corners.svg')] bg-no-repeat px-4 py-2 w-[181px] h-[26px] flex flex-col justify-between">
<div class="w-full flex items-center gap-2">
<label class="text-xs leading-3 text-pixel" for="hp">HP</label>
<progress class="h-2 rounded-sm w-full max-w-44 appearance-none accent-green" id="hp" :value="gameStore.character?.hitpoints" max="100">{{ gameStore.character?.hitpoints }}%</progress>
<span class="text-xs leading-3 text-pixel">{{ gameStore.character?.hitpoints }}%</span>
</div>
<div class="hud-bg absolute top-0 left-8 w-[280px] h-[84px] z-10 bg-[url('/assets/bg-hud-2.png')] bg-top bg-[length:cover] bg-no-repeat mask-[url('/assets/shapes/hud-image-shape.svg')] mask-center mask-[length:cover] mask-no-repeat"></div>
<div class="absolute top-0 left-8 w-[280px] h-[84px] z-10 bg-[url('/assets/shapes/hud-shape-empty.svg')] bg-center bg-[length:cover] bg-no-repeat">
<div class="h-16 flex flex-col items-end py-2.5 pl-12 pr-5">
<div class="w-full flex items-center justify-between mb-1.5">
<span class="text-ellipsis overflow-hidden whitespace-nowrap max-w-32 text-sm">{{ gameStore.character.name }}</span>
<span class="text-sm">lvl. {{ gameStore.character.level }}</span>
</div>
<div class="w-full flex items-center justify-between">
<label class="text-sm" for="hp">HP</label>
<progress class="h-2 rounded-lg w-full max-w-44 appearance-none accent-red" id="hp" :value="gameStore.character.hitpoints" max="100">{{ gameStore.character.hitpoints }}%</progress>
</div>
<div class="w-full flex items-center justify-between">
<label class="text-sm" for="mp">MP</label>
<progress class="h-2 rounded-lg w-full max-w-44 appearance-none accent-blue" id="mp" :value="gameStore.character.mana" max="100">{{ gameStore.character.mana }}%</progress>
<div class="w-full flex items-center gap-2">
<label class="text-xs leading-3 text-pixel" for="sp">SP</label>
<progress class="h-2 rounded-sm w-full max-w-44 appearance-none accent-blue" id="sp" :value="gameStore.character?.mana" max="100">{{ gameStore.character?.mana }}%</progress>
<span class="text-xs leading-3 text-pixel">{{ gameStore.character?.mana }}%</span>
</div>
</div>
</div>
</div>
<!-- TODO: Replace gameStore.character with other (selected) player's -->
<!-- <div class="hud-wrapper other-player relative right-0 w-[310px] h-[84px]">-->
<!-- <div class="absolute w-14 h-14 bg-white/80 rounded-full border-3 border-solid border-white top-1/2 -translate-y-1/2 right-0 z-20">-->
<!-- <img class="w-7 absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 -scale-x-100" draggable="false" src="/assets/avatar/default/head.png" />-->
<!-- </div>-->
<!-- <div class="hud-bg absolute top-0 right-8 w-[280px] h-[84px] z-10 bg-[url('/assets/bg-hud-2.png')] bg-center bg-[length:cover] bg-no-repeat mask-[url('/assets/shapes/hud-image-shape.svg')] mask-center mask-[length:cover] mask-no-repeat"></div>-->
<!-- <div class="absolute top-0 right-8 w-[280px] h-[84px] z-10 -scale-x-100 bg-[url('/assets/shapes/hud-shape-empty.svg')] bg-center bg-[length:cover] bg-no-repeat">-->
<!-- <div class="h-16 flex flex-col items-end -scale-x-100 py-2.5 pr-12 pl-5">-->
<!-- <div class="w-full flex items-center justify-between mb-1.5">-->
<!-- <span class="text-ellipsis overflow-hidden whitespace-nowrap max-w-32 text-sm">{{ gameStore.character.name }}</span>-->
<!-- <span class="text-sm">lvl. {{ gameStore.character.level }}</span>-->
<!-- </div>-->
<!-- <div class="w-full flex items-center justify-between">-->
<!-- <label class="text-sm" for="hp">HP</label>-->
<!-- <progress class="h-2 rounded-lg w-full max-w-44 appearance-none accent-red" id="hp" :value="gameStore.character.hitpoints" max="100">{{ gameStore.character.hitpoints }}%</progress>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
</template>
<script setup lang="ts">
@ -50,41 +20,30 @@ const gameStore = useGameStore()
</script>
<style scoped lang="scss">
.hud-wrapper {
.hud-bg {
mask: url('/assets/shapes/hud-image-shape.svg') center/cover no-repeat;
}
#hp {
#hp {
// Chrome, Safari, Edge, Opera
&::-webkit-progress-value {
@apply bg-red rounded-lg;
@apply bg-gradient-to-r from-green from-75% to-green-200 rounded-sm;
}
&::-webkit-progress-bar {
@apply bg-white rounded-lg border-2 border-solid border-white;
@apply bg-white rounded-sm border border-solid border-black;
}
// Firefox
&::-moz-progress-bar {
@apply bg-red rounded-lg border-2 border-solid border-white;
@apply bg-gradient-to-r from-green from-75% to-green-200 rounded-sm border border-solid border-black;
}
}
#mp {
}
#sp {
// Chrome, Safari, Edge, Opera
&::-webkit-progress-value {
@apply bg-blue rounded-lg;
@apply bg-gradient-to-r from-blue from-75% to-blue-200 rounded-sm;
}
&::-webkit-progress-bar {
@apply bg-white rounded-lg border-2 border-solid border-white;
@apply bg-white rounded-sm border border-solid border-black;
}
// Firefox
&::-moz-progress-bar {
@apply bg-blue rounded-lg border-2 border-solid border-white;
}
}
&.other-player {
.hud-bg {
mask: url('/assets/shapes/hud-image-shape-flipped.svg') center/cover no-repeat;
}
@apply bg-gradient-to-r from-blue from-75% to-blue-200 rounded-sm border border-solid border-black;
}
}
</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>
<ul class="list-none flex gap-2.5 items-center m-0 max-md:pl-0">
<li class="menu-item group relative" @click="gameStore.toggleChat">
<div class="group-hover:block absolute bottom-16 left-1/2 -translate-x-1/2 w-20 h-6 text-center bg-gray-300 border-2 border-solid border-cyan rounded-3xl hidden">
<p class="absolute w-full bottom-0 m-0 text-xs leading-6">Chat</p>
<div class="group-hover:block absolute -bottom-2.5 bg-cyan h-2 w-3.5 [clip-path:polygon(100%_0,_0_0,_50%_100%)] left-1/2 -translate-x-1/2 hidden"></div>
</div>
<a class="group-hover:bg-gray/70 group-hover:cursor-pointer p-1.5 bg-gray-300/70 border-2 border-solid border-cyan-200 hover:border-cyan rounded-lg block w-11 h-9">
<img class="group-hover:drop-shadow-default w-11 h-9 object-contain" draggable="false" src="/assets/icons/chat.png" />
</a>
</li>
<ul class="list-none flex flex-col gap-2.5 items-center m-0 pl-0 absolute left-4 top-4">
<li class="menu-item group relative">
<div class="group-hover:block absolute bottom-16 left-1/2 -translate-x-1/2 w-20 h-6 text-center bg-gray-300 border-2 border-solid border-cyan rounded-3xl hidden">
<p class="absolute w-full bottom-0 m-0 text-xs leading-6">World</p>
<div class="group-hover:block absolute -bottom-2.5 bg-cyan h-2 w-3.5 [clip-path:polygon(100%_0,_0_0,_50%_100%)] left-1/2 -translate-x-1/2 hidden"></div>
<div class="group-hover:block absolute top-1/2 left-14 -translate-y-1/2 w-20 h-6 text-center bg-gray-800 border-2 border-solid border-gray-500 rounded-3xl hidden">
<p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">Open menu</p>
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
</div>
<a class="group-hover:bg-gray/70 group-hover:cursor-pointer p-1.5 bg-gray-300/70 border-2 border-solid border-cyan-200 hover:border-cyan rounded-lg block w-11 h-9">
<img class="group-hover:drop-shadow-default w-11 h-9 object-contain" draggable="false" src="/assets/icons/world.png" />
</a>
</li>
<li class="menu-item group relative">
<div class="group-hover:block absolute bottom-16 left-1/2 -translate-x-1/2 w-20 h-6 text-center bg-gray-300 border-2 border-solid border-cyan rounded-3xl hidden">
<p class="absolute w-full bottom-0 m-0 text-xs leading-6">Users</p>
<div class="group-hover:block absolute -bottom-2.5 bg-cyan h-2 w-3.5 [clip-path:polygon(100%_0,_0_0,_50%_100%)] left-1/2 -translate-x-1/2 hidden"></div>
</div>
<a class="group-hover:bg-gray/70 group-hover:cursor-pointer p-1.5 bg-gray-300/70 border-2 border-solid border-cyan-200 hover:border-cyan rounded-lg block w-11 h-9">
<img class="group-hover:drop-shadow-default w-11 h-9 object-contain" draggable="false" src="/assets/icons/users.png" />
<a class="group-hover:cursor-pointer bg-[url('/assets/ui-border-4-corners.svg')] bg-no-repeat block w-[42px] h-[42px] relative">
<img class="group-hover:drop-shadow-default w-6 h-5 mx-[9px] my-[11px] object-contain" draggable="false" src="/assets/icons/menu-icon.svg" />
</a>
</li>
<li class="menu-item group relative" @click="gameStore.toggleUserPanel">
<div class="group-hover:block absolute bottom-16 left-1/2 -translate-x-1/2 w-20 h-6 text-center bg-gray-300 border-2 border-solid border-cyan rounded-3xl hidden">
<p class="absolute w-full bottom-0 m-0 text-xs leading-6">Inventory</p>
<div class="group-hover:block absolute -bottom-2.5 bg-cyan h-2 w-3.5 [clip-path:polygon(100%_0,_0_0,_50%_100%)] left-1/2 -translate-x-1/2 hidden"></div>
<div class="group-hover:block absolute top-1/2 left-14 -translate-y-1/2 w-20 h-6 text-center bg-gray-800 border-2 border-solid border-gray-500 rounded-3xl hidden">
<p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">User Profile</p>
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
</div>
<a class="group-hover:bg-gray/70 group-hover:cursor-pointer p-1.5 bg-gray-300/70 border-2 border-solid border-cyan-200 hover:border-cyan rounded-lg block w-11 h-9">
<img class="group-hover:drop-shadow-default w-11 h-9 object-contain" draggable="false" src="/assets/icons/treasure-chest.png" />
<a class="group-hover:cursor-pointer bg-[url('/assets/ui-border-4-corners.svg')] bg-no-repeat block w-[42px] h-[42px] relative">
<img class="group-hover:drop-shadow-default w-8 h-8 m-[5px] object-contain" draggable="false" src="/assets/avatar/default/head.png" />
<p class="absolute bottom-0 -right-1.5 m-0 max-w-4 font-ui z-10 text-white text-[12px] leading-[6px] drop-shadow-pixel"><span class="font-ui text-white text-[8px] ml-0.5">LVL</span> {{ characterLevel }}</p>
</a>
</li>
<li class="menu-item group relative" @click="gameStore.toggleChat">
<div class="group-hover:block absolute top-1/2 left-14 -translate-y-1/2 w-20 h-6 text-center bg-gray-800 border-2 border-solid border-gray-500 rounded-3xl hidden">
<p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">Open Chat</p>
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
</div>
<a class="group-hover:bg-gray-800 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>
</li>
</ul>
@ -43,4 +53,5 @@
import { useGameStore } from '@/stores/gameStore'
const gameStore = useGameStore()
let characterLevel = gameStore.character?.level.toString().padStart(2, '0')
</script>

View File

@ -0,0 +1,23 @@
<template>
<div class="absolute top-4 right-4">
<div class="w-40 h-40 rounded-full border border-solid border-gray-500 bg-[url('/assets/ui-texture.png')] bg-no-repeat">
<div class="w-40 h-40 rounded-full shadow-inner"></div>
</div>
<div class="absolute -bottom-3 left-1/2 -translate-x-1/2 flex gap-1">
<button class="w-6 h-6 relative p-0">
<img class="absolute w-3 h-3 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/plus-icon.svg" />
<img class="w-full h-full" src="/assets/ui-border-4-corners.svg" />
</button>
<button class="w-6 h-6 relative p-0">
<img class="absolute w-3 h-3 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/minus-icon.svg" />
<img class="w-full h-full" src="/assets/ui-border-4-corners.svg" />
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { useGameStore } from '@/stores/gameStore'
const gameStore = useGameStore()
</script>

View File

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

View File

@ -39,7 +39,7 @@
</div>
</div>
</div>
<div class="absolute -left-5 -bottom-5 w-[calc(100%_+_40px)] my-px h-px bg-cyan-200"></div>
<div class="absolute -left-5 -bottom-5 w-[calc(100%_+_40px)] my-px h-px bg-gray-500"></div>
</div>
<div class="m-4">
<h4 class="font-medium text-lg max-w-[375px]">Character stats</h4>

View File

@ -13,54 +13,54 @@
<div class="flex flex-col gap-3 mx-5 mt-2">
<div class="flex gap-3 justify-center">
<!-- Helmet -->
<div class="bg-gray-300/80 border-solid border-2 border-cyan-200 rounded-md aspect-square h-11 w-11 relative hover:bg-gray-200">
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square h-11 w-11 relative hover:bg-gray-200">
<img src="/assets/icons/inventory/helmet.svg" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" />
</div>
<!-- Head charm -->
<div class="bg-gray-300/80 border-solid border-2 border-cyan-200 rounded-md aspect-square h-11 w-11 relative hover:bg-gray-200">
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square h-11 w-11 relative hover:bg-gray-200">
<img src="/assets/icons/inventory/head_charm.svg" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" />
</div>
</div>
<div class="flex gap-3 justify-center">
<!-- Bracers -->
<div class="bg-gray-300/80 border-solid border-2 border-cyan-200 rounded-md w-11 h-[104px] relative hover:bg-gray-200">
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md w-11 h-[104px] relative hover:bg-gray-200">
<img src="/assets/icons/inventory/bracers.svg" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" />
</div>
<!-- Chestplate -->
<div class="bg-gray-300/80 border-solid border-2 border-cyan-200 rounded-md aspect-square w-[104px] h-[104px] relative hover:bg-gray-200">
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square w-[104px] h-[104px] relative hover:bg-gray-200">
<img src="/assets/icons/inventory/chestplate.svg" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-10/12 opacity-20" />
</div>
<!-- Primary Weapon -->
<div class="bg-gray-300/80 border-solid border-2 border-cyan-200 rounded-md w-11 h-[104px] self-stretch justify-self-stretch relative hover:bg-gray-200">
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md w-11 h-[104px] self-stretch justify-self-stretch relative hover:bg-gray-200">
<img src="/assets/icons/inventory/primary_weapon.svg" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" />
</div>
</div>
<div class="flex gap-3 justify-center">
<!-- Legs -->
<div class="bg-gray-300/80 border-solid border-2 border-cyan-200 rounded-md w-11 h-[104px] self-stretch justify-self-stretch relative hover:bg-gray-200">
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md w-11 h-[104px] self-stretch justify-self-stretch relative hover:bg-gray-200">
<img src="/assets/icons/inventory/legs.svg" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" />
</div>
<div class="flex flex-col gap-3">
<!-- Belt/pouch -->
<div class="bg-gray-300/80 border-solid border-2 border-cyan-200 rounded-md aspect-square h-11 w-11 self-stretch justify-self-stretch relative hover:bg-gray-200">
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square h-11 w-11 self-stretch justify-self-stretch relative hover:bg-gray-200">
<img src="/assets/icons/inventory/pouch.svg" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" />
</div>
<!-- Boots -->
<div class="bg-gray-300/80 border-solid border-2 border-cyan-200 rounded-md aspect-square h-11 w-11 self-stretch justify-self-stretch relative hover:bg-gray-200">
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square h-11 w-11 self-stretch justify-self-stretch relative hover:bg-gray-200">
<img src="/assets/icons/inventory/boots.svg" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" />
</div>
</div>
</div>
</div>
</div>
<div class="absolute -left-5 -bottom-5 w-[calc(100%_+_40px)] my-px h-px bg-cyan-200"></div>
<div class="absolute -left-5 -bottom-5 w-[calc(100%_+_40px)] my-px h-px bg-gray-500"></div>
</div>
<div class="m-4">
<h4 class="font-medium text-lg max-w-[375px]">Equipment Bonus</h4>

View File

@ -3,14 +3,14 @@
<div class="m-4 relative">
<h4 class="m-auto font-medium text-lg max-w-[375px]">Inventory</h4>
<div class="flex gap-3 mt-4 mx-auto flex-wrap max-w-[375px]">
<div v-for="n in 24" class="bg-gray-300/80 border-solid border-2 border-cyan-200 w-12 h-12 rounded-md aspect-square shrink-0 justify-self-stretch hover:bg-gray-200"></div>
<div v-for="n in 24" class="bg-gray-300/80 border-solid border-2 border-gray-500 w-12 h-12 rounded-md aspect-square shrink-0 justify-self-stretch hover:bg-gray-200"></div>
</div>
<div class="absolute -left-5 -bottom-5 w-[calc(100%_+_40px)] my-px h-px bg-cyan-200"></div>
<div class="absolute -left-5 -bottom-5 w-[calc(100%_+_40px)] my-px h-px bg-gray-500"></div>
</div>
<div class="m-4">
<h4 class="m-auto font-medium text-lg max-w-[375px]">Chest items</h4>
<div class="flex gap-3 mt-4 mx-auto flex-wrap max-w-[375px]">
<div v-for="n in 12" class="bg-gray-300/80 border-solid border-2 border-cyan-200 w-12 h-12 rounded-md aspect-square shrink-0 justify-self-stretch hover:bg-gray-200"></div>
<div v-for="n in 12" class="bg-gray-300/80 border-solid border-2 border-gray-500 w-12 h-12 rounded-md aspect-square shrink-0 justify-self-stretch hover:bg-gray-200"></div>
</div>
</div>
</div>

View File

@ -4,26 +4,26 @@
<!-- Settings Categories -->
<div class="relative p-2.5">
<h3>Settings</h3>
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</div>
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'character' }" @click.stop="settingCategory = 'character'">
<span>Character</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'account' }" @click.stop="settingCategory = 'account'">
<span>Account</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'audio' }" @click.stop="settingCategory = 'audio'">
<span>Audio</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'video' }" @click.stop="settingCategory = 'video'">
<span>Video</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
</div>
<div class="absolute w-px bg-cyan-200 h-full top-0 left-1/6"></div>
<div class="absolute w-px bg-gray-500 h-full top-0 left-1/6"></div>
<!-- Assets list -->
<div class="overflow-auto h-full w-10/12 flex flex-col relative">

View File

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

View File

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

View File

@ -38,24 +38,20 @@ const modalOpened = ref(props.modalOpened)
</script>
<template>
<Modal :closable="false" :is-resizable="false" :isModalOpen="true" @modal:close="() => (modalOpened = !modalOpened)" :modal-width="300" :modal-height="190">
<Modal :closable="false" :is-resizable="false" :isModalOpen="true" @modal:close="() => (modalOpened = !modalOpened)" :modal-width="350" :modal-height="230">
<template #modalHeader>
<div class="text-white">
<slot name="modalHeader"></slot>
</div>
</template>
<template #modalBody>
<div class="text-white h-full">
<div class="flex h-full flex-col justify-between">
<span class="p-2">
<div class="h-[calc(100%_-_32px)] p-4">
<div class="h-full flex flex-col justify-between">
<slot name="modalBody"></slot>
</span>
<div class="flex justify-between p-2">
<button class="btn-cyan py-1.5 px-4 min-w-24 inline-block" @click="props.cancelFunction()">
<div class="grid grid-flow-col justify-stretch gap-4">
<button class="btn-empty py-1.5 px-4 min-w-24 inline-block" @click="props.cancelFunction()">
{{ props.cancelButtonText }}
</button>
<button class="btn-cyan py-1.5 px-4 min-w-24 inline-block" type="submit" @click="props.confirmFunction()">
<button class="btn-red py-1.5 px-4 min-w-24 inline-block" type="submit" @click="props.confirmFunction()">
{{ props.confirmButtonText }}
</button>
</div>

View File

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

View File

@ -1,23 +1,26 @@
<template>
<Teleport to="body">
<div v-if="isModalOpenRef" class="fixed bg-gray-300/80 border-solid border-2 border-cyan-200 z-50 flex flex-col backdrop-blur-sm shadow-lg" :style="modalStyle">
<div @mousedown="startDrag" class="cursor-move p-2.5 flex justify-between items-center border-solid border-0 border-b border-cyan-200">
<div v-if="isModalOpenRef" class="fixed border-solid border-2 border-gray-500 z-50 flex flex-col backdrop-blur-sm shadow-lg" :style="modalStyle">
<div @mousedown="startDrag" class="cursor-move p-2.5 flex justify-between items-center border-solid border-0 border-b border-gray-500 relative">
<div class="rounded-t-md absolute w-full h-full top-0 left-0 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-center bg-cover opacity-90"></div>
<div class="relative z-10">
<slot name="modalHeader" />
</div>
<div class="flex gap-2.5">
<button @click="toggleFullScreen" class="w-5 h-5 m-0 p-0 relative hover:scale-110 transition-transform duration-300 ease-in-out" v-if="canFullScreen">
<img :alt="isFullScreen ? 'exit full-screen' : 'full-screen'" draggable="false" :src="isFullScreen ? '/assets/icons/minimize.svg' : '/assets/icons/full-screen.svg'" class="w-full h-full invert" />
<img :alt="isFullScreen ? 'exit full-screen' : 'full-screen'" draggable="false" :src="isFullScreen ? '/assets/icons/minimize.svg' : '/assets/icons/full-screen.svg'" class="w-3.5 h-3.5 invert" />
</button>
<button @click="close" v-if="closable" class="w-5 h-5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
<button @click="close" v-if="closable" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
<img alt="close" draggable="false" src="/assets/icons/close-button-white.svg" class="w-full h-full" />
</button>
</div>
</div>
<div class="overflow-hidden grow">
<div class="overflow-hidden grow relative">
<div class="rounded-b-md absolute w-full h-full top-0 left-0 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center opacity-90"></div>
<div class="relative z-10 h-full">
<slot name="modalBody" />
<img v-if="isResizable && !isFullScreen" src="/assets/icons/resize-icon.svg" alt="resize" class="absolute bottom-0 right-0 w-5 h-5 cursor-nwse-resize invert-[60%]" @mousedown="startResize" />
</div>
<div v-if="$slots.modalFooter" class="px-5 min-h-12 flex justify-end gap-7.5 items-center border-solid border-t border-cyan-200">
<slot name="modalFooter" />
<img v-if="isResizable && !isFullScreen" src="/assets/icons/resize-icon.svg" alt="resize" class="absolute z-10 bottom-0 right-0 w-5 h-5 cursor-nwse-resize" @mousedown="startResize" />
</div>
</div>
</Teleport>
@ -84,7 +87,7 @@ let startHeight = 0
let preFullScreenState = { x: 0, y: 0, width: 0, height: 0 }
const modalStyle = computed(() => ({
borderRadius: isFullScreen.value ? '0' : '10px',
borderRadius: isFullScreen.value ? '0' : '6px',
top: isFullScreen.value ? '0' : `${y.value}px`,
left: isFullScreen.value ? '0' : `${x.value}px`,
width: isFullScreen.value ? '100vw' : `${width.value}px`,

View File

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

View File

@ -1,6 +1,5 @@
<template>
<Image v-for="object in zoneStore.zone?.zoneObjects" :depth="calculateIsometricDepth(object.positionX, object.positionY, object.object.frameWidth, object.object.frameHeight)" :key="object.id" v-bind="getObjectImageProps(object)" />
<!-- <Text v-for="object in zoneStore.zone?.zoneObjects" :key="object.id" :depth="99999" :text="Math.ceil(calculateIsometricDepth(object.positionX, object.positionY, object.object.frameWidth, object.object.frameHeight))" v-bind="getObjectProps(object)" />-->
<Image v-for="object in zoneStore.zone?.zoneObjects" v-bind="getObjectImageProps(object)" />
</template>
<script setup lang="ts">
@ -15,19 +14,12 @@ const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
}>()
const getObjectProps = (object: ZoneObject) => {
return {
x: tileToWorldX(props.tilemap as any, object.positionX, object.positionY),
y: tileToWorldY(props.tilemap as any, object.positionX, object.positionY),
originY: Number(object.object.originX),
originX: Number(object.object.originY)
}
}
const getObjectImageProps = (object: ZoneObject) => {
return {
depth: calculateIsometricDepth(object.positionX, object.positionY, object.object.frameWidth, object.object.frameHeight),
x: tileToWorldX(props.tilemap as any, object.positionX, object.positionY),
y: tileToWorldY(props.tilemap as any, object.positionX, object.positionY),
flipX: object.isRotated,
texture: object.object.id,
originY: Number(object.object.originX),
originX: Number(object.object.originY)

View File

@ -38,29 +38,29 @@ function createTileLayer() {
const uniqueTiles = new Set(tilesFromZone.flat().filter(Boolean))
const tilesetImages = Array.from(uniqueTiles).map((tile, index) => {
return zoneTilemap.addTilesetImage(tile, tile, config.tile_size.x, config.tile_size.y, 0, 0, index + 1, { x: 0, y: -config.tile_size.y })
return zoneTilemap.addTilesetImage(tile, tile, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y })
}) as any
// Add blank tile
tilesetImages.push(zoneTilemap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 0, 0, 0, { x: 0, y: -config.tile_size.y }))
tilesetImages.push(zoneTilemap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 1, 2, 0, { x: 0, y: -config.tile_size.y }))
const layer = zoneTilemap.createBlankLayer('tiles', tilesetImages, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
layer.setDepth(0)
layer.setCullPadding(2, 2)
return layer
}
function createTileArray() {
return Array.from({ length: zoneStore.zone?.width ?? 0 }, () => Array.from({ length: zoneStore.zone?.height ?? 0 }, () => 'blank_tile'))
return Array.from({ length: zoneTilemap.height || 0 }, () => Array.from({ length: zoneTilemap.width || 0 }, () => 'blank_tile'))
}
onBeforeMount(() => {
if (zoneStore.zone?.tiles) {
if (!zoneStore.zone?.tiles) {
return
}
setAllTiles(zoneTilemap, tiles, zoneStore.zone.tiles)
tileArray = zoneStore.zone.tiles.map((row) => row.map((tileId) => tileId || 'blank_tile'))
} else {
tileArray.forEach((row, y) => row.forEach((_, x) => placeTile(zoneTilemap, tiles, x, y, 'blank_tile')))
}
})
onBeforeUnmount(() => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,15 +2,16 @@ import config from '@/config'
import Tilemap = Phaser.Tilemaps.Tilemap
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
import Tileset = Phaser.Tilemaps.Tileset
import Tile = Phaser.Tilemaps.Tile
import { useGameStore } from '@/stores/gameStore'
export function getTile(x: number, y: number, layer: Phaser.Tilemaps.TilemapLayer): Phaser.Tilemaps.Tile | undefined {
const tile: Phaser.Tilemaps.Tile = layer.getTileAtWorldXY(x, y)
export function getTile(layer: TilemapLayer | Tilemap, x: number, y: number): Tile | undefined {
const tile = layer.getTileAtWorldXY(x, y)
if (!tile) return undefined
return tile
}
export function tileToWorldXY(layer: Phaser.Tilemaps.TilemapLayer, pos_x: number, pos_y: number) {
export function tileToWorldXY(layer: TilemapLayer, pos_x: number, pos_y: number) {
const worldPoint = layer.tileToWorldXY(pos_x, pos_y)
const positionX = worldPoint.x + config.tile_size.y
const positionY = worldPoint.y
@ -18,7 +19,7 @@ export function tileToWorldXY(layer: Phaser.Tilemaps.TilemapLayer, pos_x: number
return { positionX, positionY }
}
export function tileToWorldX(layer: Phaser.Tilemaps.TilemapLayer, pos_x: number, pos_y: number): number {
export function tileToWorldX(layer: TilemapLayer, pos_x: number, pos_y: number): number {
const worldPoint = layer.tileToWorldXY(pos_x, pos_y)
return worldPoint.x + config.tile_size.x / 2
}
@ -35,8 +36,8 @@ export function placeTile(zone: Tilemap, layer: TilemapLayer, x: number, y: numb
}
export function setAllTiles(zone: Tilemap, layer: TilemapLayer, tiles: string[][]) {
tiles.forEach((row, y) => {
row.forEach((tile, x) => {
tiles.forEach((row: string[], y: number) => {
row.forEach((tile: string, x: number) => {
placeTile(zone, layer, x, y, tile)
})
})
@ -58,6 +59,8 @@ export const sortByIsometricDepth = <T extends { positionX: number; positionY: n
})
}
export const clearAssets = (scene: Phaser.Scene) => {}
export const loadAssets = (scene: Phaser.Scene): Promise<void> => {
return new Promise((resolve) => {
const gameStore = useGameStore()

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More