Compare commits

..

167 Commits

Author SHA1 Message Date
4042808d4e Socket event enum enhancement 2025-02-17 01:20:16 +01:00
a6d6d894a9 #363 : Moved socket logic into socketManager and removed it from Pinia store 2025-02-17 01:17:02 +01:00
0c61fe77de Paint tool fixes 2025-02-16 21:36:31 +01:00
bfb2bcb939 Map editor teleport enhancements 2025-02-16 21:18:06 +01:00
af5a97f66d Removed debug console log 2025-02-16 19:04:35 +01:00
79fa54b1bb Cache improvement 2025-02-16 19:03:58 +01:00
dbb4cae154 Only update value if is self 2025-02-16 18:21:53 +01:00
9a8220e4e0 Teleport walk fix 2025-02-16 18:16:35 +01:00
bc0db8b32b Receive new location as array instead of object 2025-02-16 17:29:21 +01:00
ad611ef593 Send new location as array instead of object 2025-02-16 17:19:19 +01:00
d819a84a37 npm update 2025-02-16 17:15:28 +01:00
15dc331a43 Improved movement logic 2025-02-16 17:14:24 +01:00
920baaebde Updated interval value 2025-02-16 01:55:04 +01:00
b569888682 Set selectedPlacedObject null when exiting map editor 2025-02-15 20:46:00 +01:00
94eab073e6 Max 2. pivot points 2025-02-15 17:57:30 +01:00
d843b954ab TS 2025-02-15 17:42:01 +01:00
337446497b Simple 2025-02-15 16:51:58 +01:00
d8805dd775 Added pivot point logic 2025-02-15 16:40:17 +01:00
4c040c21d6 npm update 2025-02-15 15:37:05 +01:00
d0af83ec60 N/A 2025-02-14 23:15:35 +01:00
2de34d2034 Package updates 2025-02-14 22:56:33 +01:00
132121c082 ToggleGmPanel > set false 2025-02-14 16:22:21 +01:00
201f628bfa Saving teleports works again 2025-02-14 03:16:22 +01:00
af99d66595 Add if check to quick login 2025-02-14 03:06:49 +01:00
56f30093f6 Teleport fix WIP 2025-02-14 03:04:42 +01:00
8f26a40a0e 11 for fast login 2025-02-14 03:03:57 +01:00
110fd4e608 TS improvements 2025-02-14 02:49:14 +01:00
c1edf31ca0 TS fix 2025-02-14 02:44:14 +01:00
90c0ed3141 Open teleport modal fix 2025-02-14 02:42:01 +01:00
bcf0d2832d Feedback Shilo
the placement of map objects sometimes placed in a tile that the mouse wasnt over. but i didnt try reproducing the issue.
opening map editor doesnt close the gm window (nor show the map editor). not obvious.
side panels like map objects doesnt have a close button. so i was only able to close it by switching to "move" tool.
2025-02-14 02:34:56 +01:00
8bf67ab168 Minor fix and format 2025-02-14 02:23:13 +01:00
f83e2bf8c8 Merge remote-tracking branch 'origin/main' into feature/map-refactor 2025-02-14 02:08:23 +01:00
8b0bf6534e Most comprehensive undo/redo so far 2025-02-13 13:27:15 -06:00
5e243e5201 Fixed object moving when its not supposed to 2025-02-13 12:19:09 -06:00
c82db9813e Remove redundant import 2025-02-13 11:34:32 -06:00
579749f4e0 fuck depth sorting man 2025-02-13 17:08:47 +01:00
ddc26a021b Depth sort fix attempt 2025-02-13 14:50:46 +01:00
2d6b1ff1e0 Merge branch 'feature/map-refactor' of ssh://gitea.directonline.io:29417/noxious/client into feature/map-refactor
# Conflicts:
#	src/components/gameMaster/mapEditor/Map.vue
#	src/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.vue
#	src/components/gameMaster/mapEditor/partials/Toolbar.vue
#	src/components/screens/MapEditor.vue
2025-02-12 18:38:14 -06:00
16720777c9 Rm unused func 2025-02-12 21:32:34 +01:00
41e7832cbe Minor improvements 2025-02-12 21:11:17 +01:00
e6412d8a65 Temp. fix for finding children in scene, character create bug fix, chat logic improvements, added image compression upon build 2025-02-12 13:44:37 +01:00
faa8e5def9 Remove history commit that was used to pad the start 2025-02-11 21:14:03 -06:00
beed1d6903 Chat fix 2025-02-12 03:19:56 +01:00
2ebcc24390 npm update 2025-02-12 03:17:01 +01:00
2e3ff803f6 Improvement 2025-02-11 23:17:06 +01:00
dd1cc795de Replaced all event names with numbers for less bandwidth usage 2025-02-11 23:13:15 +01:00
59243e0e17 Merge cleanup 2025-02-10 16:21:23 -06:00
87ffc98cce Best undo/redo function across all map editor elements 2025-02-10 16:08:56 -06:00
0c450b24ed Teleport modal restored, and expanded undo/redo to include all placed and erase edits across each map element type (map object advanced actions WIP) 2025-02-10 16:08:15 -06:00
9459639497 Best undo/redo function across all map editor elements 2025-02-10 16:02:38 -06:00
5f2c7a09b1 Slightly improved logic 2025-02-10 18:32:35 +01:00
13e8c1b4dd Show sprite duration 2025-02-10 16:10:36 +01:00
b27a2e8779 Updated move interval 2025-02-10 15:33:09 +01:00
b3c9e3ca3d 75 2025-02-10 15:30:08 +01:00
31a91c3f9f Prod. env improvements 2025-02-10 15:26:47 +01:00
5d4de60f90 Broken import fix 2025-02-10 15:12:34 +01:00
4070bcf048 Removed BackgroundImageLoader component 2025-02-10 15:11:36 +01:00
04203cb9c1 Add opacity to placed map object preview, don't show seconds in clock , updated date format 2025-02-10 14:37:34 +01:00
592d1df9bf Removed conflicting and redundant logic 2025-02-10 13:39:17 +01:00
9413fdbb2f Improved check 2025-02-10 02:12:24 +01:00
34caac562c Minor fixes 2025-02-10 02:05:16 +01:00
52dafb8643 Added character profile images to preload 2025-02-10 01:53:21 +01:00
390187f353 #202 - Enable / disable placed map object preview 2025-02-10 01:51:45 +01:00
cbd111a05b Map eidtor work , new walk sound 2025-02-10 01:46:31 +01:00
5ef11f3157 Add ID instead of object when adding map object 2025-02-09 22:40:06 +01:00
c56c2796c4 format 2025-02-09 22:36:55 +01:00
c228af7bb6 Moved if logic into component for improved performance, inline value updating for select fields 2025-02-09 22:29:24 +01:00
f45a51c230 Clean 2025-02-09 22:09:42 +01:00
790a62c600 Merge remote-tracking branch 'origin/feature/#321' 2025-02-09 21:54:36 +01:00
82a854e647 Replaced walk sound, removed redundant logic, removed emits in favour of using mapEditor directly (less logic), added soundStorage clear to reset cmd 2025-02-09 21:54:21 +01:00
3bcb16fa9c Merge branch 'feature/#321' of ssh://gitea.directonline.io:29417/noxious/client into feature/#321
# Conflicts:
#	src/components/gameMaster/mapEditor/partials/Toolbar.vue
2025-02-09 21:27:17 +01:00
f79ebedc62 Improvement 2025-02-09 21:27:02 +01:00
44b0368276 Adjusted selectedplacedmapobject toolbar, close list when eraser selected 2025-02-09 20:59:03 +01:00
b8b985470f Move SelectedPlacedMapObject toolbar 2025-02-09 20:40:51 +01:00
39e00c6feb Move toolbar over when listpanel is open 2025-02-09 20:03:47 +01:00
6de0bb200d Removed nginx config 2025-02-09 19:58:54 +01:00
2a00e206eb Updated bg img 2025-02-09 18:36:51 +01:00
8f9b19ba8b i hate environment differences 2025-02-09 17:44:26 +01:00
d997a33b86 Added check 2025-02-09 17:43:39 +01:00
9749b02ccf Debug 2025-02-09 17:42:45 +01:00
f83d5eabee Map service fix attempt 2025-02-09 17:41:08 +01:00
a9cedba4e0 Renamed get to getById, map improvement 2025-02-09 17:33:10 +01:00
49dcd92a9e Map bug fix 2025-02-09 17:28:42 +01:00
d010159989 map bug fix 2025-02-09 17:25:48 +01:00
275dd95c69 Cleaned character create event 2025-02-09 17:13:22 +01:00
e3c3d4d420 Removed debug 2025-02-09 16:32:34 +01:00
87e7f14469 format 2025-02-09 15:31:51 +00:00
723aa59142 rm scp 2025-02-09 16:30:43 +01:00
c369719564 Debug 2025-02-09 16:26:30 +01:00
2d8c421ac6 ah 2025-02-09 03:41:10 +01:00
1137c95ff3 Caddyfile 2025-02-09 03:40:24 +01:00
4b56da0fa0 Caddyfile 2025-02-09 03:39:23 +01:00
c21e78c2ec Removed getDomain func. 2025-02-09 03:34:33 +01:00
fcf96a25ae Added Caddyfile 2025-02-09 03:01:05 +01:00
cf9deebc94 Removed docker files 2025-02-09 01:21:08 +01:00
ca307d4de3 Teleport modal restored, and expanded undo/redo to include all placed and erase edits across each map element type (map object advanced actions WIP) 2025-02-08 15:07:21 -06:00
4c4e8ffe02 Better formatting 2025-02-07 20:47:24 +01:00
369522fda3 #321 - Show corresponding list when drawMode is selected & vice versa
Synced with main, cleaned up unused consts
2025-02-07 20:37:11 +01:00
dc7e20842a Merge remote-tracking branch 'origin/main' into feature/#321 2025-02-07 20:08:19 +01:00
75c9d5f349 Login fix 2025-02-07 20:07:54 +01:00
b35794d6d3 Merge remote-tracking branch 'origin/main' into feature/#321 2025-02-07 19:59:58 +01:00
6ba4c1b843 Updated Dockerfile, renamed config key 2025-02-07 01:14:57 +01:00
6a52546a08 Field step attr. improvement 2025-02-07 00:28:28 +01:00
8133bd02df Merge remote-tracking branch 'origin/main' into feature/#321 2025-02-07 00:26:57 +01:00
e720a1098e Z-index fix for selectedPlacedMapObject 2025-02-07 00:26:47 +01:00
48d1d920be Merge remote-tracking branch 'origin/main' into feature/#321 2025-02-07 00:23:32 +01:00
7542fd70ed #351 : Added map editor settings modal 2025-02-07 00:23:13 +01:00
9f866fea72 Reapply tilelist processing changes 2025-02-06 23:31:50 +01:00
ec6f3031b8 Rebuilt side panel for object & tile lists
Reorganised file structure
2025-02-06 23:20:30 +01:00
838610d041 Cleaned sound composable code 2025-02-06 22:35:54 +01:00
fb3a59aa59 Cache audio 2025-02-06 22:32:25 +01:00
ccb64fc048 mp3 > wav 2025-02-06 22:01:16 +01:00
db52bcfff3 Global const for composable 2025-02-06 21:46:51 +01:00
12735756d7 Added more SFX logic 2025-02-06 21:43:09 +01:00
6383320e8c #209 : Improved logic for button presses 2025-02-06 21:20:49 +01:00
557b8aaabb Added connect sound 2025-02-06 21:08:55 +01:00
c09e9ea841 Replaced button press sound 2025-02-06 21:05:29 +01:00
c2d41a63a7 Added playSound func, use this on attack 2025-02-06 21:00:32 +01:00
122a178feb Minor change 2025-02-06 14:50:30 +01:00
909dbf4280 Fix for scroll 2025-02-06 14:36:28 +01:00
8add054f63 Added drag & resize logic to tileList and mapObjectList 2025-02-06 14:32:39 +01:00
04d55f994e Reimplemented web worker logic for tile analysis 2025-02-06 14:14:41 +01:00
b83c340385 Iso depth fix for character on spawn 2025-02-06 14:04:43 +01:00
d5984f1c3f npm update 2025-02-06 14:03:33 +01:00
7071d934b4 Update origin X and Y values in real-time 2025-02-06 14:02:50 +01:00
15b212160d Walk improvement 2025-02-06 13:46:21 +01:00
2a2841cf16 Fix 2025-02-06 13:34:12 +01:00
a545018639 Removed unused param 2025-02-06 13:24:32 +01:00
90f3056e08 Minor improvements 2025-02-06 13:22:52 +01:00
7730fd81bd Phaser moment 2025-02-05 22:13:29 +01:00
b195f1399f Updated modal bg styles to correct values, started working on map object setting modal 2025-02-05 21:04:47 +01:00
3c06f7db97 Loop fix, added btn-indigo to general styling 2025-02-05 19:50:53 +01:00
6c7864b4d4 Moved map character network event logic into characters component, added playAnimation function to characterComposable, finished attack animation 2025-02-05 18:27:33 +01:00
0c9a41c286 Image dimension bug fix 2025-02-05 17:42:16 +01:00
dffdd0542f #343: Depth sorting improvement for character component 2025-02-05 17:06:31 +01:00
d2abf8fda8 #327 : Fixed map load bug after closing map editor 2025-02-05 17:01:27 +01:00
fdbc101f96 npm run format 2025-02-05 15:19:13 +01:00
7ff1de4018 Removed client notif. Server sends one already. 2025-02-05 15:12:52 +01:00
f258c65403 Merge remote-tracking branch 'origin/main' into feature/map-refactor 2025-02-05 15:11:14 +01:00
bab13646ed Updated packages 2025-02-05 15:10:30 +01:00
adc3eba237 Fixed event tile erasing and moving/flipping map objects 2025-02-04 21:39:59 -06:00
2097a51f07 Put back Colin's FE changes 2025-02-05 04:33:40 +01:00
50daf01a01 Depth sorting works semi-better this way 2025-02-05 03:36:24 +01:00
14474f7665 Removed map from mapEffects type 2025-02-05 03:04:33 +01:00
f14d9baaa1 Send PVP value as integer 2025-02-05 02:55:33 +01:00
d2b6d8dcb3 Showing placed map object works in both game and map editor, updated types, cleaned some logic 2025-02-05 02:27:18 +01:00
027fdd7dac TS improvements, WIP loading map objects in game map, WIP loading tile textures 2025-02-05 00:47:28 +01:00
2b40741ca7 npm run format, moved some files for improved file structure, removed redundant logic 2025-02-05 00:19:55 +01:00
aee18956f3 Restored tile editing and proper map clearing behavior 2025-02-04 14:09:57 -06:00
cf54ab842a Merge remote-tracking branch 'origin/main' into feature/map-refactor
# Conflicts:
#	src/components/gameMaster/mapEditor/Map.vue
#	src/components/gameMaster/mapEditor/partials/MapObjectList.vue
#	src/components/gameMaster/mapEditor/partials/TileList.vue
#	src/components/screens/MapEditor.vue
#	src/composables/pointerHandlers/useMapEditorPointerHandlers.ts
2025-02-04 15:15:29 +01:00
d25100c810 Depth sorting 2025-02-04 15:10:43 +01:00
cd1daf9345 Somehwat improved default object position 2025-02-04 15:07:25 +01:00
0ecd951710 Redundant code removal and synchronizing map settings modal with the editor 2025-02-03 16:18:20 -06:00
ff9dcb91b0 Map object position as tile coordinates, map objects correctly snap to each tile now when moving, and fixed bug of placing objects over eachother on the same tile 2025-02-03 16:18:20 -06:00
841ec0f3df Map object position as tile coordinates, map objects correctly snap to each tile now when moving, and fixed bug of placing objects over eachother on the same tile 2025-02-03 14:53:46 -06:00
90d7252784 Map object depth calculation adjustment 2025-02-02 14:39:21 -06:00
554497ecbc Map object isometric placement 2025-02-02 13:45:35 -06:00
efeae337ab Restored map editor event tiles 2025-02-02 00:02:19 -06:00
ad47b37279 deleting map objects restored 2025-02-01 23:19:45 -06:00
5e11b67774 Moving map objects restored 2025-02-01 21:36:37 -06:00
7daefb74eb Restored placed map object selection and implemented object snapping to tile coordinates 2025-02-01 18:44:00 -06:00
4adcf8d61d Placement of map objects 2025-02-01 15:56:07 -06:00
fb6e2aa742 Undo and redo cycling through map edit history 2025-01-29 19:48:09 -06:00
e530f69311 Recording a stack of editor tile changes with command pattern 2025-01-28 15:33:47 -06:00
144a513cb6 Switched list modals to right side of screen 2025-01-28 14:12:18 -06:00
2a6321b06b Tile and map object list modals start at top left 2025-01-28 14:09:40 -06:00
ba90982e35 Implemented tap vs hold drawing setting 2025-01-28 13:52:15 -06:00
91 changed files with 3003 additions and 1559 deletions

View File

@ -1,5 +1,6 @@
VITE_NAME=Noxious VITE_NAME=Noxious
VITE_DEVELOPMENT=true VITE_DOMAIN=localhost
VITE_ENVIRONMENT=development
VITE_SERVER_ENDPOINT=http://localhost:4000 VITE_SERVER_ENDPOINT=http://localhost:4000
VITE_TILE_SIZE_WIDTH=64 VITE_TILE_SIZE_WIDTH=64
VITE_TILE_SIZE_HEIGHT=32 VITE_TILE_SIZE_HEIGHT=32

70
Caddyfile Normal file
View File

@ -0,0 +1,70 @@
{
# Global options
admin off # Disable admin API
# Global logging configuration
log {
output file /var/log/caddy/access.log
format json
level INFO
}
}
noxious.gg {
# Root directory for your Vue app
root * ./dist
# Enable compression with optimal settings
encode zstd gzip
# Handle SPA routing
try_files {path} /index.html
# Serve static files with optimizations
file_server
# Enhanced security headers
header {
# Existing headers with improvements
X-Frame-Options "SAMEORIGIN"
X-XSS-Protection "1; mode=block"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
# Additional security headers
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"
# Remove server information
-Server
}
# Improved cache configuration for static assets
@static {
file
path *.js *.css *.png *.jpg *.jpeg *.gif *.ico *.svg *.woff *.woff2 *.ttf *.eot
}
header @static {
Cache-Control "public, max-age=31536000, immutable"
Vary Accept-Encoding
}
# Cache control for HTML files
@html {
file
path *.html
}
header @html {
Cache-Control "no-cache, must-revalidate"
}
# Handle errors
handle_errors {
respond "{http.error.status_code} {http.error.status_text}" {http.error.status_code}
}
}
# Improved redirect configuration
www.noxious.gg {
redir https://noxious.gg{uri} permanent
}

View File

@ -1,32 +0,0 @@
# Build stage
FROM node:22.4.1-alpine as builder
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
FROM nginx:1.26.1-alpine
COPY --from=builder /usr/src/app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@ -1,4 +0,0 @@
{
"schemaVersion": 2,
"dockerfilePath" :"./Dockerfile"
}

View File

@ -1,16 +0,0 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Redirect example
location /discord {
return 301 https://discord.gg/JTev3nzeDa;
}
# Serve static files
location / {
try_files $uri $uri/ /index.html;
}
}

1206
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -16,14 +16,18 @@
"dependencies": { "dependencies": {
"@vueuse/core": "^10.5.0", "@vueuse/core": "^10.5.0",
"@vueuse/integrations": "^10.5.0", "@vueuse/integrations": "^10.5.0",
"axios": "^1.7.7", "axios": "^1.7.9",
"dexie": "^4.0.8", "dexie": "^4.0.11",
"phaser": "^3.86.0", "phaser": "^3.88.2",
"pinia": "^2.1.6", "phavuer": "^0.16.5",
"socket.io-client": "^4.8.0", "phaser3-rex-plugins": "^1.80.13",
"pinia": "^2.3.1",
"sharp": "^0.33.5",
"socket.io-client": "^4.8.1",
"universal-cookie": "^6.1.3", "universal-cookie": "^6.1.3",
"vue": "^3.5.12", "vite-plugin-image-optimizer": "^1.1.8",
"zod": "^3.22.2" "vue": "^3.5.13",
"zod": "^3.24.2"
}, },
"devDependencies": { "devDependencies": {
"@ianvs/prettier-plugin-sort-imports": "^4.4.0", "@ianvs/prettier-plugin-sort-imports": "^4.4.0",
@ -36,8 +40,6 @@
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"jsdom": "^24.1.1", "jsdom": "^24.1.1",
"npm-run-all2": "^6.2.3", "npm-run-all2": "^6.2.3",
"phaser3-rex-plugins": "^1.80.8",
"phavuer": "^0.16.1",
"postcss": "^8.4.47", "postcss": "^8.4.47",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"sass": "^1.79.4", "sass": "^1.79.4",

View File

@ -0,0 +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="M17 9.5L12 14.5L7 9.5" stroke="#fff" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 325 B

View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.3333 17.5L11.0833 12.25C10.6667 12.5833 10.1875 12.8472 9.64583 13.0417C9.10417 13.2361 8.52778 13.3333 7.91667 13.3333C6.40278 13.3333 5.12153 12.809 4.07292 11.7604C3.02431 10.7118 2.5 9.43056 2.5 7.91667C2.5 6.40278 3.02431 5.12153 4.07292 4.07292C5.12153 3.02431 6.40278 2.5 7.91667 2.5C9.43056 2.5 10.7118 3.02431 11.7604 4.07292C12.809 5.12153 13.3333 6.40278 13.3333 7.91667C13.3333 8.52778 13.2361 9.10417 13.0417 9.64583C12.8472 10.1875 12.5833 10.6667 12.25 11.0833L17.5 16.3333L16.3333 17.5ZM7.91667 11.6667C8.95833 11.6667 9.84375 11.3021 10.5729 10.5729C11.3021 9.84375 11.6667 8.95833 11.6667 7.91667C11.6667 6.875 11.3021 5.98958 10.5729 5.26042C9.84375 4.53125 8.95833 4.16667 7.91667 4.16667C6.875 4.16667 5.98958 4.53125 5.26042 5.26042C4.53125 5.98958 4.16667 6.875 4.16667 7.91667C4.16667 8.95833 4.53125 9.84375 5.26042 10.5729C5.98958 11.3021 6.875 11.6667 7.91667 11.6667Z" fill="#808080"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<g>
<g>
<path d="M490.667,405.333h-56.811C424.619,374.592,396.373,352,362.667,352s-61.931,22.592-71.189,53.333H21.333
C9.557,405.333,0,414.891,0,426.667S9.557,448,21.333,448h270.144c9.237,30.741,37.483,53.333,71.189,53.333
s61.931-22.592,71.189-53.333h56.811c11.797,0,21.333-9.557,21.333-21.333S502.464,405.333,490.667,405.333z M362.667,458.667
c-17.643,0-32-14.357-32-32s14.357-32,32-32s32,14.357,32,32S380.309,458.667,362.667,458.667z"/>
</g>
</g>
<g>
<g>
<path d="M490.667,64h-56.811c-9.259-30.741-37.483-53.333-71.189-53.333S300.736,33.259,291.477,64H21.333
C9.557,64,0,73.557,0,85.333s9.557,21.333,21.333,21.333h270.144C300.736,137.408,328.96,160,362.667,160
s61.931-22.592,71.189-53.333h56.811c11.797,0,21.333-9.557,21.333-21.333S502.464,64,490.667,64z M362.667,117.333
c-17.643,0-32-14.357-32-32c0-17.643,14.357-32,32-32s32,14.357,32,32C394.667,102.976,380.309,117.333,362.667,117.333z"/>
</g>
</g>
<g>
<g>
<path d="M490.667,234.667H220.523c-9.259-30.741-37.483-53.333-71.189-53.333s-61.931,22.592-71.189,53.333H21.333
C9.557,234.667,0,244.224,0,256c0,11.776,9.557,21.333,21.333,21.333h56.811c9.259,30.741,37.483,53.333,71.189,53.333
s61.931-22.592,71.189-53.333h270.144c11.797,0,21.333-9.557,21.333-21.333C512,244.224,502.464,234.667,490.667,234.667z
M149.333,288c-17.643,0-32-14.357-32-32s14.357-32,32-32c17.643,0,32,14.357,32,32S166.976,288,149.333,288z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 946 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,8 +1,7 @@
<template> <template>
<Debug /> <Debug />
<Notifications /> <Notifications />
<BackgroundImageLoader /> <GmPanel v-if="gameStore.character?.role === 'gm'" />
<GmPanel v-if="gameStore.character?.role === 'gm'" @open-map-editor="mapEditor.toggleActive" />
<component :is="currentScreen" /> <component :is="currentScreen" />
</template> </template>
@ -13,21 +12,23 @@ import Game from '@/components/screens/Game.vue'
import Loading from '@/components/screens/Loading.vue' import Loading from '@/components/screens/Loading.vue'
import Login from '@/components/screens/Login.vue' import Login from '@/components/screens/Login.vue'
import MapEditor from '@/components/screens/MapEditor.vue' import MapEditor from '@/components/screens/MapEditor.vue'
import BackgroundImageLoader from '@/components/utilities/BackgroundImageLoader.vue'
import Debug from '@/components/utilities/Debug.vue' import Debug from '@/components/utilities/Debug.vue'
import Notifications from '@/components/utilities/Notifications.vue' import Notifications from '@/components/utilities/Notifications.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { useSoundComposable } from '@/composables/useSoundComposable'
import { socketManager } from '@/managers/SocketManager'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore' import { computed, watch } from 'vue'
import { computed, ref, useTemplateRef, watch } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const mapEditor = useMapEditorComposable() const mapEditor = useMapEditorComposable()
const { playSound } = useSoundComposable()
const currentScreen = computed(() => { const currentScreen = computed(() => {
if (!gameStore.game.isLoaded) return Loading if (!gameStore.game.isLoaded) return Loading
if (!gameStore.connection) return Login if (!socketManager.connection) return Login
if (!gameStore.token) return Login if (!socketManager.token) return Login
if (!gameStore.character) return Characters if (!gameStore.character) return Characters
if (mapEditor.active.value) return MapEditor if (mapEditor.active.value) return MapEditor
return Game return Game
@ -42,13 +43,13 @@ watch(
) )
// #209: Play sound when a button is pressed // #209: Play sound when a button is pressed
// @TODO: Not all button-like elements will actually be a button, so we need to find a better way to do this
addEventListener('click', (event) => { addEventListener('click', (event) => {
if (!(event.target instanceof HTMLButtonElement)) { const classList = ['btn-cyan', 'btn-red', 'btn-indigo', 'btn-empty', 'btn-sound']
return const target = event.target as HTMLElement
// console.log(target) // Uncomment to log the clicked element
if (classList.some((className) => target.classList.contains(className))) {
playSound('/assets/sounds/button-click.wav')
} }
const audio = new Audio('/assets/music/click-btn.mp3')
audio.play()
}) })
// Watch for "G" key press and toggle the gm panel // Watch for "G" key press and toggle the gm panel

View File

@ -1,6 +1,7 @@
export default { export default {
name: import.meta.env.VITE_NAME, name: import.meta.env.VITE_NAME,
development: import.meta.env.VITE_DEVELOPMENT === 'true', domain: import.meta.env.VITE_DOMAIN,
environment: import.meta.env.VITE_ENVIRONMENT,
server_endpoint: import.meta.env.VITE_SERVER_ENDPOINT, server_endpoint: import.meta.env.VITE_SERVER_ENDPOINT,
tile_size: { tile_size: {
width: Number(import.meta.env.VITE_TILE_SIZE_WIDTH), width: Number(import.meta.env.VITE_TILE_SIZE_WIDTH),

View File

@ -3,3 +3,61 @@ export enum Direction {
NEGATIVE, NEGATIVE,
UNCHANGED UNCHANGED
} }
export enum SocketEvent {
CONNECT_ERROR = 'connect_error',
RECONNECT_FAILED = 'reconnect_failed',
CLOSE = '52',
DATA = '51',
CHARACTER_CONNECT = '50',
CHARACTER_CREATE = '49',
CHARACTER_DELETE = '48',
CHARACTER_LIST = '47',
GM_CHARACTERHAIR_CREATE = '46',
GM_CHARACTERHAIR_REMOVE = '45',
GM_CHARACTERHAIR_LIST = '44',
GM_CHARACTERHAIR_UPDATE = '43',
GM_CHARACTERTYPE_CREATE = '42',
GM_CHARACTERTYPE_REMOVE = '41',
GM_CHARACTERTYPE_LIST = '40',
GM_CHARACTERTYPE_UPDATE = '39',
GM_ITEM_CREATE = '38',
GM_ITEM_REMOVE = '37',
GM_ITEM_LIST = '36',
GM_ITEM_UPDATE = '35',
GM_MAPOBJECT_LIST = '34',
GM_MAPOBJECT_REMOVE = '33',
GM_MAPOBJECT_UPDATE = '32',
GM_MAPOBJECT_UPLOAD = '31',
GM_SPRITE_COPY = '30',
GM_SPRITE_CREATE = '29',
GM_SPRITE_DELETE = '28',
GM_SPRITE_LIST = '27',
GM_SPRITE_UPDATE = '26',
GM_TILE_DELETE = '25',
GM_TILE_LIST = '24',
GM_TILE_UPDATE = '23',
GM_TILE_UPLOAD = '22',
GM_MAP_CREATE = '21',
GM_MAP_DELETE = '20',
GM_MAP_REQUEST = '19',
GM_MAP_UPDATE = '18',
MAP_CHARACTER_MOVEERROR = '17',
DISCONNECT = 'disconnect',
USER_DISCONNECT = '15',
LOGIN = '14',
LOGGED_IN = '13',
NOTIFICATION = '12',
DATE = '11',
FAILED = '10',
COMPLETED = '9',
CONNECTION = 'connection',
WEATHER = '7',
CHARACTER_DISCONNECT = '6',
MAP_CHARACTER_ATTACK = '5',
MAP_CHARACTER_TELEPORT = '4',
MAP_CHARACTER_JOIN = '3',
MAP_CHARACTER_LEAVE = '2',
MAP_CHARACTER_MOVE = '1',
CHAT_MESSAGE = '0'
}

View File

@ -26,7 +26,7 @@ export type TextureData = {
} }
export type Tile = { export type Tile = {
id: UUID id: string
name: string name: string
tags: any | null tags: any | null
createdAt: Date createdAt: Date
@ -34,9 +34,10 @@ export type Tile = {
} }
export type MapObject = { export type MapObject = {
id: UUID id: string
name: string name: string
tags: any | null tags: string[]
pivotPoints: { x: number; y: number }[]
originX: number originX: number
originY: number originY: number
frameRate: number frameRate: number
@ -47,7 +48,7 @@ export type MapObject = {
} }
export type Item = { export type Item = {
id: UUID id: string
name: string name: string
description: string | null description: string | null
itemType: ItemType itemType: ItemType
@ -62,7 +63,7 @@ export type ItemType = 'WEAPON' | 'HELMET' | 'CHEST' | 'LEGS' | 'BOOTS' | 'GLOVE
export type ItemRarity = 'COMMON' | 'UNCOMMON' | 'RARE' | 'EPIC' | 'LEGENDARY' export type ItemRarity = 'COMMON' | 'UNCOMMON' | 'RARE' | 'EPIC' | 'LEGENDARY'
export type Map = { export type Map = {
id: UUID id: string
name: string name: string
width: number width: number
height: number height: number
@ -78,17 +79,14 @@ export type Map = {
} }
export type MapEffect = { export type MapEffect = {
id: UUID id: string
map: Map
effect: string effect: string
strength: number strength: number
} }
export type PlacedMapObject = { export type PlacedMapObject = {
id: UUID id: string
map: Map mapObject: MapObject | string
mapObject: MapObject
depth: number
isRotated: boolean isRotated: boolean
positionX: number positionX: number
positionY: number positionY: number
@ -102,8 +100,8 @@ export enum MapEventTileType {
} }
export type MapEventTile = { export type MapEventTile = {
id: UUID id: string
mapId: UUID map: string
type: MapEventTileType type: MapEventTileType
positionX: number positionX: number
positionY: number positionY: number
@ -111,7 +109,7 @@ export type MapEventTile = {
} }
export type MapEventTileTeleport = { export type MapEventTileTeleport = {
id: UUID id: string
mapEventTile: MapEventTile mapEventTile: MapEventTile
toMap: Map toMap: Map
toPositionX: number toPositionX: number
@ -120,7 +118,7 @@ export type MapEventTileTeleport = {
} }
export type User = { export type User = {
id: UUID id: string
username: string username: string
password: string password: string
characters: Character[] characters: Character[]
@ -140,7 +138,7 @@ export enum CharacterRace {
} }
export type CharacterType = { export type CharacterType = {
id: UUID id: string
name: string name: string
gender: CharacterGender gender: CharacterGender
race: CharacterRace race: CharacterRace
@ -151,7 +149,7 @@ export type CharacterType = {
} }
export type CharacterHair = { export type CharacterHair = {
id: UUID id: string
name: string name: string
sprite?: Sprite sprite?: Sprite
gender: CharacterGender gender: CharacterGender
@ -159,8 +157,8 @@ export type CharacterHair = {
} }
export type Character = { export type Character = {
id: UUID id: string
userId: UUID userid: string
user: User user: User
name: string name: string
hitpoints: number hitpoints: number
@ -187,14 +185,14 @@ export type MapCharacter = {
} }
export type CharacterItem = { export type CharacterItem = {
id: UUID id: string
character: Character character: Character
item: Item item: Item
quantity: number quantity: number
} }
export type CharacterEquipment = { export type CharacterEquipment = {
id: UUID id: string
slot: CharacterEquipmentSlotType slot: CharacterEquipmentSlotType
characterItem: CharacterItem characterItem: CharacterItem
} }
@ -209,7 +207,7 @@ export enum CharacterEquipmentSlotType {
} }
export type Sprite = { export type Sprite = {
id: UUID id: string
name: string name: string
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
@ -256,6 +254,6 @@ export type WeatherState = {
} }
export type mapLoadData = { export type mapLoadData = {
mapId: UUID mapId: string
characters: MapCharacter[] characters: MapCharacter[]
} }

View File

@ -7,25 +7,8 @@ export function uuidv4() {
} }
export function unduplicateArray(array: any[]) { export function unduplicateArray(array: any[]) {
return [...new Set(array.flat())] const arrayToProcess = typeof array.flat === 'function' ? array.flat() : array
} return [...new Set(arrayToProcess)]
export function getDomain() {
// Check if not localhost
if (window.location.hostname !== 'localhost') {
return window.location.hostname
}
// Check if not IP address
if (window.location.hostname.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) {
return window.location.hostname
}
if (window.location.hostname.split('.').length < 3) {
return window.location.hostname
}
return window.location.hostname.split('.').slice(-2).join('.')
} }
export async function downloadCache<T extends { id: string; updatedAt: Date }>(endpoint: string, storage: BaseStorage<T>) { export async function downloadCache<T extends { id: string; updatedAt: Date }>(endpoint: string, storage: BaseStorage<T>) {
@ -38,10 +21,20 @@ export async function downloadCache<T extends { id: string; updatedAt: Date }>(e
} }
const items = response.data ?? [] const items = response.data ?? []
const serverItemIds = new Set(items.map((item) => item.id))
// Remove items that don't exist on server
const existingItems = await storage.getAll()
for (const existingItem of existingItems) {
if (!serverItemIds.has(existingItem.id)) {
await storage.delete(existingItem.id)
}
}
// Update or add new items
for (const item of items) { for (const item of items) {
let overwrite = false let overwrite = false
const existingItem = await storage.get(item.id) const existingItem = await storage.getById(item.id)
if (!existingItem || item.updatedAt > existingItem.updatedAt) { if (!existingItem || item.updatedAt > existingItem.updatedAt) {
overwrite = true overwrite = true

View File

@ -73,7 +73,7 @@ input {
} }
.input-field { .input-field {
@apply px-4 py-2.5 text-base leading-5 bg-gray border border-solid border-gray-500 rounded text-gray-300; @apply px-4 py-2.5 text-base leading-5 bg-gray border border-solid border-gray-500 rounded text-gray-300 font-default;
&:focus-visible { &:focus-visible {
@apply outline-none border-cyan rounded bg-gray-900; @apply outline-none border-cyan rounded bg-gray-900;
} }
@ -88,6 +88,12 @@ input {
} }
} }
select {
&.input-field {
@apply appearance-none bg-[url('/assets/icons/mapEditor/dropdown-chevron.svg')] bg-no-repeat bg-[calc(100%_-_10px)_center] bg-[length:20px] text-white;
}
}
.form-field-full { .form-field-full {
@apply w-full flex flex-col mb-5; @apply w-full flex flex-col mb-5;
label { label {
@ -122,6 +128,15 @@ button {
} }
} }
&.btn-indigo {
@apply bg-indigo-500 text-gray-50 text-base leading-5 rounded py-2.5;
&.active,
&:hover {
@apply bg-indigo-600;
}
}
&.btn-empty { &.btn-empty {
@apply text-gray-50 border-2 border-solid border-gray-500 text-base leading-5 rounded py-2.5; @apply text-gray-50 border-2 border-solid border-gray-500 text-base leading-5 rounded py-2.5;
@ -149,6 +164,10 @@ button {
@apply bg-gray bg-none; @apply bg-gray bg-none;
} }
.list-open {
@apply w-[calc(75%_-_40px)] max-xl:w-[calc(100%_-_360px)];
}
.hair-deselect:has(:checked) { .hair-deselect:has(:checked) {
img { img {
@apply brightness-200; @apply brightness-200;

View File

@ -8,19 +8,19 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { Direction } from '@/application/enums'
import { type MapCharacter } from '@/application/types' import { type MapCharacter } from '@/application/types'
import CharacterHair from '@/components/game/character/partials/CharacterHair.vue' import CharacterHair from '@/components/game/character/partials/CharacterHair.vue'
import ChatBubble from '@/components/game/character/partials/ChatBubble.vue' import ChatBubble from '@/components/game/character/partials/ChatBubble.vue'
import HealthBar from '@/components/game/character/partials/HealthBar.vue' import HealthBar from '@/components/game/character/partials/HealthBar.vue'
import { useCharacterSprite } from '@/composables/useCharacterSpriteComposable' import { useCharacterSpriteComposable } from '@/composables/useCharacterSpriteComposable'
import { useSoundComposable } from '@/composables/useSoundComposable'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapStore } from '@/stores/mapStore' import { useMapStore } from '@/stores/mapStore'
import { Container, Sprite, useScene } from 'phavuer' import { Container, Sprite, useScene } from 'phavuer'
import { onMounted, onUnmounted, watch } from 'vue' import { onMounted, onUnmounted, watch } from 'vue'
const props = defineProps<{ const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap tileMap: Phaser.Tilemaps.Tilemap
mapCharacter: MapCharacter mapCharacter: MapCharacter
}>() }>()
@ -28,14 +28,14 @@ const gameStore = useGameStore()
const mapStore = useMapStore() const mapStore = useMapStore()
const scene = useScene() const scene = useScene()
const { characterContainer, characterSprite, currentPositionX, currentPositionY, isometricDepth, isFlippedX, updatePosition, calcDirection, updateSprite, initializeSprite, cleanup } = useCharacterSprite(scene, props.tilemap, props.mapCharacter) const { characterContainer, characterSprite, currentPositionX, currentPositionY, isometricDepth, isFlippedX, updatePosition, playAnimation, updateSprite, initializeSprite, cleanup } = useCharacterSpriteComposable(scene, props.tileMap, props.mapCharacter)
const { playSound, stopSound } = useSoundComposable()
const handlePositionUpdate = (newValues: any, oldValues: any) => { const handlePositionUpdate = (newValues: any, oldValues: any) => {
if (!newValues) return if (!newValues) return
if (!oldValues || newValues.positionX !== oldValues.positionX || newValues.positionY !== oldValues.positionY) { if (!oldValues || newValues.positionX !== oldValues.positionX || newValues.positionY !== oldValues.positionY) {
const direction = !oldValues ? Direction.POSITIVE : calcDirection(oldValues.positionX, oldValues.positionY, newValues.positionX, newValues.positionY) updatePosition(newValues.positionX, newValues.positionY)
updatePosition(newValues.positionX, newValues.positionY, direction)
} }
if (newValues.isMoving !== oldValues?.isMoving || newValues.rotation !== oldValues?.rotation) { if (newValues.isMoving !== oldValues?.isMoving || newValues.rotation !== oldValues?.rotation) {
@ -43,6 +43,39 @@ const handlePositionUpdate = (newValues: any, oldValues: any) => {
} }
} }
/**
* Plays walk sound when character is moving
*/
watch(
() => props.mapCharacter.isMoving,
(newValue) => {
if (newValue) {
playSound('/assets/sounds/walk.wav', false, true)
} else {
stopSound('/assets/sounds/walk.wav')
}
}
)
/**
* Plays attack animation and sound when character is attacking
*/
watch(
() => props.mapCharacter.isAttacking,
(newValue) => {
if (newValue) {
playAnimation('attack')
playSound('/assets/sounds/attack.wav', false, true)
} else {
stopSound('/assets/sounds/attack.wav')
}
mapStore.updateCharacterProperty(props.mapCharacter.character.id, 'isAttacking', false)
}
)
/**
* Handles position updates and movement delay
*/
watch( watch(
() => ({ () => ({
positionX: props.mapCharacter.character.positionX, positionX: props.mapCharacter.character.positionX,
@ -51,14 +84,14 @@ watch(
rotation: props.mapCharacter.character.rotation, rotation: props.mapCharacter.character.rotation,
isAttacking: props.mapCharacter.isAttacking isAttacking: props.mapCharacter.isAttacking
}), }),
handlePositionUpdate (oldValues, newValues) => {
handlePositionUpdate(oldValues, newValues)
}
) )
onMounted(async () => { onMounted(async () => {
await initializeSprite() await initializeSprite()
if (props.mapCharacter.character.id === gameStore.character!.id) { if (props.mapCharacter.character.id === gameStore.character!.id) {
mapStore.setCharacterLoaded(true)
scene.cameras.main.startFollow(characterContainer.value as Phaser.GameObjects.Container) scene.cameras.main.startFollow(characterContainer.value as Phaser.GameObjects.Container)
} }
}) })

View File

@ -1,51 +0,0 @@
<template>
<Image v-bind="imageProps" v-if="gameStore.getLoadedAsset(texture)" />
</template>
<script lang="ts" setup>
import type { MapCharacter, Sprite as SpriteT } from '@/application/types'
import { loadSpriteTextures } from '@/composables/gameComposable'
import { useGameStore } from '@/stores/gameStore'
import { Image, useScene } from 'phavuer'
import { computed } from 'vue'
const props = defineProps<{
mapCharacter: MapCharacter
currentX: number
currentY: number
}>()
const gameStore = useGameStore()
const scene = useScene()
const texture = computed(() => {
const { rotation, characterHair } = props.mapCharacter.character
const spriteId = characterHair?.sprite?.id
const direction = [0, 6].includes(rotation) ? 'back' : 'front'
return `${spriteId}-${direction}`
})
const isFlippedX = computed(() => [6, 4].includes(props.mapCharacter.character.rotation ?? 0))
const imageProps = computed(() => {
// Get the current sprite action based on direction
const direction = [0, 6].includes(props.mapCharacter.character.rotation ?? 0) ? 'back' : 'front'
const spriteAction = props.mapCharacter.character.characterHair?.sprite?.spriteActions?.find((spriteAction) => spriteAction.action === direction)
return {
depth: 1,
originX: Number(spriteAction?.originX) ?? 0,
originY: Number(spriteAction?.originY) ?? 0,
flipX: isFlippedX.value,
texture: texture.value
// y: props.mapCharacter.isMoving ? Math.floor(Date.now() / 250) % 2 : 0
}
})
loadSpriteTextures(scene, props.mapCharacter.character.characterHair?.sprite as SpriteT)
.then(() => {})
.catch((error) => {
console.error('Error loading texture:', error)
})
</script>

View File

@ -4,7 +4,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { MapCharacter, Sprite as SpriteT } from '@/application/types' import type { MapCharacter, Sprite as SpriteT } from '@/application/types'
import { loadSpriteTextures } from '@/composables/gameComposable' import { loadSpriteTextures } from '@/services/textureService'
import { CharacterHairStorage, CharacterTypeStorage, SpriteStorage } from '@/storage/storages' import { CharacterHairStorage, CharacterTypeStorage, SpriteStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { Image, useScene } from 'phavuer' import { Image, useScene } from 'phavuer'
@ -48,7 +48,7 @@ onMounted(async () => {
hairSpriteId.value = spriteId hairSpriteId.value = spriteId
const spriteStorage = new SpriteStorage() const spriteStorage = new SpriteStorage()
sprite.value = await spriteStorage.get(spriteId) sprite.value = await spriteStorage.getById(spriteId)
await loadSpriteTextures(scene, spriteId) await loadSpriteTextures(scene, spriteId)
}) })
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<Container ref="characterChatContainer" :depth="999"> <Container ref="characterChatContainer">
<RoundRectangle @create="createChatBubble" :origin-x="0.5" :origin-y="7.5" :fillColor="0xffffff" :width="194" :height="21" :radius="20" /> <RoundRectangle @create="createChatBubble" :origin-x="0.5" :origin-y="7.5" :fillColor="0xffffff" :width="194" :height="21" :radius="20" />
<Text @create="createChatText" :style="{ fontSize: 13, fontFamily: 'Arial', color: '#000' }" /> <Text @create="createChatText" :style="{ fontSize: 13, fontFamily: 'Arial', color: '#000' }" />
</Container> </Container>

View File

@ -2,7 +2,7 @@
<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 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 ref="chatWindow" class="w-full overflow-auto h-32 mb-5 bg-gray rounded-md border-2 border-solid border-gray-500 text-gray-300" v-show="gameStore.uiSettings.isChatOpen">
<div v-for="message in chats" class="flex-col py-2 items-center p-3"> <div v-for="message in chats" class="flex-col py-2 items-center p-3">
<span class="text-ellipsis overflow-hidden whitespace-nowrap text-sm" :class="{ 'text-cyan-300': gameStore.character?.role == 'gm' }">{{ message.character.name }}</span> <span class="text-ellipsis overflow-hidden whitespace-nowrap text-sm" :class="{ 'text-cyan-300': gameStore.character?.role == 'gm' }">{{ message.character }}</span>
<p class="text-gray-50 m-0">{{ message.message }}</p> <p class="text-gray-50 m-0">{{ message.message }}</p>
</div> </div>
</div> </div>
@ -21,7 +21,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { Chat } from '@/application/types' import { SocketEvent } from '@/application/enums'
import { socketManager } from '@/managers/SocketManager'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapStore } from '@/stores/mapStore' import { useMapStore } from '@/stores/mapStore'
import { onClickOutside, useFocus } from '@vueuse/core' import { onClickOutside, useFocus } from '@vueuse/core'
@ -30,10 +31,9 @@ import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
const scene = useScene() const scene = useScene()
const gameStore = useGameStore() const gameStore = useGameStore()
const mapStore = useMapStore()
const message = ref('') const message = ref('')
const chats = ref([] as Chat[]) const chats = ref<{ character: string; message: string }[]>([])
const chatWindow = ref<HTMLElement | null>(null) const chatWindow = ref<HTMLElement | null>(null)
const chatInput = ref<HTMLElement | null>(null) const chatInput = ref<HTMLElement | null>(null)
@ -55,7 +55,7 @@ function unfocusChat(event: Event, targetElement: HTMLElement) {
const sendMessage = () => { const sendMessage = () => {
if (!message.value.trim()) return if (!message.value.trim()) return
gameStore.connection?.emit('chat:message', { message: message.value }, (response: boolean) => {}) socketManager.emit(SocketEvent.CHAT_MESSAGE, { message: message.value }, (response: boolean) => {})
message.value = '' message.value = ''
} }
@ -79,21 +79,30 @@ const scrollToBottom = () => {
}) })
} }
gameStore.connection?.on('chat:message', (data: Chat) => { socketManager.on(SocketEvent.CHAT_MESSAGE, (data: { character: string; message: string }) => {
chats.value.push(data) if (!data.character || !data.message) return
chats.value.push({ character: data.character, message: data.message })
scrollToBottom() scrollToBottom()
if (!mapStore.characterLoaded) return const characterContainer = scene.children.getByName(data.character) as Phaser.GameObjects.Container
if (!characterContainer) {
console.log('No character container found')
return
}
const characterContainer = scene.children.getByName(data.character.name) as Phaser.GameObjects.Container const characterChatContainer = characterContainer.getByName(data.character + '_chatContainer') as Phaser.GameObjects.Container
if (!characterContainer) return if (!characterChatContainer) {
console.log('No character chat container found')
return
}
const characterChatContainer = characterContainer.getByName(data.character.name + '_chatContainer') as Phaser.GameObjects.Container const chatBubble = characterChatContainer.getByName(data.character + '_chatBubble') as Phaser.GameObjects.Container
if (!characterChatContainer) return const chatText = characterChatContainer.getByName(data.character + '_chatText') as Phaser.GameObjects.Text
if (!chatText || !chatBubble) {
const chatBubble = characterChatContainer.getByName(data.character.name + '_chatBubble') as Phaser.GameObjects.Container console.log('No chat text or bubble found')
const chatText = characterChatContainer.getByName(data.character.name + '_chatText') as Phaser.GameObjects.Text return
if (!chatText || !chatBubble) return }
function calculateTextWidth(text: string, font: string, fontSize: number): number { function calculateTextWidth(text: string, font: string, fontSize: number): number {
// Create a canvas element // Create a canvas element
@ -144,7 +153,7 @@ onMounted(() => {
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
gameStore.connection?.off('chat:message') socketManager.off(SocketEvent.CHAT_MESSAGE)
removeEventListener('keydown', focusChat) removeEventListener('keydown', focusChat)
}) })
</script> </script>

View File

@ -1,21 +1,21 @@
<template> <template>
<div class="absolute top-0 right-4 hidden lg:block"> <div class="absolute top-0 right-4 hidden lg:block" v-if="gameStore.world.date && typeof gameStore.world.date === 'object'">
<p class="text-white text-lg">{{ gameStore.world.date.toLocaleString() }}</p> <p class="text-white text-lg">
{{ useDateFormat(gameStore.world.date, 'YYYY/MM/DD HH:mm') }}
</p>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SocketEvent } from '@/application/enums'
import { socketManager } from '@/managers/SocketManager'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useDateFormat } from '@vueuse/core'
import { onUnmounted } from 'vue' import { onUnmounted } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
// Listen for new date from socket
gameStore.connection?.on('date', (data: Date) => {
gameStore.world.date = new Date(data)
})
onUnmounted(() => { onUnmounted(() => {
gameStore.connection?.off('date') socketManager.off(SocketEvent.DATE)
}) })
</script> </script>

View File

@ -1,14 +1,49 @@
<template> <template>
<Character v-for="item in mapStore.characters" :key="item.character.id" :tilemap="tilemap" :mapCharacter="item" /> <Character v-for="item in mapStore.characters" :key="item.character.id" :tileMap :mapCharacter="item" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SocketEvent } from '@/application/enums'
import type { MapCharacter, UUID } from '@/application/types'
import Character from '@/components/game/character/Character.vue' import Character from '@/components/game/character/Character.vue'
import { socketManager } from '@/managers/SocketManager'
import { useGameStore } from '@/stores/gameStore'
import { useMapStore } from '@/stores/mapStore' import { useMapStore } from '@/stores/mapStore'
import { onUnmounted } from 'vue'
const gameStore = useGameStore()
const mapStore = useMapStore() const mapStore = useMapStore()
const props = defineProps<{ const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap tileMap: Phaser.Tilemaps.Tilemap
}>() }>()
socketManager.on(SocketEvent.MAP_CHARACTER_JOIN, (data: MapCharacter) => {
mapStore.addCharacter(data)
})
socketManager.on(SocketEvent.MAP_CHARACTER_LEAVE, (characterId: UUID) => {
mapStore.removeCharacter(characterId)
})
socketManager.on(SocketEvent.MAP_CHARACTER_MOVE, ([characterId, posX, posY, rot, isMoving]: [UUID, number, number, number, boolean]) => {
mapStore.updateCharacterPosition([characterId, posX, posY, rot, isMoving])
if (characterId === gameStore.character?.id) {
gameStore.character!.positionX = posX
gameStore.character!.positionY = posY
gameStore.character!.rotation = rot
}
})
socketManager.on(SocketEvent.MAP_CHARACTER_ATTACK, (characterId: UUID) => {
mapStore.updateCharacterProperty(characterId, 'isAttacking', true)
})
onUnmounted(() => {
socketManager.off(SocketEvent.MAP_CHARACTER_ATTACK)
socketManager.off(SocketEvent.MAP_CHARACTER_MOVE)
socketManager.off(SocketEvent.MAP_CHARACTER_JOIN)
socketManager.off(SocketEvent.MAP_CHARACTER_LEAVE)
})
</script> </script>

View File

@ -1,56 +1,71 @@
<template> <template>
<MapTiles :key="mapStore.mapId" @tileMap:create="tileMap = $event" /> <MapTiles v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer />
<PlacedMapObjects v-if="tileMap" :key="mapStore.mapId" :tilemap="tileMap" /> <PlacedMapObjects v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer />
<Characters v-if="tileMap && mapStore.characters" :tilemap="tileMap" /> <Characters v-if="tileMap && mapStore.characters" :tileMap />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { MapCharacter, mapLoadData, UUID } from '@/application/types' import { SocketEvent } from '@/application/enums'
import type { mapLoadData } from '@/application/types'
import { unduplicateArray } from '@/application/utilities'
import Characters from '@/components/game/map/Characters.vue' import Characters from '@/components/game/map/Characters.vue'
import MapTiles from '@/components/game/map/MapTiles.vue' import MapTiles from '@/components/game/map/MapTiles.vue'
import PlacedMapObjects from '@/components/game/map/PlacedMapObjects.vue' import PlacedMapObjects from '@/components/game/map/PlacedMapObjects.vue'
import { socketManager } from '@/managers/SocketManager'
import { createTileLayer, createTileMap, loadTileTexturesFromMapTileArray } from '@/services/mapService'
import { MapStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapStore } from '@/stores/mapStore' import { useMapStore } from '@/stores/mapStore'
import { onUnmounted, shallowRef } from 'vue' import { useScene } from 'phavuer'
import { onMounted, onUnmounted, shallowRef, watch } from 'vue'
const scene = useScene()
const gameStore = useGameStore() const gameStore = useGameStore()
const mapStore = useMapStore() const mapStore = useMapStore()
const mapStorage = new MapStorage()
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>() const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
const tileMapLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
// Event listeners // Event listeners
gameStore.connection?.on('map:character:teleport', async (data: mapLoadData) => { socketManager.on(SocketEvent.MAP_CHARACTER_TELEPORT, (data: mapLoadData) => {
mapStore.setMapId(data.mapId) mapStore.setMapId(data.mapId)
mapStore.setCharacters(data.characters) mapStore.setCharacters(data.characters)
}) })
gameStore.connection?.on('map:character:join', async (data: MapCharacter) => { async function initialize() {
mapStore.addCharacter(data) if (!mapStore.mapId) return
})
gameStore.connection?.on('map:character:leave', (characterId: UUID) => { const map = await mapStorage.getById(mapStore.mapId)
mapStore.removeCharacter(characterId) if (!map) return
})
gameStore.connection?.on('map:character:attack', (characterId: UUID) => { await loadTileTexturesFromMapTileArray(mapStore.mapId, scene)
mapStore.updateCharacterProperty(characterId, 'isAttacking', true)
})
gameStore.connection?.on('map:character:move', (data: { characterId: UUID; positionX: number; positionY: number; rotation: number; isMoving: boolean }) => { tileMap.value = createTileMap(scene, map)
mapStore.updateCharacterPosition(data) tileMapLayer.value = createTileLayer(tileMap.value, unduplicateArray(map.tiles))
// @TODO: Replace with universal class, composable or store
if (data.characterId === gameStore.character?.id) {
gameStore.character!.positionX = data.positionX
gameStore.character!.positionY = data.positionY
gameStore.character!.rotation = data.rotation
} }
watch(
() => mapStore.mapId,
async () => {
await initialize()
}
)
onMounted(async () => {
if (!mapStore.mapId) return
await initialize()
}) })
onUnmounted(() => { onUnmounted(() => {
mapStore.reset() if (tileMap.value) {
gameStore.connection?.off('map:character:teleport') tileMap.value.destroyLayer('tiles')
gameStore.connection?.off('map:character:join') tileMap.value.removeAllLayers()
gameStore.connection?.off('map:character:leave') tileMap.value.destroy()
gameStore.connection?.off('map:character:move') }
socketManager.off(SocketEvent.MAP_CHARACTER_TELEPORT)
}) })
</script> </script>

View File

@ -1,74 +1,32 @@
<template> <template>
<Controls v-if="tileLayer" :layer="tileLayer" :depth="0" /> <Controls v-if="tileMapLayer" :layer="tileMapLayer" :depth="0" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config'
import type { Map as MapT, UUID } from '@/application/types'
import { unduplicateArray } from '@/application/utilities'
import Controls from '@/components/utilities/Controls.vue' import Controls from '@/components/utilities/Controls.vue'
import { loadMapTilesIntoScene, setLayerTiles } from '@/composables/mapComposable' import { loadTileTexturesFromMapTileArray, placeTiles } from '@/services/mapService'
import { MapStorage } from '@/storage/storages' import { MapStorage } from '@/storage/storages'
import { useMapStore } from '@/stores/mapStore' import { useMapStore } from '@/stores/mapStore'
import { useScene } from 'phavuer' import { useScene } from 'phavuer'
import { onBeforeUnmount, shallowRef } from 'vue' import { onMounted } from 'vue'
import Tileset = Phaser.Tilemaps.Tileset
const emit = defineEmits(['tileMap:create'])
const scene = useScene() const scene = useScene()
const mapStore = useMapStore() const mapStore = useMapStore()
const mapStorage = new MapStorage() const mapStorage = new MapStorage()
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>() const props = defineProps<{
const tileLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>() tileMap: Phaser.Tilemaps.Tilemap
tileMapLayer: Phaser.Tilemaps.TilemapLayer
}>()
function createTileMap(map: MapT) { onMounted(async () => {
const mapConfig = new Phaser.Tilemaps.MapData({ if (!mapStore.mapId) return
width: map.width,
height: map.height,
tileWidth: config.tile_size.width,
tileHeight: config.tile_size.height,
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
format: Phaser.Tilemaps.Formats.ARRAY_2D
})
const newTileMap = new Phaser.Tilemaps.Tilemap(scene, mapConfig) const map = await mapStorage.getById(mapStore.mapId)
emit('tileMap:create', newTileMap) if (!map) return
return newTileMap
}
function createTileLayer(currentTileMap: Phaser.Tilemaps.Tilemap, mapData: any) { await loadTileTexturesFromMapTileArray(mapStore.mapId, scene)
const tilesArray = unduplicateArray(mapData?.tiles.flat())
const tilesetImages = tilesArray.map((tile: string, index: number) => { placeTiles(props.tileMap, props.tileMapLayer, map.tiles)
return currentTileMap.addTilesetImage(tile, tile, config.tile_size.width, config.tile_size.height, 1, 2, index + 1, { x: 0, y: -config.tile_size.height })
})
// Add blank tile
tilesetImages.push(currentTileMap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.width, config.tile_size.height, 1, 2, 0, { x: 0, y: -config.tile_size.height }))
const layer = currentTileMap.createBlankLayer('tiles', tilesetImages as Tileset[], 0, config.tile_size.height) as Phaser.Tilemaps.TilemapLayer
layer.setDepth(0)
layer.setCullPadding(2, 2)
return layer
}
loadMapTilesIntoScene(mapStore.mapId as UUID, scene)
.then(() => mapStorage.get(mapStore.mapId))
.then((mapData) => {
if (!mapData || !mapData?.tiles) return
tileMap.value = createTileMap(mapData)
tileLayer.value = createTileLayer(tileMap.value, mapData)
setLayerTiles(tileMap.value, tileLayer.value, mapData.tiles)
})
.catch((error) => console.error('Failed to initialize map:', error))
onBeforeUnmount(() => {
if (!tileMap.value) return
tileMap.value.destroyLayer('tiles')
tileMap.value.removeAllLayers()
tileMap.value.destroy()
}) })
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<PlacedMapObject v-for="placedMapObject in items" :tilemap="tilemap" :placedMapObject /> <PlacedMapObject v-for="placedMapObject in items" :tileMap :tileMapLayer :placedMapObject />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -9,8 +9,11 @@ import { MapStorage } from '@/storage/storages'
import { useMapStore } from '@/stores/mapStore' import { useMapStore } from '@/stores/mapStore'
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
defineProps<{ defineProps<{
tilemap: Phaser.Tilemaps.Tilemap tileMap: Phaser.Tilemaps.Tilemap
tileMapLayer: TilemapLayer
}>() }>()
const mapStore = useMapStore() const mapStore = useMapStore()
@ -20,7 +23,7 @@ const items = ref<PlacedMapObjectT[]>([])
onMounted(async () => { onMounted(async () => {
if (!mapStore.mapId) return if (!mapStore.mapId) return
const map = await mapStorage.get(mapStore.mapId) const map = await mapStorage.getById(mapStore.mapId)
if (!map) return if (!map) return
items.value = map.placedMapObjects items.value = map.placedMapObjects

View File

@ -1,43 +1,82 @@
<template> <template>
<Image v-if="gameStore.isTextureLoaded(props.placedMapObject.mapObject.id)" v-bind="imageProps" /> <Image v-if="mapObject && gameStore.isTextureLoaded(props.placedMapObject.mapObject as string)" v-bind="imageProps" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { PlacedMapObject, TextureData } from '@/application/types' import config from '@/application/config'
import { loadTexture } from '@/composables/gameComposable' import type { MapObject, PlacedMapObject } from '@/application/types'
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/mapComposable' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { calculateIsometricDepth, loadMapObjectTextures, tileToWorldXY } from '@/services/mapService'
import { MapObjectStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { Image, useScene } from 'phavuer' import { Image, useScene } from 'phavuer'
import { computed, onMounted } from 'vue' import { computed, onMounted, ref, watch } from 'vue'
import Tilemap = Phaser.Tilemaps.Tilemap
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
const props = defineProps<{ const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
placedMapObject: PlacedMapObject placedMapObject: PlacedMapObject
tileMap: Tilemap
tileMapLayer: TilemapLayer
}>() }>()
const gameStore = useGameStore()
const scene = useScene() const scene = useScene()
const gameStore = useGameStore()
const mapEditor = useMapEditorComposable()
const mapObject = ref<MapObject>()
async function initialize() {
if (!props.placedMapObject.mapObject) return
/**
* Check if mapObject is an string or object, if its an object we assume its a mapObject and change it to a string
* We do this because this component is shared with the map editor, which gets sent the mapObject as an object by the server
*/
if (typeof props.placedMapObject.mapObject === 'object') {
// @ts-ignore
props.placedMapObject.mapObject = props.placedMapObject.mapObject.id
}
const mapObjectStorage = new MapObjectStorage()
const _mapObject = await mapObjectStorage.getById(props.placedMapObject.mapObject as string)
if (!_mapObject) return
mapObject.value = _mapObject
await loadMapObjectTextures([_mapObject], scene)
}
function calculateObjectPlacement(mapObj: PlacedMapObject): { x: number; y: number } {
let position = tileToWorldXY(props.tileMapLayer, mapObj.positionX, mapObj.positionY)
return {
x: position.worldPositionX - mapObject.value!.frameWidth / 2,
y: position.worldPositionY - mapObject.value!.frameHeight / 2 + config.tile_size.height
}
}
const imageProps = computed(() => ({ const imageProps = computed(() => ({
depth: calculateIsometricDepth(props.placedMapObject.positionX, props.placedMapObject.positionY, props.placedMapObject.mapObject.frameWidth, props.placedMapObject.mapObject.frameHeight), alpha: mapEditor.movingPlacedObject.value?.id == props.placedMapObject.id || mapEditor.selectedMapObject.value?.id == props.placedMapObject.id ? 0.5 : 1,
x: tileToWorldX(props.tilemap, props.placedMapObject.positionX, props.placedMapObject.positionY), tint: mapEditor.selectedPlacedObject.value?.id == props.placedMapObject.id ? 0x00ff00 : 0xffffff,
y: tileToWorldY(props.tilemap, props.placedMapObject.positionX, props.placedMapObject.positionY), depth: calculateIsometricDepth(props.placedMapObject.positionX, props.placedMapObject.positionY),
...calculateObjectPlacement(props.placedMapObject),
flipX: props.placedMapObject.isRotated, flipX: props.placedMapObject.isRotated,
texture: props.placedMapObject.mapObject.id, texture: mapObject.value!.id,
originY: Number(props.placedMapObject.mapObject.originX), originX: mapObject.value!.originX,
originX: Number(props.placedMapObject.mapObject.originY) originY: mapObject.value!.originY
})) }))
loadTexture(scene, { watch(
key: props.placedMapObject.mapObject.id, () => mapEditor.refreshMapObject.value,
data: '/textures/map_objects/' + props.placedMapObject.mapObject.id + '.png', async () => {
group: 'map_objects', await initialize()
updatedAt: props.placedMapObject.mapObject.updatedAt, }
frameWidth: props.placedMapObject.mapObject.frameWidth, )
frameHeight: props.placedMapObject.mapObject.frameHeight
} as TextureData).catch((error) => {
console.error('Error loading texture:', error)
})
onMounted(async () => {}) onMounted(async () => {
await initialize()
})
</script> </script>

View File

@ -6,7 +6,7 @@
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Users</button> <button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Users</button>
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Chats</button> <button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Chats</button>
<button @mousedown.stop class="btn-cyan active py-1.5 px-4 min-w-24">Asset manager</button> <button @mousedown.stop class="btn-cyan active py-1.5 px-4 min-w-24">Asset manager</button>
<button class="btn-cyan py-1.5 px-4 min-w-24" type="button" @click="$emit('open-map-editor')">Map editor</button> <button class="btn-cyan py-1.5 px-4 min-w-24" type="button" @click="mapEditor.toggleActive()">Map editor</button>
</div> </div>
</template> </template>
<template #modalBody> <template #modalBody>
@ -24,9 +24,8 @@ import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { ref } from 'vue' import { ref } from 'vue'
defineEmits(['open-map-editor'])
const gameStore = useGameStore()
const mapEditor = useMapEditorComposable() const mapEditor = useMapEditorComposable()
const gameStore = useGameStore()
let toggle = ref('asset-manager') let toggle = ref('asset-manager')
</script> </script>

View File

@ -34,7 +34,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SocketEvent } from '@/application/enums'
import type { CharacterGender, CharacterHair, Sprite } from '@/application/types' import type { CharacterGender, CharacterHair, Sprite } from '@/application/types'
import { socketManager } from '@/managers/SocketManager'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
@ -65,7 +67,7 @@ if (selectedCharacterHair.value) {
function removeCharacterHair() { function removeCharacterHair() {
if (!selectedCharacterHair.value) return if (!selectedCharacterHair.value) return
gameStore.connection?.emit('gm:characterHair:remove', { id: selectedCharacterHair.value.id }, (response: boolean) => { socketManager.emit(SocketEvent.GM_CHARACTERHAIR_REMOVE, { id: selectedCharacterHair.value.id }, (response: boolean) => {
if (!response) { if (!response) {
console.error('Failed to remove character hair') console.error('Failed to remove character hair')
return return
@ -75,7 +77,7 @@ function removeCharacterHair() {
} }
function refreshCharacterHairList(unsetSelectedCharacterHair = true) { function refreshCharacterHairList(unsetSelectedCharacterHair = true) {
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => { socketManager.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
assetManagerStore.setCharacterHairList(response) assetManagerStore.setCharacterHairList(response)
if (unsetSelectedCharacterHair) { if (unsetSelectedCharacterHair) {
@ -93,7 +95,7 @@ function saveCharacterHair() {
spriteId: characterSpriteId.value spriteId: characterSpriteId.value
} }
gameStore.connection?.emit('gm:characterHair:update', characterHairData, (response: boolean) => { socketManager.emit(SocketEvent.GM_CHARACTERHAIR_UPDATE, characterHairData, (response: boolean) => {
if (!response) { if (!response) {
console.error('Failed to save character type') console.error('Failed to save character type')
return return
@ -113,7 +115,7 @@ watch(selectedCharacterHair, (characterHair: CharacterHair | null) => {
onMounted(() => { onMounted(() => {
if (!selectedCharacterHair.value) return if (!selectedCharacterHair.value) return
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => { socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
assetManagerStore.setSpriteList(response) assetManagerStore.setSpriteList(response)
}) })
}) })

View File

@ -32,7 +32,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SocketEvent } from '@/application/enums'
import type { CharacterHair } from '@/application/types' import type { CharacterHair } from '@/application/types'
import { socketManager } from '@/managers/SocketManager'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useVirtualList } from '@vueuse/core' import { useVirtualList } from '@vueuse/core'
@ -52,13 +54,13 @@ const handleSearch = () => {
} }
const createNewCharacterHair = () => { const createNewCharacterHair = () => {
gameStore.connection?.emit('gm:characterHair:create', {}, (response: boolean) => { socketManager.emit(SocketEvent.GM_CHARACTERHAIR_CREATE, {}, (response: boolean) => {
if (!response) { if (!response) {
console.error('Failed to create new character type') console.error('Failed to create new character type')
return return
} }
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => { socketManager.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
assetManagerStore.setCharacterHairList(response) assetManagerStore.setCharacterHairList(response)
}) })
}) })
@ -92,7 +94,7 @@ function toTop() {
} }
onMounted(() => { onMounted(() => {
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => { socketManager.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
assetManagerStore.setCharacterHairList(response) assetManagerStore.setCharacterHairList(response)
}) })
}) })

View File

@ -40,7 +40,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SocketEvent } from '@/application/enums'
import type { CharacterGender, CharacterRace, CharacterType, Sprite } from '@/application/types' import type { CharacterGender, CharacterRace, CharacterType, Sprite } from '@/application/types'
import { socketManager } from '@/managers/SocketManager'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
@ -74,7 +76,7 @@ if (selectedCharacterType.value) {
function removeCharacterType() { function removeCharacterType() {
if (!selectedCharacterType.value) return if (!selectedCharacterType.value) return
gameStore.connection?.emit('gm:characterType:remove', { id: selectedCharacterType.value.id }, (response: boolean) => { socketManager.emit(SocketEvent.GM_CHARACTERTYPE_REMOVE, { id: selectedCharacterType.value.id }, (response: boolean) => {
if (!response) { if (!response) {
console.error('Failed to remove character type') console.error('Failed to remove character type')
return return
@ -84,7 +86,7 @@ function removeCharacterType() {
} }
function refreshCharacterTypeList(unsetSelectedCharacterType = true) { function refreshCharacterTypeList(unsetSelectedCharacterType = true) {
gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => { socketManager.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
assetManagerStore.setCharacterTypeList(response) assetManagerStore.setCharacterTypeList(response)
if (unsetSelectedCharacterType) { if (unsetSelectedCharacterType) {
@ -103,7 +105,7 @@ function saveCharacterType() {
spriteId: characterSpriteId.value spriteId: characterSpriteId.value
} }
gameStore.connection?.emit('gm:characterType:update', characterTypeData, (response: boolean) => { socketManager.emit(SocketEvent.GM_CHARACTERTYPE_UPDATE, characterTypeData, (response: boolean) => {
if (!response) { if (!response) {
console.error('Failed to save character type') console.error('Failed to save character type')
return return
@ -124,7 +126,7 @@ watch(selectedCharacterType, (characterType: CharacterType | null) => {
onMounted(() => { onMounted(() => {
if (!selectedCharacterType.value) return if (!selectedCharacterType.value) return
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => { socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
assetManagerStore.setSpriteList(response) assetManagerStore.setSpriteList(response)
}) })
}) })

View File

@ -32,7 +32,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SocketEvent } from '@/application/enums'
import type { CharacterType } from '@/application/types' import type { CharacterType } from '@/application/types'
import { socketManager } from '@/managers/SocketManager'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useVirtualList } from '@vueuse/core' import { useVirtualList } from '@vueuse/core'
@ -52,13 +54,13 @@ const handleSearch = () => {
} }
const createNewCharacterType = () => { const createNewCharacterType = () => {
gameStore.connection?.emit('gm:characterType:create', {}, (response: boolean) => { socketManager.emit(SocketEvent.GM_CHARACTERTYPE_CREATE, {}, (response: boolean) => {
if (!response) { if (!response) {
console.error('Failed to create new character type') console.error('Failed to create new character type')
return return
} }
gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => { socketManager.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
assetManagerStore.setCharacterTypeList(response) assetManagerStore.setCharacterTypeList(response)
}) })
}) })
@ -92,7 +94,7 @@ function toTop() {
} }
onMounted(() => { onMounted(() => {
gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => { socketManager.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
assetManagerStore.setCharacterTypeList(response) assetManagerStore.setCharacterTypeList(response)
}) })
}) })

View File

@ -44,7 +44,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SocketEvent } from '@/application/enums'
import type { Item, ItemRarity, ItemType, Sprite } from '@/application/types' import type { Item, ItemRarity, ItemType, Sprite } from '@/application/types'
import { socketManager } from '@/managers/SocketManager'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
@ -80,7 +82,7 @@ if (selectedItem.value) {
function removeItem() { function removeItem() {
if (!selectedItem.value) return if (!selectedItem.value) return
gameStore.connection?.emit('gm:item:remove', { id: selectedItem.value.id }, (response: boolean) => { socketManager.emit(SocketEvent.GM_ITEM_REMOVE, { id: selectedItem.value.id }, (response: boolean) => {
if (!response) { if (!response) {
console.error('Failed to remove item') console.error('Failed to remove item')
return return
@ -90,7 +92,7 @@ function removeItem() {
} }
function refreshItemList(unsetSelectedItem = true) { function refreshItemList(unsetSelectedItem = true) {
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => { socketManager.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
assetManagerStore.setItemList(response) assetManagerStore.setItemList(response)
if (unsetSelectedItem) { if (unsetSelectedItem) {
@ -110,7 +112,7 @@ function saveItem() {
spriteId: itemSpriteId.value spriteId: itemSpriteId.value
} }
gameStore.connection?.emit('gm:item:update', itemData, (response: boolean) => { socketManager.emit(SocketEvent.GM_ITEM_UPDATE, itemData, (response: boolean) => {
if (!response) { if (!response) {
console.error('Failed to save item') console.error('Failed to save item')
return return
@ -132,7 +134,7 @@ watch(selectedItem, (item: Item | null) => {
onMounted(() => { onMounted(() => {
if (!selectedItem.value) return if (!selectedItem.value) return
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => { socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
assetManagerStore.setSpriteList(response) assetManagerStore.setSpriteList(response)
}) })
}) })

View File

@ -29,7 +29,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SocketEvent } from '@/application/enums'
import type { Item } from '@/application/types' import type { Item } from '@/application/types'
import { socketManager } from '@/managers/SocketManager'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useVirtualList } from '@vueuse/core' import { useVirtualList } from '@vueuse/core'
@ -48,13 +50,13 @@ const handleSearch = () => {
} }
const createNewItem = () => { const createNewItem = () => {
gameStore.connection?.emit('gm:item:create', {}, (response: boolean) => { socketManager.emit(SocketEvent.GM_ITEM_CREATE, {}, (response: boolean) => {
if (!response) { if (!response) {
console.error('Failed to create new item') console.error('Failed to create new item')
return return
} }
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => { socketManager.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
assetManagerStore.setItemList(response) assetManagerStore.setItemList(response)
}) })
}) })
@ -88,7 +90,7 @@ function toTop() {
} }
onMounted(() => { onMounted(() => {
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => { socketManager.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
assetManagerStore.setItemList(response) assetManagerStore.setItemList(response)
}) })
}) })

View File

@ -1,7 +1,20 @@
<template> <template>
<div class="h-full overflow-auto"> <div class="h-full overflow-auto">
<div class="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray"> <div class="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray">
<img class="max-h-56" :src="`${config.server_endpoint}/textures/map_objects/${selectedMapObject?.id}.png`" :alt="'Object ' + selectedMapObject?.id" /> <div class="relative">
<img class="max-h-56" :src="`${config.server_endpoint}/textures/map_objects/${selectedMapObject?.id}.png`" :alt="'Object ' + selectedMapObject?.id" @click="addPivotPoint" ref="imageRef" />
<svg class="absolute bottom-1 left-0 w-full h-full pointer-events-none">
<line v-for="(_, index) in mapObjectPivotPoints.slice(0, -1)" :key="index" :x1="mapObjectPivotPoints[index].x" :y1="mapObjectPivotPoints[index].y" :x2="mapObjectPivotPoints[index + 1].x" :y2="mapObjectPivotPoints[index + 1].y" stroke="white" stroke-width="2" />
</svg>
<div
v-for="(point, index) in mapObjectPivotPoints"
:key="index"
class="absolute w-2 h-2 bg-white rounded-full cursor-move -translate-x-1.5 -translate-y-1.5 ring-2 ring-black"
:style="{ left: point.x + 'px', top: point.y + 'px' }"
@mousedown="startDragging(index, $event)"
@contextmenu.prevent="removePivotPoint(index)"
/>
</div>
</div> </div>
<div class="mt-5 block"> <div class="mt-5 block">
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject"> <form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject">
@ -44,8 +57,10 @@
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import config from '@/application/config'
import { SocketEvent } from '@/application/enums'
import type { MapObject } from '@/application/types' import type { MapObject } from '@/application/types'
import ChipsInput from '@/components/forms/ChipsInput.vue' import ChipsInput from '@/components/forms/ChipsInput.vue'
import { socketManager } from '@/managers/SocketManager'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
@ -57,11 +72,15 @@ const selectedMapObject = computed(() => assetManagerStore.selectedMapObject)
const mapObjectName = ref('') const mapObjectName = ref('')
const mapObjectTags = ref<string[]>([]) const mapObjectTags = ref<string[]>([])
const mapObjectPivotPoints = ref<Array<{ x: number; y: number }>>([])
const mapObjectOriginX = ref(0) const mapObjectOriginX = ref(0)
const mapObjectOriginY = ref(0) const mapObjectOriginY = ref(0)
const mapObjectFrameRate = ref(0) const mapObjectFrameRate = ref(0)
const mapObjectFrameWidth = ref(0) const mapObjectFrameWidth = ref(0)
const mapObjectFrameHeight = ref(0) const mapObjectFrameHeight = ref(0)
const imageRef = ref<HTMLImageElement | null>(null)
const isDragging = ref(false)
const draggedPointIndex = ref(-1)
if (!selectedMapObject.value) { if (!selectedMapObject.value) {
console.error('No map mapObject selected') console.error('No map mapObject selected')
@ -70,6 +89,7 @@ if (!selectedMapObject.value) {
if (selectedMapObject.value) { if (selectedMapObject.value) {
mapObjectName.value = selectedMapObject.value.name mapObjectName.value = selectedMapObject.value.name
mapObjectTags.value = selectedMapObject.value.tags mapObjectTags.value = selectedMapObject.value.tags
mapObjectPivotPoints.value = selectedMapObject.value.pivotPoints
mapObjectOriginX.value = selectedMapObject.value.originX mapObjectOriginX.value = selectedMapObject.value.originX
mapObjectOriginY.value = selectedMapObject.value.originY mapObjectOriginY.value = selectedMapObject.value.originY
mapObjectFrameRate.value = selectedMapObject.value.frameRate mapObjectFrameRate.value = selectedMapObject.value.frameRate
@ -78,7 +98,7 @@ if (selectedMapObject.value) {
} }
function removeObject() { function removeObject() {
gameStore.connection?.emit('gm:mapObject:remove', { mapObject: selectedMapObject.value?.id }, (response: boolean) => { socketManager.emit(SocketEvent.GM_MAPOBJECT_REMOVE, { mapObject: selectedMapObject.value?.id }, (response: boolean) => {
if (!response) { if (!response) {
console.error('Failed to remove mapObject') console.error('Failed to remove mapObject')
return return
@ -88,7 +108,7 @@ function removeObject() {
} }
function refreshObjectList(unsetSelectedMapObject = true) { function refreshObjectList(unsetSelectedMapObject = true) {
gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => { socketManager.emit(SocketEvent.GM_MAPOBJECT_LIST, {}, (response: MapObject[]) => {
assetManagerStore.setMapObjectList(response) assetManagerStore.setMapObjectList(response)
if (unsetSelectedMapObject) { if (unsetSelectedMapObject) {
@ -103,12 +123,13 @@ function saveObject() {
return return
} }
gameStore.connection?.emit( socketManager.emit(
'gm:mapObject:update', SocketEvent.GM_MAPOBJECT_UPDATE,
{ {
id: selectedMapObject.value.id, id: selectedMapObject.value.id,
name: mapObjectName.value, name: mapObjectName.value,
tags: mapObjectTags.value, tags: mapObjectTags.value,
pivotPoints: mapObjectPivotPoints.value,
originX: mapObjectOriginX.value, originX: mapObjectOriginX.value,
originY: mapObjectOriginY.value, originY: mapObjectOriginY.value,
frameRate: mapObjectFrameRate.value, frameRate: mapObjectFrameRate.value,
@ -129,6 +150,7 @@ watch(selectedMapObject, (mapObject: MapObject | null) => {
if (!mapObject) return if (!mapObject) return
mapObjectName.value = mapObject.name mapObjectName.value = mapObject.name
mapObjectTags.value = mapObject.tags mapObjectTags.value = mapObject.tags
mapObjectPivotPoints.value = mapObject.pivotPoints
mapObjectOriginX.value = mapObject.originX mapObjectOriginX.value = mapObject.originX
mapObjectOriginY.value = mapObject.originY mapObjectOriginY.value = mapObject.originY
mapObjectFrameRate.value = mapObject.frameRate mapObjectFrameRate.value = mapObject.frameRate
@ -140,7 +162,51 @@ onMounted(() => {
if (!selectedMapObject.value) return if (!selectedMapObject.value) return
}) })
function addPivotPoint(event: MouseEvent) {
if (!imageRef.value) return
// Max 2
if (mapObjectPivotPoints.value.length >= 2) return
const rect = imageRef.value.getBoundingClientRect()
const x = event.clientX - rect.left
const y = event.clientY - rect.top
mapObjectPivotPoints.value.push({ x, y })
}
function startDragging(index: number, event: MouseEvent) {
isDragging.value = true
draggedPointIndex.value = index
const moveHandler = (e: MouseEvent) => {
if (!isDragging.value || !imageRef.value) return
const rect = imageRef.value.getBoundingClientRect()
mapObjectPivotPoints.value[draggedPointIndex.value] = {
x: e.clientX - rect.left,
y: e.clientY - rect.top
}
}
const upHandler = () => {
isDragging.value = false
draggedPointIndex.value = -1
window.removeEventListener('mousemove', moveHandler)
window.removeEventListener('mouseup', upHandler)
}
window.addEventListener('mousemove', moveHandler)
window.addEventListener('mouseup', upHandler)
}
function removePivotPoint(index: number) {
mapObjectPivotPoints.value.splice(index, 1)
}
onBeforeUnmount(() => { onBeforeUnmount(() => {
assetManagerStore.setSelectedMapObject(null) assetManagerStore.setSelectedMapObject(null)
}) })
</script> </script>
<style scoped>
.pointer-events-none {
pointer-events: none;
}
</style>

View File

@ -29,7 +29,9 @@
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import config from '@/application/config'
import { SocketEvent } from '@/application/enums'
import type { MapObject } from '@/application/types' import type { MapObject } from '@/application/types'
import { socketManager } from '@/managers/SocketManager'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useVirtualList } from '@vueuse/core' import { useVirtualList } from '@vueuse/core'
@ -47,13 +49,13 @@ const elementToScroll = ref()
const handleFileUpload = (e: Event) => { const handleFileUpload = (e: Event) => {
const files = (e.target as HTMLInputElement).files const files = (e.target as HTMLInputElement).files
if (!files) return if (!files) return
gameStore.connection?.emit('gm:mapObject:upload', files, (response: boolean) => { socketManager.emit(SocketEvent.GM_MAPOBJECT_UPLOAD, files, (response: boolean) => {
if (!response) { if (!response) {
if (config.development) console.error('Failed to upload object') if (config.environment === 'development') console.error('Failed to upload map object')
return return
} }
gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => { socketManager.emit(SocketEvent.GM_MAPOBJECT_LIST, {}, (response: MapObject[]) => {
assetManagerStore.setMapObjectList(response) assetManagerStore.setMapObjectList(response)
}) })
}) })
@ -92,7 +94,7 @@ function toTop() {
} }
onMounted(() => { onMounted(() => {
gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => { socketManager.emit(SocketEvent.GM_MAPOBJECT_LIST, {}, (response: MapObject[]) => {
assetManagerStore.setMapObjectList(response) assetManagerStore.setMapObjectList(response)
}) })
}) })

View File

@ -10,7 +10,7 @@
<div class="w-full flex gap-2 mt-2 pb-4 relative"> <div class="w-full flex gap-2 mt-2 pb-4 relative">
<button class="btn-cyan px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="saveSprite">Save</button> <button class="btn-cyan px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="saveSprite">Save</button>
<button class="btn-red px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="deleteSprite">Delete</button> <button class="btn-red px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="deleteSprite">Delete</button>
<button class="btn bg-indigo-500 hover:bg-indigo-600 rounded text-white px-4 py-2 flex-1 sm:flex-none" type="button" @click.prevent="copySprite"> <button class="btn-indigo px-4 py-2 flex-1 sm:flex-none" type="button" @click.prevent="copySprite">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg> </svg>
@ -68,11 +68,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SocketEvent } from '@/application/enums'
import type { Sprite, SpriteAction, UUID } from '@/application/types' import type { Sprite, SpriteAction, UUID } from '@/application/types'
import { uuidv4 } from '@/application/utilities' import { uuidv4 } from '@/application/utilities'
import SpriteActionsInput from '@/components/gameMaster/assetManager/partials/sprite/partials/SpriteImagesInput.vue' import SpriteActionsInput from '@/components/gameMaster/assetManager/partials/sprite/partials/SpriteImagesInput.vue'
import SpritePreview from '@/components/gameMaster/assetManager/partials/sprite/partials/SpritePreview.vue' import SpritePreview from '@/components/gameMaster/assetManager/partials/sprite/partials/SpritePreview.vue'
import Accordion from '@/components/utilities/Accordion.vue' import Accordion from '@/components/utilities/Accordion.vue'
import { socketManager } from '@/managers/SocketManager'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
@ -97,7 +99,7 @@ if (selectedSprite.value) {
} }
function deleteSprite() { function deleteSprite() {
gameStore.connection?.emit('gm:sprite:delete', { id: selectedSprite.value?.id }, (response: boolean) => { socketManager.emit(SocketEvent.GM_SPRITE_DELETE, { id: selectedSprite.value?.id }, (response: boolean) => {
if (!response) { if (!response) {
console.error('Failed to delete sprite') console.error('Failed to delete sprite')
return return
@ -107,7 +109,7 @@ function deleteSprite() {
} }
function copySprite() { function copySprite() {
gameStore.connection?.emit('gm:sprite:copy', { id: selectedSprite.value?.id }, (response: boolean) => { socketManager.emit(SocketEvent.GM_SPRITE_COPY, { id: selectedSprite.value?.id }, (response: boolean) => {
if (!response) { if (!response) {
console.error('Failed to copy sprite') console.error('Failed to copy sprite')
return return
@ -117,7 +119,7 @@ function copySprite() {
} }
function refreshSpriteList(unsetSelectedSprite = true) { function refreshSpriteList(unsetSelectedSprite = true) {
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => { socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
assetManagerStore.setSpriteList(response) assetManagerStore.setSpriteList(response)
if (unsetSelectedSprite) { if (unsetSelectedSprite) {
@ -149,7 +151,7 @@ function saveSprite() {
}) ?? [] }) ?? []
} }
gameStore.connection?.emit('gm:sprite:update', updatedSprite, (response: boolean) => { socketManager.emit(SocketEvent.GM_SPRITE_UPDATE, updatedSprite, (response: boolean) => {
if (!response) { if (!response) {
console.error('Failed to save sprite') console.error('Failed to save sprite')
return return

View File

@ -25,7 +25,9 @@
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import config from '@/application/config'
import { SocketEvent } from '@/application/enums'
import type { Sprite } from '@/application/types' import type { Sprite } from '@/application/types'
import { socketManager } from '@/managers/SocketManager'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useVirtualList } from '@vueuse/core' import { useVirtualList } from '@vueuse/core'
@ -40,13 +42,13 @@ const hasScrolled = ref(false)
const elementToScroll = ref() const elementToScroll = ref()
function newButtonClickHandler() { function newButtonClickHandler() {
gameStore.connection?.emit('gm:sprite:create', {}, (response: boolean) => { socketManager.emit(SocketEvent.GM_SPRITE_CREATE, {}, (response: boolean) => {
if (!response) { if (!response) {
if (config.development) console.error('Failed to create new sprite') if (config.environment === 'development') console.error('Failed to create new sprite')
return return
} }
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => { socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
assetManagerStore.setSpriteList(response) assetManagerStore.setSpriteList(response)
}) })
}) })
@ -85,7 +87,7 @@ function toTop() {
} }
onMounted(() => { onMounted(() => {
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => { socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
assetManagerStore.setSpriteList(response) assetManagerStore.setSpriteList(response)
}) })
}) })

View File

@ -2,7 +2,7 @@
<div class="flex flex-wrap gap-3"> <div class="flex flex-wrap gap-3">
<div v-for="(image, index) in modelValue" :key="index" class="h-20 w-20 p-4 bg-gray-300 bg-opacity-50 rounded text-center relative group cursor-move" draggable="true" @dragstart="dragStart($event, index)" @dragover.prevent @dragenter.prevent @drop="drop($event, index)"> <div v-for="(image, index) in modelValue" :key="index" class="h-20 w-20 p-4 bg-gray-300 bg-opacity-50 rounded text-center relative group cursor-move" draggable="true" @dragstart="dragStart($event, index)" @dragover.prevent @dragenter.prevent @drop="drop($event, index)">
<img :src="image.url" class="max-w-full max-h-full object-contain pointer-events-none" alt="Uploaded image" @load="updateImageDimensions($event, index)" /> <img :src="image.url" class="max-w-full max-h-full object-contain pointer-events-none" alt="Uploaded image" @load="updateImageDimensions($event, index)" />
<div v-if="imageDimensions[index]" class="absolute bottom-1 right-1 bg-black/50 text-white text-xs px-1 py-0.5 rounded transition-opacity font-default">{{ image.dimensions.width }}x{{ image.dimensions.height }}</div> <div v-if="imageDimensions[index]" class="absolute bottom-1 right-1 bg-black/50 text-white text-xs px-1 py-0.5 rounded transition-opacity font-default">{{ imageDimensions[index].width }}x{{ imageDimensions[index].height }}</div>
<div class="absolute top-1 left-1 flex-row space-y-1"> <div class="absolute top-1 left-1 flex-row space-y-1">
<button @click.stop="deleteImage(index)" class="bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" aria-label="Delete image"> <button @click.stop="deleteImage(index)" class="bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" aria-label="Delete image">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@ -17,7 +17,7 @@
</button> </button>
</div> </div>
<Modal :is-modal-open="selectedImageIndex === index" :modal-width="300" :modal-height="210" :is-resizable="false" :bg-style="'none'" @modal:close="closeOffsetModal"> <Modal :is-modal-open="selectedImageIndex === index" :modal-width="300" :modal-height="210" :is-resizable="false" bg-style="none" @modal:close="closeOffsetModal">
<template #modalHeader> <template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">Action offset ({{ selectedImageIndex }})</h3> <h3 class="m-0 font-medium shrink-0 text-white">Action offset ({{ selectedImageIndex }})</h3>
</template> </template>

View File

@ -1,5 +1,5 @@
<template> <template>
<Modal :is-modal-open="isModalOpen" :modal-width="700" :modal-height="330" :bg-style="'none'" @modal:close="closeModal"> <Modal :is-modal-open="isModalOpen" :modal-width="700" :modal-height="330" bg-style="none" @modal:close="closeModal">
<template #modalHeader> <template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">View sprite</h3> <h3 class="m-0 font-medium shrink-0 text-white">View sprite</h3>
</template> </template>
@ -34,7 +34,7 @@
</div> </div>
<div class="flex flex-col justify-center gap-8 flex-1"> <div class="flex flex-col justify-center gap-8 flex-1">
<div class="flex flex-col"> <div class="flex flex-col">
<label class="block mb-2 text-white">Frame Rate: {{ frameRate }} FPS</label> <label class="block mb-2 text-white">Frame Rate: {{ frameRate }} FPS (Duration: {{ totalDuration }}s)</label>
<input type="range" v-model.number="localFrameRate" min="0" max="60" step="1" class="w-full accent-cyan-500" @input="updateFrameRate" /> <input type="range" v-model.number="localFrameRate" min="0" max="60" step="1" class="w-full accent-cyan-500" @input="updateFrameRate" />
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col">
@ -76,6 +76,11 @@ const localFrameRate = ref(props.frameRate)
const zoomLevel = ref(100) const zoomLevel = ref(100)
let animationInterval: number | null = null let animationInterval: number | null = null
const totalDuration = computed(() => {
if (props.frameRate <= 0) return 0
return (props.sprites.length / props.frameRate).toFixed(2)
})
const spritesWithTempOffset = computed(() => { const spritesWithTempOffset = computed(() => {
return props.sprites.map((sprite, index) => { return props.sprites.map((sprite, index) => {
if (index === props.tempOffsetIndex && props.tempOffset) { if (index === props.tempOffsetIndex && props.tempOffset) {

View File

@ -24,8 +24,10 @@
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import config from '@/application/config'
import { SocketEvent } from '@/application/enums'
import type { Tile } from '@/application/types' import type { Tile } from '@/application/types'
import ChipsInput from '@/components/forms/ChipsInput.vue' import ChipsInput from '@/components/forms/ChipsInput.vue'
import { socketManager } from '@/managers/SocketManager'
import { TileStorage } from '@/storage/storages' import { TileStorage } from '@/storage/storages'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
@ -56,7 +58,7 @@ watch(selectedTile, (tile: Tile | null) => {
}) })
async function deleteTile() { async function deleteTile() {
gameStore.connection?.emit('gm:tile:delete', { id: selectedTile.value?.id }, async (response: boolean) => { socketManager.emit(SocketEvent.GM_TILE_DELETE, { id: selectedTile.value?.id }, async (response: boolean) => {
if (!response) { if (!response) {
console.error('Failed to delete tile') console.error('Failed to delete tile')
return return
@ -67,7 +69,7 @@ async function deleteTile() {
} }
function refreshTileList(unsetSelectedTile = true) { function refreshTileList(unsetSelectedTile = true) {
gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => { socketManager.emit(SocketEvent.GM_TILE_LIST, {}, (response: Tile[]) => {
assetManagerStore.setTileList(response) assetManagerStore.setTileList(response)
if (unsetSelectedTile) { if (unsetSelectedTile) {
@ -82,7 +84,7 @@ function saveTile() {
return return
} }
gameStore.connection?.emit( socketManager.emit(
'gm:tile:update', 'gm:tile:update',
{ {
id: selectedTile.value.id, id: selectedTile.value.id,

View File

@ -29,7 +29,9 @@
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import config from '@/application/config'
import { SocketEvent } from '@/application/enums'
import type { Tile } from '@/application/types' import type { Tile } from '@/application/types'
import { socketManager } from '@/managers/SocketManager'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useVirtualList } from '@vueuse/core' import { useVirtualList } from '@vueuse/core'
@ -47,13 +49,13 @@ const elementToScroll = ref()
const handleFileUpload = (e: Event) => { const handleFileUpload = (e: Event) => {
const files = (e.target as HTMLInputElement).files const files = (e.target as HTMLInputElement).files
if (!files) return if (!files) return
gameStore.connection?.emit('gm:tile:upload', files, (response: boolean) => { socketManager.emit(SocketEvent.GM_TILE_UPLOAD, files, (response: boolean) => {
if (!response) { if (!response) {
if (config.development) console.error('Failed to upload tile') if (config.environment === 'development') console.error('Failed to upload tile')
return return
} }
gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => { socketManager.emit(SocketEvent.GM_TILE_LIST, {}, (response: Tile[]) => {
assetManagerStore.setTileList(response) assetManagerStore.setTileList(response)
}) })
}) })
@ -92,7 +94,7 @@ function toTop() {
} }
onMounted(() => { onMounted(() => {
gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => { socketManager.emit(SocketEvent.GM_TILE_LIST, {}, (response: Tile[]) => {
assetManagerStore.setTileList(response) assetManagerStore.setTileList(response)
}) })
}) })

View File

@ -1,46 +1,240 @@
<template> <template>
<MapTiles ref="mapTiles" @tileMap:create="tileMap = $event" /> <MapTiles ref="mapTiles" @createCommand="addCommand" v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer />
<PlacedMapObjects ref="mapObjects" v-if="tileMap" :tileMap="tileMap as Phaser.Tilemaps.Tilemap" /> <PlacedMapObjects ref="mapObjects" @update="updateMapObjects" @updateAndCommit="updateAndCommit" @pauseObjectTracking="pause" @resumeObjectTracking="resume" v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer />
<MapEventTiles ref="eventTiles" v-if="tileMap" :tileMap="tileMap as Phaser.Tilemaps.Tilemap" /> <MapEventTiles ref="eventTiles" @createCommand="addCommand" v-if="tileMap" :tileMap />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { MapEventTile, Map as MapT, PlacedMapObject as PlacedMapObjectT } from '@/application/types'
import MapEventTiles from '@/components/gameMaster/mapEditor/mapPartials/MapEventTiles.vue' import MapEventTiles from '@/components/gameMaster/mapEditor/mapPartials/MapEventTiles.vue'
import MapTiles from '@/components/gameMaster/mapEditor/mapPartials/MapTiles.vue' import MapTiles from '@/components/gameMaster/mapEditor/mapPartials/MapTiles.vue'
import PlacedMapObjects from '@/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.vue' import PlacedMapObjects from '@/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { cloneArray, createTileArray, createTileLayer, createTileMap, placeTiles } from '@/services/mapService'
import { TileStorage } from '@/storage/storages'
import { useManualRefHistory, useRefHistory } from '@vueuse/core'
import { useScene } from 'phavuer' import { useScene } from 'phavuer'
import { onMounted, onUnmounted, shallowRef, useTemplateRef } from 'vue' import { onBeforeUnmount, onMounted, onUnmounted, ref, shallowRef, useTemplateRef, watch } from 'vue'
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>() const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
const mapEditor = useMapEditorComposable() const tileMapLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
const mapEditor = useMapEditorComposable()
const scene = useScene() const scene = useScene()
const mapTiles = useTemplateRef('mapTiles') const mapTiles = useTemplateRef('mapTiles')
const mapObjects = useTemplateRef('mapObjects') const mapObjects = useTemplateRef('mapObjects')
const eventTiles = useTemplateRef('eventTiles') const eventTiles = useTemplateRef('eventTiles')
function handlePointer(pointer: Phaser.Input.Pointer) { //Record of commands
let commandStack: (EditorCommand | number)[] = []
let commandIndex = ref(0)
let originTiles: string[][] = []
let originEventTiles: MapEventTile[] = []
let originObjects = ref<PlacedMapObjectT[]>(mapEditor.currentMap.value?.placedMapObjects ?? [])
const { undo, redo, commit, pause, resume, canUndo, canRedo } = useRefHistory(originObjects, { deep: true, capacity: 9 })
//Command Pattern basic interface, extended to store what elements have been changed by each edit
export interface EditorCommand {
apply: (elements: any[]) => any[]
type: 'tile' | 'map_object' | 'event_tile'
operation: 'draw' | 'erase' | 'clear'
}
function applyCommands(tiles: any[], ...commands: EditorCommand[]): any[] {
let tileVersion = cloneArray(tiles)
for (let command of commands) {
tileVersion = command.apply(tileVersion)
}
return tileVersion
}
watch(
() => mapEditor.shouldClearTiles.value,
(shouldClear) => {
if (shouldClear && mapEditor.currentMap.value) {
mapTiles.value!.clearTiles()
eventTiles.value!.clearTiles()
mapEditor.currentMap.value.placedMapObjects = []
updateAndCommit(mapEditor.currentMap.value)
mapEditor.resetClearTilesFlag()
}
}
)
function update(commands: (EditorCommand | number)[]) {
if (!mapEditor.currentMap.value) return
if (commandStack.length >= 9) {
if (typeof commandStack[0] !== 'number') {
const base = commandStack.shift() as EditorCommand
if (base.operation !== 'clear') {
switch (base.type) {
case 'tile':
originTiles = base.apply(originTiles) as string[][]
break
case 'event_tile':
originEventTiles = base.apply(originEventTiles) as MapEventTile[]
break
}
} else {
commandStack.shift()
}
} else if (typeof commandStack[0] === 'number') {
commandStack.shift()
}
}
let tileCommands = commands.filter((item) => typeof item !== 'number' && item.type === 'tile') as EditorCommand[]
let eventTileCommands = commands.filter((item) => typeof item !== 'number' && item.type === 'event_tile') as EditorCommand[]
let modifiedTiles = applyCommands(originTiles, ...tileCommands)
placeTiles(tileMap.value!, tileMapLayer.value!, modifiedTiles)
let eventTiles = applyCommands(originEventTiles, ...eventTileCommands)
mapEditor.currentMap.value.tiles = modifiedTiles
mapEditor.currentMap.value.mapEventTiles = eventTiles
mapEditor.currentMap.value.placedMapObjects = originObjects.value
}
function updateMapObjects(map: MapT) {
originObjects.value = map.placedMapObjects
}
function updateAndCommit(map?: MapT) {
commandStack = commandStack.slice(0, commandIndex.value)
if (map) updateMapObjects(map)
commit()
commandStack.push(0)
commandIndex.value = commandStack.length
}
function addCommand(command: EditorCommand) {
commandStack = commandStack.slice(0, commandIndex.value)
commandStack.push(command)
commandIndex.value = commandStack.length
}
function undoEdit() {
if (commandIndex.value > 0) {
if (typeof commandStack[--commandIndex.value] === 'number' && canUndo) {
undo()
}
update(commandStack.slice(0, commandIndex.value))
}
}
function redoEdit() {
if (commandIndex.value <= 9 && commandIndex.value < commandStack.length) {
if (typeof commandStack[commandIndex.value++] === 'number' && canRedo) {
redo()
}
update(commandStack.slice(0, commandIndex.value))
}
}
function handlePointerDown(pointer: Phaser.Input.Pointer) {
if (!mapTiles.value || !mapObjects.value || !eventTiles.value) return if (!mapTiles.value || !mapObjects.value || !eventTiles.value) return
// Check if left mouse button is pressed // Check if left mouse button is pressed
if (!pointer.isDown) return if (!pointer.isDown) return
// Check if shift is pressed or if we're in move mode, this means we are moving the camera // Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey || mapEditor.tool.value === 'move') return if (pointer.event.shiftKey) return
// Check if draw mode is tile // Check if draw mode is tile
switch (mapEditor.drawMode.value) { switch (mapEditor.drawMode.value) {
case 'tile': case 'tile':
mapTiles.value.handlePointer(pointer) mapTiles.value.handlePointer(pointer)
break break
case 'object': case 'map_object':
mapObjects.value.handlePointer(pointer) mapObjects.value.handlePointer(pointer)
break break
case 'event': case 'teleport':
eventTiles.value.handlePointer(pointer)
break
case 'blocking tile':
eventTiles.value.handlePointer(pointer) eventTiles.value.handlePointer(pointer)
break break
} }
} }
function handleKeyDown(event: KeyboardEvent) {
//CTRL+Y
if (event.key === 'y' && event.ctrlKey) {
redoEdit()
}
//CTRL+Z
if (event.key === 'z' && event.ctrlKey) {
undoEdit()
}
}
function handlePointerMove(pointer: Phaser.Input.Pointer) {
if (mapEditor.inputMode.value === 'hold' && pointer.isDown) {
handlePointerDown(pointer)
}
}
function handlePointerUp(pointer: Phaser.Input.Pointer) {
switch (mapEditor.drawMode.value) {
case 'tile':
mapTiles.value!.finalizeCommand()
break
case 'map_object':
if (mapEditor.tool.value === 'pencil' || mapEditor.tool.value === 'eraser') {
resume()
updateAndCommit()
}
break
case 'teleport':
eventTiles.value!.finalizeCommand()
break
case 'blocking tile':
eventTiles.value!.finalizeCommand()
break
}
}
onMounted(async () => {
let mapValue = mapEditor.currentMap.value
if (!mapValue) return
//Clone
originTiles = cloneArray(mapValue.tiles)
originEventTiles = cloneArray(mapValue.mapEventTiles)
const tileStorage = new TileStorage()
const allTiles = await tileStorage.getAll()
const allTileIds = allTiles.map((tile) => tile.id)
tileMap.value = createTileMap(scene, mapValue)
tileMapLayer.value = createTileLayer(tileMap.value, allTileIds)
addEventListener('keydown', handleKeyDown)
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)
})
onUnmounted(() => {
if (tileMap.value) {
tileMap.value.destroyLayer('tiles')
tileMap.value.removeAllLayers()
tileMap.value.destroy()
}
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)
mapEditor.reset()
})
onBeforeUnmount(() => {
removeEventListener('keydown', handleKeyDown)
})
</script> </script>

View File

@ -3,22 +3,71 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { MapEventTileType, type MapEventTile, type Map as MapT } from '@/application/types' import { MapEventTileType, type MapEventTile, type Map as MapT, type UUID } from '@/application/types'
import { uuidv4 } from '@/application/utilities' import { uuidv4 } from '@/application/utilities'
import { getTile, tileToWorldX, tileToWorldY } from '@/composables/mapComposable' import { type EditorCommand } from '@/components/gameMaster/mapEditor/Map.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { Image, useScene } from 'phavuer' import { cloneArray, getTile, tileToWorldX, tileToWorldY } from '@/services/mapService'
import { shallowRef } from 'vue' import { Image } from 'phavuer'
const mapEditor = useMapEditorComposable() const mapEditor = useMapEditorComposable()
defineExpose({ handlePointer }) defineExpose({ handlePointer, finalizeCommand, clearTiles })
const emit = defineEmits(['createCommand'])
const props = defineProps<{ const props = defineProps<{
tileMap: Phaser.Tilemaps.Tilemap tileMap: Phaser.Tilemaps.Tilemap
}>() }>()
const tileLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>() // *** COMMAND STATE ***
let currentCommand: EventTileCommand | null = null
class EventTileCommand implements EditorCommand {
public operation: 'draw' | 'erase' | 'clear' = 'draw'
public type: 'event_tile' = 'event_tile'
public affectedTiles: MapEventTile[] = []
apply(elements: MapEventTile[]) {
let tileVersion = cloneArray(elements) as MapEventTile[]
if (this.operation === 'draw') {
tileVersion = tileVersion.concat(this.affectedTiles)
} else if (this.operation === 'erase') {
tileVersion = tileVersion.filter((v) => !this.affectedTiles.includes(v))
} else if (this.operation === 'clear') {
tileVersion = []
}
return tileVersion
}
constructor(operation: 'draw' | 'erase' | 'clear') {
this.operation = operation
}
}
function createCommandUpdate(tile?: MapEventTile | null, operation: 'draw' | 'erase' | 'clear' = 'draw') {
if (!tile) return
if (!currentCommand) {
currentCommand = new EventTileCommand(operation)
}
//If position is already in, do not proceed
for (const priorTile of currentCommand.affectedTiles) {
if (priorTile.positionX === tile.positionX && priorTile.positionY == tile.positionY) return
}
currentCommand.affectedTiles.push(tile)
}
function finalizeCommand() {
if (!currentCommand) return
emit('createCommand', currentCommand)
currentCommand = null
}
// *** HANDLERS ***
function getImageProps(tile: MapEventTile) { function getImageProps(tile: MapEventTile) {
return { return {
@ -30,10 +79,8 @@ function getImageProps(tile: MapEventTile) {
} }
function pencil(pointer: Phaser.Input.Pointer, map: MapT) { function pencil(pointer: Phaser.Input.Pointer, map: MapT) {
if (!tileLayer.value) return
// Check if there is a tile // Check if there is a tile
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY) const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
if (!tile) return if (!tile) return
// Check if event tile already exists on position // Check if event tile already exists on position
@ -41,19 +88,17 @@ function pencil(pointer: Phaser.Input.Pointer, map: MapT) {
if (existingEventTile) return if (existingEventTile) return
// If teleport, check if there is a selected map // If teleport, check if there is a selected map
if (mapEditor.drawMode.value === 'teleport' && !mapEditor.teleportSettings.value.toMapId) return if (mapEditor.drawMode.value === 'teleport' && !mapEditor.teleportSettings.value.toMap) return
const newEventTile = { const newEventTile = {
id: uuidv4(), id: uuidv4() as UUID,
mapId: map?.id,
map: map?.id,
type: mapEditor.drawMode.value === 'blocking tile' ? MapEventTileType.BLOCK : MapEventTileType.TELEPORT, type: mapEditor.drawMode.value === 'blocking tile' ? MapEventTileType.BLOCK : MapEventTileType.TELEPORT,
positionX: tile.x, positionX: tile.x,
positionY: tile.y, positionY: tile.y,
teleport: teleport:
mapEditor.drawMode.value === 'teleport' mapEditor.drawMode.value === 'teleport'
? { ? {
toMap: mapEditor.teleportSettings.value.toMapId, toMap: mapEditor.teleportSettings.value.toMap,
toPositionX: mapEditor.teleportSettings.value.toPositionX, toPositionX: mapEditor.teleportSettings.value.toPositionX,
toPositionY: mapEditor.teleportSettings.value.toPositionY, toPositionY: mapEditor.teleportSettings.value.toPositionY,
toRotation: mapEditor.teleportSettings.value.toRotation toRotation: mapEditor.teleportSettings.value.toRotation
@ -61,19 +106,28 @@ function pencil(pointer: Phaser.Input.Pointer, map: MapT) {
: undefined : undefined
} }
map!.mapEventTiles = map!.mapEventTiles.concat(newEventTile as MapEventTile) createCommandUpdate(newEventTile as MapEventTile, 'draw')
map.mapEventTiles.push(newEventTile as MapEventTile)
} }
function erase(pointer: Phaser.Input.Pointer, map: MapT) { function erase(pointer: Phaser.Input.Pointer, map: MapT) {
if (!tileLayer.value) return
// Check if there is a tile // Check if there is a tile
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY) const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
if (!tile) return if (!tile) return
// Check if event tile already exists on position // Check if event tile already exists on position
const existingEventTile = map.mapEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y) const existingEventTile = map.mapEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y)
if (!existingEventTile) return if (!existingEventTile) return
if (mapEditor.drawMode.value !== existingEventTile.type.toLowerCase()) {
if (mapEditor.drawMode.value === 'blocking tile' && existingEventTile.type === MapEventTileType.BLOCK)
null //skip this case
else return
}
createCommandUpdate(existingEventTile, 'erase')
// Remove existing event tile // Remove existing event tile
map.mapEventTiles = map.mapEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id) map.mapEventTiles = map.mapEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id)
} }
@ -82,11 +136,7 @@ function handlePointer(pointer: Phaser.Input.Pointer) {
const map = mapEditor.currentMap.value const map = mapEditor.currentMap.value
if (!map) return if (!map) return
// Check if left mouse button is pressed if (pointer.event.altKey) return
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
switch (mapEditor.tool.value) { switch (mapEditor.tool.value) {
case 'pencil': case 'pencil':
@ -97,4 +147,10 @@ function handlePointer(pointer: Phaser.Input.Pointer) {
break break
} }
} }
function clearTiles() {
if (mapEditor.currentMap.value?.mapEventTiles.length === 0) return
createCommandUpdate(null, 'clear')
finalizeCommand()
}
</script> </script>

View File

@ -1,109 +1,102 @@
<template> <template>
<Controls v-if="tileLayer" :layer="tileLayer" :depth="0" /> <Controls v-if="tileMapLayer" :layer="tileMapLayer" :depth="0" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import { type EditorCommand } from '@/components/gameMaster/mapEditor/Map.vue'
import Controls from '@/components/utilities/Controls.vue' import Controls from '@/components/utilities/Controls.vue'
import { createTileArray, getTile, placeTile, setLayerTiles } from '@/composables/mapComposable'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { TileStorage } from '@/storage/storages' import { cloneArray, createTileArray, getTile, placeTile, placeTiles } from '@/services/mapService'
import { useScene } from 'phavuer' import { onMounted, ref, watch } from 'vue'
import { onMounted, onUnmounted, shallowRef, watch } from 'vue'
import Tileset = Phaser.Tilemaps.Tileset
const emit = defineEmits(['tileMap:create'])
const scene = useScene()
const mapEditor = useMapEditorComposable() const mapEditor = useMapEditorComposable()
const tileStorage = new TileStorage()
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>() defineExpose({ handlePointer, finalizeCommand, clearTiles })
const tileLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
defineExpose({ handlePointer }) const emit = defineEmits(['createCommand'])
function createTileMap() { const props = defineProps<{
const mapData = new Phaser.Tilemaps.MapData({ tileMap: Phaser.Tilemaps.Tilemap
width: mapEditor.currentMap.value?.width, tileMapLayer: Phaser.Tilemaps.TilemapLayer
height: mapEditor.currentMap.value?.height, }>()
tileWidth: config.tile_size.width,
tileHeight: config.tile_size.height,
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
format: Phaser.Tilemaps.Formats.ARRAY_2D
})
const newTileMap = new Phaser.Tilemaps.Tilemap(scene, mapData) // *** COMMAND STATE ***
emit('tileMap:create', newTileMap)
return newTileMap let currentCommand: TileCommand | null = null
class TileCommand implements EditorCommand {
public operation: 'draw' | 'erase' | 'clear' = 'draw'
public type: 'tile' = 'tile'
public tileName: string = 'blank_tile'
public affectedTiles: number[][] = []
apply(elements: string[][]) {
let tileVersion
if (this.operation === 'clear') {
tileVersion = createTileArray(props.tileMapLayer.width, props.tileMapLayer.height, 'blank_tile')
} else {
tileVersion = cloneArray(elements) as string[][]
for (const position of this.affectedTiles) {
tileVersion[position[1]][position[0]] = this.tileName
}
}
return tileVersion
} }
async function createTileLayer(currentTileMap: Phaser.Tilemaps.Tilemap) { constructor(operation: 'draw' | 'erase' | 'clear', tileName: string) {
const tiles = await tileStorage.getAll() this.operation = operation
const tilesetImages = [] this.tileName = tileName
}
for (const tile of tiles) {
tilesetImages.push(currentTileMap.addTilesetImage(tile.id, tile.id, config.tile_size.width, config.tile_size.height, 1, 2, tilesetImages.length + 1, { x: 0, y: -config.tile_size.height }))
} }
// Add blank tile function createCommandUpdate(x: number, y: number, tileName: string, operation: 'draw' | 'erase' | 'clear') {
tilesetImages.push(currentTileMap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.width, config.tile_size.height, 1, 2, 0, { x: 0, y: -config.tile_size.height })) if (!currentCommand) {
currentCommand = new TileCommand(operation, tileName)
const layer = currentTileMap.createBlankLayer('tiles', tilesetImages as Tileset[], 0, config.tile_size.height) as Phaser.Tilemaps.TilemapLayer
layer.setDepth(0)
layer.setCullPadding(2, 2)
return layer
} }
function pencil(pointer: Phaser.Input.Pointer) { //If position is already in, do not proceed
for (const vec of currentCommand.affectedTiles) {
if (vec[0] === x && vec[1] === y) return
}
currentCommand.affectedTiles.push([x, y])
}
function finalizeCommand() {
if (!currentCommand) return
emit('createCommand', currentCommand)
currentCommand = null
}
// *** HANDLERS ***
function draw(pointer: Phaser.Input.Pointer, tileName: string) {
let map = mapEditor.currentMap.value let map = mapEditor.currentMap.value
if (!map) return if (!map) return
// Check if there is a selected tile
if (!mapEditor.selectedTile.value) return
if (!tileMap.value || !tileLayer.value) return
// Check if there is a tile // Check if there is a tile
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY) const tile = getTile(props.tileMapLayer, pointer.worldX, pointer.worldY)
if (!tile) return if (!tile) return
// Place tile // Place tile
placeTile(tileMap.value, tileLayer.value, tile.x, tile.y, mapEditor.selectedTile.value) placeTile(props.tileMap, props.tileMapLayer, tile.x, tile.y, tileName)
createCommandUpdate(tile.x, tile.y, tileName, tileName === 'blank_tile' ? 'erase' : 'draw')
// Adjust mapEditorStore.map.tiles // Adjust mapEditorStore.map.tiles
map.tiles[tile.y][tile.x] = mapEditor.selectedTile.value map.tiles[tile.y][tile.x] = tileName
}
function eraser(pointer: Phaser.Input.Pointer) {
let map = mapEditor.currentMap.value
if (!map) return
if (!tileMap.value || !tileLayer.value) return
// Check if there is a tile
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
if (!tile) return
// Place tile
placeTile(tileMap.value, tileLayer.value, tile.x, tile.y, 'blank_tile')
// Adjust mapEditorStore.map.tiles
map.tiles[tile.y][tile.x] = 'blank_tile'
} }
function paint(pointer: Phaser.Input.Pointer) { function paint(pointer: Phaser.Input.Pointer) {
if (!tileMap.value || !tileLayer.value) return let map = mapEditor.currentMap.value
if (!map) return
// Set new tileArray with selected tile // Set new tileArray with selected tile
const tileArray = createTileArray(tileMap.value.width, tileMap.value.height, mapEditor.selectedTile.value) const tileArray = createTileArray(props.tileMap.width, props.tileMap.height, mapEditor.selectedTile.value)
setLayerTiles(tileMap.value, tileLayer.value, tileArray) placeTiles(props.tileMap, props.tileMapLayer, tileArray)
// Adjust mapEditorStore.map.tiles // Adjust mapEditorStore.map.tiles
if (mapEditor.currentMap.value) { map.tiles = tileArray
mapEditor.currentMap.value.tiles = tileArray
}
} }
// When alt is pressed, and the pointer is down, select the tile that the pointer is over // When alt is pressed, and the pointer is down, select the tile that the pointer is over
@ -111,33 +104,17 @@ function tilePicker(pointer: Phaser.Input.Pointer) {
let map = mapEditor.currentMap.value let map = mapEditor.currentMap.value
if (!map) return if (!map) return
if (!tileMap.value || !tileLayer.value) return
// Check if there is a tile // Check if there is a tile
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY) const tile = getTile(props.tileMapLayer, pointer.worldX, pointer.worldY)
if (!tile) return if (!tile) return
// Select the tile // Select the tile
mapEditor.setSelectedTile(map.tiles[tile.y][tile.x]) mapEditor.setSelectedTile(map.tiles[tile.y][tile.x])
} }
watch(
() => mapEditor.shouldClearTiles,
(shouldClear) => {
if (shouldClear && mapEditor.currentMap.value && tileMap.value && tileLayer.value) {
const blankTiles = createTileArray(tileLayer.value.width, tileLayer.value.height, 'blank_tile')
setLayerTiles(tileMap.value, tileLayer.value, blankTiles)
mapEditor.currentMap.value.tiles = blankTiles
mapEditor.resetClearTilesFlag()
}
}
)
function handlePointer(pointer: Phaser.Input.Pointer) { function handlePointer(pointer: Phaser.Input.Pointer) {
if (!tileMap.value || !tileLayer.value) return
// Check if left mouse button is pressed // Check if left mouse button is pressed
if (!pointer.isDown) return if (!pointer.isDown && pointer.button === 0) return
// Check if shift is not pressed, this means we are moving the camera // Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return if (pointer.event.shiftKey) return
@ -151,10 +128,10 @@ function handlePointer(pointer: Phaser.Input.Pointer) {
// Check if draw mode is tile // Check if draw mode is tile
switch (mapEditor.tool.value) { switch (mapEditor.tool.value) {
case 'pencil': case 'pencil':
pencil(pointer) draw(pointer, mapEditor.selectedTile.value!)
break break
case 'eraser': case 'eraser':
eraser(pointer) draw(pointer, 'blank_tile')
break break
case 'paint': case 'paint':
paint(pointer) paint(pointer)
@ -162,33 +139,19 @@ function handlePointer(pointer: Phaser.Input.Pointer) {
} }
} }
// *** LIFECYCLE ***
function clearTiles() {
const tileArray = createTileArray(props.tileMap.width, props.tileMap.height, 'blank_tile')
placeTiles(props.tileMap, props.tileMapLayer, tileArray)
createCommandUpdate(0, 0, 'blank_tile', 'clear')
finalizeCommand()
}
onMounted(async () => { onMounted(async () => {
if (!mapEditor.currentMap.value) return if (!mapEditor.currentMap.value) return
const mapState = mapEditor.currentMap.value
tileMap.value = createTileMap() placeTiles(props.tileMap, props.tileMapLayer, mapState.tiles)
tileLayer.value = await createTileLayer(tileMap.value)
// First fill the entire map with blank tiles using current map dimensions
const blankTiles = createTileArray(mapEditor.currentMap.value.width, mapEditor.currentMap.value.height, 'blank_tile')
// Then overlay the map tiles, but only within the current map dimensions
const mapTiles = mapEditor.currentMap.value.tiles
for (let y = 0; y < mapEditor.currentMap.value.height; y++) {
for (let x = 0; x < mapEditor.currentMap.value.width; x++) {
if (mapTiles[y] && mapTiles[y][x] !== undefined) {
blankTiles[y][x] = mapTiles[y][x]
}
}
}
setLayerTiles(tileMap.value, tileLayer.value, blankTiles)
})
onUnmounted(() => {
if (tileMap.value) {
tileMap.value.destroyLayer('tiles')
tileMap.value.removeAllLayers()
tileMap.value.destroy()
}
}) })
</script> </script>

View File

@ -1,45 +0,0 @@
<template>
<Image v-if="gameStore.isTextureLoaded(props.placedMapObject.mapObject.id)" v-bind="imageProps" />
</template>
<script setup lang="ts">
import type { PlacedMapObject, TextureData } from '@/application/types'
import { loadTexture } from '@/composables/gameComposable'
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/mapComposable'
import { useGameStore } from '@/stores/gameStore'
import { Image, useScene } from 'phavuer'
import { computed } from 'vue'
const props = defineProps<{
tileMap: Phaser.Tilemaps.Tilemap
placedMapObject: PlacedMapObject
selectedPlacedMapObject: PlacedMapObject | null
movingPlacedMapObject: PlacedMapObject | null
}>()
const gameStore = useGameStore()
const scene = useScene()
const imageProps = computed(() => ({
alpha: props.movingPlacedMapObject?.id === props.placedMapObject.id ? 0.5 : 1,
tint: props.selectedPlacedMapObject?.id === props.placedMapObject.id ? 0x00ff00 : 0xffffff,
depth: calculateIsometricDepth(props.placedMapObject.positionX, props.placedMapObject.positionY, props.placedMapObject.mapObject.frameWidth, props.placedMapObject.mapObject.frameHeight),
x: tileToWorldX(props.tileMap, props.placedMapObject.positionX, props.placedMapObject.positionY),
y: tileToWorldY(props.tileMap, props.placedMapObject.positionX, props.placedMapObject.positionY),
flipX: props.placedMapObject.isRotated,
texture: props.placedMapObject.mapObject.id,
originY: Number(props.placedMapObject.mapObject.originX),
originX: Number(props.placedMapObject.mapObject.originY)
}))
loadTexture(scene, {
key: props.placedMapObject.mapObject.id,
data: '/textures/map_objects/' + props.placedMapObject.mapObject.id + '.png',
group: 'map_objects',
updatedAt: props.placedMapObject.mapObject.updatedAt,
frameWidth: props.placedMapObject.mapObject.frameWidth,
frameHeight: props.placedMapObject.mapObject.frameHeight
} as TextureData).catch((error) => {
console.error('Error loading texture:', error)
})
</script>

View File

@ -1,112 +1,172 @@
<template> <template>
<SelectedPlacedMapObjectComponent v-if="selectedPlacedMapObject" :placedMapObject="selectedPlacedMapObject" @move="moveMapObject" @rotate="rotatePlacedMapObject" @delete="deletePlacedMapObject" /> <PlacedMapObject
<PlacedMapObject v-for="placedMapObject in mapEditor.currentMap.value?.placedMapObjects" :tileMap="tileMap" :placedMapObject :selectedPlacedMapObject :movingPlacedMapObject @pointerup="clickPlacedMapObject(placedMapObject)" /> v-if="mapEditor.tool.value === 'pencil' && mapEditor.drawMode.value === 'map_object' && mapEditor.isPlacedMapObjectPreviewEnabled.value && mapEditor.selectedMapObject.value && previewPlacedMapObject"
:tileMap
:tileMapLayer
:key="previewPlacedMapObject?.id"
:placedMapObject="previewPlacedMapObject as PlacedMapObjectT"
/>
<SelectedPlacedMapObjectComponent v-if="mapEditor.selectedPlacedObject.value" :key="mapEditor.selectedPlacedObject.value.id" :map :placedMapObject="mapEditor.selectedPlacedObject.value" @move="moveMapObject" @rotate="rotatePlacedMapObject" @delete="deletePlacedMapObject" />
<PlacedMapObject v-for="placedMapObject in mapEditor.currentMap.value?.placedMapObjects" :tileMap :tileMapLayer :placedMapObject @pointerdown="clickPlacedMapObject(placedMapObject)" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { Map as MapT, PlacedMapObject as PlacedMapObjectT } from '@/application/types' import type { MapObject, Map as MapT, PlacedMapObject as PlacedMapObjectT } from '@/application/types'
import { uuidv4 } from '@/application/utilities' import { uuidv4 } from '@/application/utilities'
import PlacedMapObject from '@/components/gameMaster/mapEditor/mapPartials/PlacedMapObject.vue' import PlacedMapObject from '@/components/game/map/partials/PlacedMapObject.vue'
import SelectedPlacedMapObjectComponent from '@/components/gameMaster/mapEditor/partials/SelectedPlacedMapObject.vue' import SelectedPlacedMapObjectComponent from '@/components/gameMaster/mapEditor/partials/SelectedPlacedMapObject.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { getTile } from '@/services/mapService'
import { useScene } from 'phavuer' import { useScene } from 'phavuer'
import { ref, watch } from 'vue' import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import Tilemap = Phaser.Tilemaps.Tilemap
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
const scene = useScene() const scene = useScene()
const mapEditor = useMapEditorComposable() const mapEditor = useMapEditorComposable()
const selectedPlacedMapObject = ref<PlacedMapObjectT | null>(null) const map = computed(() => mapEditor.currentMap.value!)
const movingPlacedMapObject = ref<PlacedMapObjectT | null>(null)
const props = defineProps<{ const emit = defineEmits<{ (e: 'update', map: MapT): void; (e: 'updateAndCommit', map: MapT): void; (e: 'pauseObjectTracking'): void; (e: 'resumeObjectTracking'): void }>()
tileMap: Phaser.Tilemaps.Tilemap
}>()
defineExpose({ handlePointer }) defineExpose({ handlePointer })
const props = defineProps<{
tileMap: Tilemap
tileMapLayer: TilemapLayer
}>()
const previewPosition = ref({ x: 0, y: 0 })
const previewPlacedMapObject = computed(() => ({
id: mapEditor.selectedMapObject.value!.id,
mapObject: mapEditor.selectedMapObject.value!.id,
isRotated: false,
positionX: previewPosition.value.x,
positionY: previewPosition.value.y
}))
function updatePreviewPosition(pointer: Phaser.Input.Pointer) {
const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
if (!tile || (previewPosition.value.x === tile.x && previewPosition.value.y === tile.y)) return
previewPosition.value = { x: tile.x, y: tile.y }
}
function pencil(pointer: Phaser.Input.Pointer, map: MapT) { function pencil(pointer: Phaser.Input.Pointer, map: MapT) {
emit('pauseObjectTracking')
const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if object already exists on position // Check if object already exists on position
const existingPlacedMapObject = findInMap(pointer, map) const existingPlacedMapObject = findObjectByPointer(pointer, mapEditor.currentMap.value!)
if (existingPlacedMapObject) return if (existingPlacedMapObject) return
if (!mapEditor.selectedMapObject.value) return
const newPlacedMapObject: PlacedMapObjectT = { const newPlacedMapObject: PlacedMapObjectT = {
id: uuidv4(), id: uuidv4(),
depth: 0, mapObject: mapEditor.selectedMapObject.value.id,
map: map,
mapObject: mapEditor.selectedMapObject.value!,
isRotated: false, isRotated: false,
positionX: pointer.x, positionX: tile.x,
positionY: pointer.y positionY: tile.y
} }
// Add new object to mapObjects // Add new object to mapObjects
map.placedMapObjects.concat(newPlacedMapObject) mapEditor.selectedPlacedObject.value = newPlacedMapObject
map.placedMapObjects.push(newPlacedMapObject)
emit('update', map)
} }
function eraser(pointer: Phaser.Input.Pointer, map: MapT) { function eraser(pointer: Phaser.Input.Pointer, map: MapT) {
emit('pauseObjectTracking')
// Check if object already exists on position // Check if object already exists on position
const existingPlacedMapObject = findInMap(pointer, map) const existingPlacedMapObject = findObjectByPointer(pointer, map)
if (!existingPlacedMapObject) return if (!existingPlacedMapObject) return
// Remove existing object // Remove existing object
map.placedMapObjects = map.placedMapObjects.filter((placedMapObject) => placedMapObject.id !== existingPlacedMapObject.id) map.placedMapObjects = map.placedMapObjects.filter((placedMapObject) => placedMapObject.id !== existingPlacedMapObject.id)
emit('update', map)
} }
function findInMap(pointer: Phaser.Input.Pointer, map: MapT) { function findObjectByPointer(pointer: Phaser.Input.Pointer, map: MapT): PlacedMapObjectT | undefined {
return map.placedMapObjects.find((placedMapObject) => placedMapObject.positionX === pointer.worldX && placedMapObject.positionY === pointer.worldY) const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
if (!tile) return undefined
return map.placedMapObjects.find((placedMapObject) => placedMapObject.positionX === tile.x && placedMapObject.positionY === tile.y)
} }
function objectPicker(pointer: Phaser.Input.Pointer, map: MapT) { function objectPicker(pointer: Phaser.Input.Pointer, map: MapT) {
// Check if object already exists on position // Check if object already exists on position
const existingPlacedMapObject = findInMap(pointer, map) const existingPlacedMapObject = findObjectByPointer(pointer, map)
if (!existingPlacedMapObject) return if (!existingPlacedMapObject) return
// Select the object // Select the object
mapEditor.setSelectedMapObject(existingPlacedMapObject.mapObject) mapEditor.setSelectedMapObject(existingPlacedMapObject.mapObject as MapObject)
} }
function moveMapObject(id: string, map: MapT) { function moveMapObject(id: string, map: MapT) {
movingPlacedMapObject.value = map.placedMapObjects.find((object) => object.id === id) as PlacedMapObjectT mapEditor.movingPlacedObject.value = map.placedMapObjects.find((object) => object.id === id) as PlacedMapObjectT
emit('pauseObjectTracking')
function handlePointerMove(pointer: Phaser.Input.Pointer) { function handlePointerMove(pointer: Phaser.Input.Pointer) {
if (!movingPlacedMapObject.value) return if (!mapEditor.movingPlacedObject.value) return
const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
if (!tile) return
movingPlacedMapObject.value.positionX = pointer.worldX mapEditor.movingPlacedObject.value.positionX = tile.x
movingPlacedMapObject.value.positionY = pointer.worldY mapEditor.movingPlacedObject.value.positionY = tile.y
} }
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove) scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
function handlePointerUp() {
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
movingPlacedMapObject.value = null
}
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp) scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
function handlePointerUp(pointer: Phaser.Input.Pointer) {
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
if (!tile) return
map.placedMapObjects.map((placed) => {
if (placed.id === id) {
placed.positionX = tile.x
placed.positionY = tile.y
}
})
mapEditor.movingPlacedObject.value = null
scene.input.off(Phaser.Input.Events.POINTER_UP, handlePointerUp)
emit('resumeObjectTracking')
emit('updateAndCommit', map)
}
} }
function rotatePlacedMapObject(id: string, map: MapT) { function rotatePlacedMapObject(id: string, map: MapT) {
map.placedMapObjects = map.placedMapObjects.map((placedMapObject) => { map.placedMapObjects.map((placed) => {
if (placedMapObject.id === id) { if (placed.id === id) {
return { placed.isRotated = !placed.isRotated
...placedMapObject,
isRotated: !placedMapObject.isRotated
} }
}
return placedMapObject
}) })
emit('updateAndCommit', map)
} }
function deletePlacedMapObject(id: string, map: MapT) { function deletePlacedMapObject(id: string, map: MapT) {
map.placedMapObjects = map.placedMapObjects.filter((object) => object.id !== id) map.placedMapObjects = map.placedMapObjects.filter((object) => object.id !== id)
selectedPlacedMapObject.value = null mapEditor.selectedPlacedObject.value = null
emit('updateAndCommit', map)
} }
function clickPlacedMapObject(placedMapObject: PlacedMapObjectT) { function clickPlacedMapObject(placedMapObject: PlacedMapObjectT) {
selectedPlacedMapObject.value = placedMapObject mapEditor.selectedPlacedObject.value = placedMapObject
// If alt is pressed, select the object // If alt is pressed, select the object
if (scene.input.activePointer.event.altKey) { if (scene.input.activePointer.event.altKey) {
mapEditor.setSelectedMapObject(placedMapObject.mapObject) mapEditor.setSelectedMapObject(placedMapObject.mapObject as MapObject)
} }
} }
@ -114,21 +174,13 @@ function handlePointer(pointer: Phaser.Input.Pointer) {
const map = mapEditor.currentMap.value const map = mapEditor.currentMap.value
if (!map) return if (!map) return
if (mapEditor.drawMode.value !== 'map_object') return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if alt is pressed, this means we are selecting the object // Check if alt is pressed, this means we are selecting the object
if (pointer.event.altKey) return if (pointer.event.altKey) return
// Check if tool is pencil // Check if tool is pencil
switch (mapEditor.tool.value) { switch (mapEditor.tool.value) {
case 'pencil': case 'pencil':
if (mapEditor.selectedMapObject.value) pencil(pointer, map) pencil(pointer, map)
break break
case 'eraser': case 'eraser':
eraser(pointer, map) eraser(pointer, map)
@ -139,43 +191,11 @@ function handlePointer(pointer: Phaser.Input.Pointer) {
} }
} }
// watch mapEditorStore.mapObjectList and update originX and originY of objects in mapObjects onMounted(() => {
watch( scene.input.on(Phaser.Input.Events.POINTER_MOVE, updatePreviewPosition)
() => mapEditor.currentMap.value,
() => {
const map = mapEditor.currentMap.value
if (!map) return
const updatedMapObjects = map.placedMapObjects.map((mapObject) => {
const updatedMapObject = map.placedMapObjects.find((obj) => obj.id === mapObject.mapObject.id)
if (updatedMapObject) {
return {
...mapObject,
mapObject: {
...mapObject.mapObject,
originX: updatedMapObject.positionX,
originY: updatedMapObject.positionY
}
}
}
return mapObject
}) })
// Update the map with the new mapObjects onUnmounted(() => {
map.placedMapObjects = [...map.placedMapObjects, ...updatedMapObjects] scene.input.off(Phaser.Input.Events.POINTER_MOVE, updatePreviewPosition)
// Update mapObject if it's set
if (mapEditor.selectedMapObject.value) {
const updatedMapObject = map.placedMapObjects.find((obj) => obj.id === mapEditor.selectedMapObject.value?.id)
if (updatedMapObject) {
mapEditor.setSelectedMapObject({
...mapEditor.selectedMapObject.value,
originX: updatedMapObject.positionX,
originY: updatedMapObject.positionY
}) })
}
}
}
// { deep: true }
)
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<Modal ref="modalRef" :modal-width="300" :modal-height="420" :is-resizable="false" :bg-style="'none'"> <Modal ref="modalRef" :modal-width="300" :modal-height="420" :is-resizable="false" bg-style="none">
<template #modalHeader> <template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">Create new map</h3> <h3 class="m-0 font-medium shrink-0 text-white">Create new map</h3>
</template> </template>
@ -35,9 +35,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SocketEvent } from '@/application/enums'
import type { Map } from '@/application/types' import type { Map } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { socketManager } from '@/managers/SocketManager'
import { MapStorage } from '@/storage/storages' import { MapStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { ref, useTemplateRef } from 'vue' import { ref, useTemplateRef } from 'vue'
@ -56,12 +58,8 @@ const pvp = ref(false)
defineExpose({ open: () => modalRef.value?.open() }) defineExpose({ open: () => modalRef.value?.open() })
async function submit() { async function submit() {
gameStore.connection?.emit('gm:map:create', { name: name.value, width: width.value, height: height.value }, async (response: Map | false) => { socketManager.emit(SocketEvent.GM_MAP_CREATE, { name: name.value, width: width.value, height: height.value }, async (response: Map | false) => {
if (!response) { if (!response) {
gameStore.addNotification({
title: 'Error',
message: 'Failed to create map.'
})
return return
} }
@ -72,8 +70,6 @@ async function submit() {
pvp.value = false pvp.value = false
// Add map to storage // Add map to storage
console.log(response)
await mapStorage.add(response) await mapStorage.add(response)
// Let list know to fetch new maps // Let list know to fetch new maps

View File

@ -1,5 +1,5 @@
<template> <template>
<Modal ref="modalRef" :is-resizable="false" :modal-width="300" :modal-height="360" :bg-style="'none'"> <Modal ref="modalRef" :is-resizable="false" :modal-width="300" :modal-height="360" bg-style="none">
<template #modalHeader> <template #modalHeader>
<h3 class="text-lg text-white">Maps</h3> <h3 class="text-lg text-white">Maps</h3>
</template> </template>
@ -29,13 +29,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { Map, UUID } from '@/application/types' import { SocketEvent } from '@/application/enums'
import type { Map } from '@/application/types'
import CreateMap from '@/components/gameMaster/mapEditor/partials/CreateMap.vue' import CreateMap from '@/components/gameMaster/mapEditor/partials/CreateMap.vue'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { socketManager } from '@/managers/SocketManager'
import { MapStorage } from '@/storage/storages' import { MapStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { onMounted, ref, useTemplateRef } from 'vue' import { onMounted, ref, useTemplateRef } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
@ -60,15 +61,15 @@ async function fetchMaps() {
mapList.value = await mapStorage.getAll() mapList.value = await mapStorage.getAll()
} }
function loadMap(id: UUID) { function loadMap(id: string) {
gameStore.connection?.emit('gm:map:request', { mapId: id }, (response: Map) => { socketManager.emit(SocketEvent.GM_MAP_REQUEST, { mapId: id }, (response: Map) => {
mapEditor.loadMap(response) mapEditor.loadMap(response)
}) })
modalRef.value?.close() modalRef.value?.close()
} }
async function deleteMap(id: UUID) { async function deleteMap(id: string) {
gameStore.connection?.emit('gm:map:delete', { mapId: id }, async (response: boolean) => { socketManager.emit(SocketEvent.GM_MAP_DELETE, { mapId: id }, async (response: boolean) => {
if (!response) { if (!response) {
gameStore.addNotification({ gameStore.addNotification({
title: 'Error', title: 'Error',

View File

@ -1,25 +1,24 @@
<template> <template>
<div class="absolute border-0 border-l-2 border-solid border-gray-500 w-1/4 min-w-80 flex flex-col top-0 right-0 z-10 h-dvh bg-gray-800 z-20" v-if="isOpen"> <div class="absolute border-0 border-l-2 border-solid border-gray-500 w-1/4 min-w-80 flex flex-col top-0 right-0 z-10 h-dvh bg-gray-800" v-if="mapEditor.tool.value === 'pencil' && mapEditor.drawMode.value === 'map_object'">
<div class="relative z-10 p-2.5 border-solid border-0 border-b border-gray-500"> <div class="flex flex-col gap-2.5 p-2.5">
<h3 class="text-lg text-white">Map objects</h3> <div class="flex justify-between items-center">
</div> <div class="flex-grow">
<div class="overflow-hidden grow relative"> <div class="relative flex">
<div class="absolute w-full h-full top-0 left-0"> <img src="/assets/icons/mapEditor/search.svg" class="w-4 h-4 py-0.5 absolute top-1/2 -translate-y-1/2 left-2.5" alt="Search icon" />
<div class="relative z-10 h-full"> <label class="mb-1.5 font-titles hidden" for="search">Search</label>
<div class="flex pt-4 pl-4"> <input @mousedown.stop class="!pl-7 input-field w-full" type="text" name="search" placeholder="Search" v-model="searchQuery" />
<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-field" type="text" name="search" placeholder="Search" v-model="searchQuery" />
</div> </div>
</div> </div>
<img src="/assets/icons/mapEditor/dropdown-chevron.svg" class="w-12 h-12 ml-2 cursor-pointer hover:opacity-80 -rotate-90" alt="Close" @click="mapEditor.setTool('move')" />
</div> </div>
<div class="flex flex-col h-[calc(100%_-_170px)] p-4 pb-24"> <div class="flex">
<div class="mb-4 flex flex-wrap gap-2"> <select class="input-field w-full" name="lists" v-model="mapEditor.drawMode.value" @change="(event: any) => mapEditor.setDrawMode(event.target.value)">
<button v-for="tag in uniqueTags" :key="tag" @click="toggleTag(tag)" class="btn-cyan" :class="{ 'opacity-50': !selectedTags.includes(tag) }"> <option value="tile">Tiles</option>
{{ tag }} <option value="map_object">Objects</option>
</button> </select>
</div> </div>
</div>
<div class="h-full overflow-auto relative border-0 border-t border-solid border-gray-500 p-2.5">
<div class="h-full overflow-auto"> <div class="h-full overflow-auto">
<div class="flex justify-between flex-wrap gap-2.5 items-center"> <div class="flex justify-between flex-wrap gap-2.5 items-center">
<div v-for="(mapObject, index) in filteredMapObjects" :key="index" class="max-w-1/4 inline-block"> <div v-for="(mapObject, index) in filteredMapObjects" :key="index" class="max-w-1/4 inline-block">
@ -38,7 +37,10 @@
</div> </div>
</div> </div>
</div> </div>
</div> <div class="flex flex-col h-40 gap-2.5 p-3.5 border-t border-0 border-solid border-gray-500">
<span>Tags:</span>
<div class="flex grow items-center flex-wrap gap-1.5 overflow-auto">
<span class="m-auto">No tags selected</span>
</div> </div>
</div> </div>
</div> </div>
@ -52,13 +54,6 @@ import { MapObjectStorage } from '@/storage/storages'
import { liveQuery } from 'dexie' import { liveQuery } from 'dexie'
import { computed, onMounted, onUnmounted, ref } from 'vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
defineExpose({
open: () => (isOpen.value = true),
close: () => (isOpen.value = false),
toggle: () => (isOpen.value = !isOpen.value)
})
const isOpen = ref(false)
const mapObjectStorage = new MapObjectStorage() const mapObjectStorage = new MapObjectStorage()
const mapEditor = useMapEditorComposable() const mapEditor = useMapEditorComposable()
const searchQuery = ref('') const searchQuery = ref('')
@ -70,14 +65,6 @@ const uniqueTags = computed(() => {
return Array.from(new Set(allTags)) return Array.from(new Set(allTags))
}) })
const filteredMapObjects = computed(() => {
return mapObjectList.value.filter((object) => {
const matchesSearch = !searchQuery.value || object.name.toLowerCase().includes(searchQuery.value.toLowerCase())
const matchesTags = selectedTags.value.length === 0 || (object.tags && selectedTags.value.some((tag) => object.tags.includes(tag)))
return matchesSearch && matchesTags
})
})
const toggleTag = (tag: string) => { const toggleTag = (tag: string) => {
if (selectedTags.value.includes(tag)) { if (selectedTags.value.includes(tag)) {
selectedTags.value = selectedTags.value.filter((t) => t !== tag) selectedTags.value = selectedTags.value.filter((t) => t !== tag)
@ -86,6 +73,14 @@ const toggleTag = (tag: string) => {
} }
} }
const filteredMapObjects = computed(() => {
return mapObjectList.value.filter((object) => {
const matchesSearch = !searchQuery.value || object.name.toLowerCase().includes(searchQuery.value.toLowerCase())
const matchesTags = selectedTags.value.length === 0 || (object.tags && selectedTags.value.some((tag) => object.tags.includes(tag)))
return matchesSearch && matchesTags
})
})
let subscription: any = null let subscription: any = null
onMounted(() => { onMounted(() => {
@ -94,14 +89,13 @@ onMounted(() => {
mapObjectList.value = result mapObjectList.value = result
}, },
error: (error) => { error: (error) => {
console.error('Failed to fetch objects:', error) console.error('Failed to fetch tiles:', error)
} }
}) })
}) })
onUnmounted(() => { onUnmounted(() => {
if (subscription) { if (!subscription) return
subscription.unsubscribe() subscription.unsubscribe()
}
}) })
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<Modal ref="modalRef" :modal-width="600" :modal-height="430" :bg-style="'none'"> <Modal ref="modalRef" :modal-width="600" :modal-height="430" bg-style="none">
<template #modalHeader> <template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">Map settings</h3> <h3 class="m-0 font-medium shrink-0 text-white">Map settings</h3>
</template> </template>
@ -14,22 +14,19 @@
<div class="gap-2.5 flex flex-wrap mt-4"> <div class="gap-2.5 flex flex-wrap mt-4">
<div class="form-field-full"> <div class="form-field-full">
<label for="name">Name</label> <label for="name">Name</label>
<input class="input-field" v-model="name" name="name" id="name" /> <input class="input-field" v-model="name" @input="updateValue" name="name" id="name" />
</div> </div>
<div class="form-field-half"> <div class="form-field-half">
<label for="width">Width</label> <label for="width">Width</label>
<input class="input-field" v-model="width" name="width" id="width" type="number" /> <input class="input-field" v-model="width" @input="updateValue" name="width" id="width" type="number" />
</div> </div>
<div class="form-field-half"> <div class="form-field-half">
<label for="height">Height</label> <label for="height">Height</label>
<input class="input-field" v-model="height" name="height" id="height" type="number" /> <input class="input-field" v-model="height" @input="updateValue" name="height" id="height" type="number" />
</div> </div>
<div class="form-field-full"> <div>
<label for="pvp">PVP enabled</label> <label class="mr-4" for="pvp">PVP enabled</label>
<select v-model="pvp" class="input-field" name="pvp" id="pvp"> <input type="checkbox" v-model="pvp" @input="updateValue" class="input-field scale-125" name="pvp" id="pvp" />
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>
</div> </div>
</div> </div>
</form> </form>
@ -51,15 +48,15 @@ import type { UUID } from '@/application/types'
import { uuidv4 } from '@/application/utilities' import { uuidv4 } from '@/application/utilities'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { ref, useTemplateRef, watch } from 'vue' import { onMounted, ref, useTemplateRef, watch } from 'vue'
const mapEditor = useMapEditorComposable() const mapEditor = useMapEditorComposable()
const screen = ref('settings') const screen = ref('settings')
const name = ref(mapEditor.currentMap.value?.name) const name = ref<string | undefined>('Map')
const width = ref(mapEditor.currentMap.value?.width) const width = ref<number>(0)
const height = ref(mapEditor.currentMap.value?.height) const height = ref<number>(0)
const pvp = ref(mapEditor.currentMap.value?.pvp) const pvp = ref<boolean>(false)
const mapEffects = ref(mapEditor.currentMap.value?.mapEffects || []) const mapEffects = ref(mapEditor.currentMap.value?.mapEffects || [])
const modalRef = useTemplateRef('modalRef') const modalRef = useTemplateRef('modalRef')
@ -67,36 +64,35 @@ defineExpose({
open: () => modalRef.value?.open() open: () => modalRef.value?.open()
}) })
watch(name, (value) => { function updateValue(event: Event) {
mapEditor.updateProperty('name', value!) let ev = event.target as HTMLInputElement
}) const value = ev.name === 'pvp' ? (ev.checked ? 1 : 0) : ev.value
mapEditor.updateProperty(ev.name as 'name' | 'width' | 'height' | 'pvp' | 'mapEffects', value)
}
watch(width, (value) => { watch(
mapEditor.updateProperty('width', value!) () => mapEditor.currentMap.value,
}) (map) => {
if (!map) return
watch(height, (value) => { name.value = map.name
mapEditor.updateProperty('height', value!) width.value = map.width
}) height.value = map.height
pvp.value = map.pvp
watch(pvp, (value) => { mapEffects.value = map.mapEffects
mapEditor.updateProperty('pvp', value!) }
}) )
watch(mapEffects, (value) => {
mapEditor.updateProperty('mapEffects', value!)
})
const addEffect = () => { const addEffect = () => {
mapEffects.value.push({ mapEffects.value.push({
id: uuidv4() as UUID, // Simple unique id generation id: uuidv4(),
map: mapEditor.currentMap.value!,
effect: '', effect: '',
strength: 1 strength: 1
}) })
mapEditor.updateProperty('mapEffects', mapEffects.value)
} }
const removeEffect = (index: number) => { const removeEffect = (index: number) => {
mapEffects.value.splice(index, 1) mapEffects.value.splice(index, 1)
mapEditor.updateProperty('mapEffects', mapEffects.value)
} }
</script> </script>

View File

@ -1,33 +1,115 @@
<template> <template>
<div class="flex flex-col items-center py-5 px-3 fixed bottom-14 right-0"> <div class="flex flex-col items-center px-5 py-1 fixed bottom-20 left-0 z-20">
<div class="self-end mt-2 flex gap-2"> <div class="flex h-10 gap-2">
<button @mousedown.stop @click="handleDelete" class="btn-red py-1.5 px-4"> <button @mousedown.stop @click="handleDelete" class="btn-red !py-3 px-4">
<img src="/assets/icons/trashcan.svg" class="w-4 h-4" alt="Delete" /> <img src="/assets/icons/trashcan.svg" class="w-4 h-4" alt="Delete" />
</button> </button>
<button @mousedown.stop @click="showMapObjectSettings = !showMapObjectSettings" class="btn-indigo !py-3 px-4">
<img src="/assets/icons/mapEditor/gear.svg" class="w-4 h-4 invert" alt="Delete" />
</button>
<button @mousedown.stop @click="handleRotate" class="btn-cyan py-1.5 px-4">Rotate</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> <button @mousedown.stop @click="handleMove" class="btn-cyan py-1.5 px-4 min-w-24">Move</button>
</div> </div>
</div> </div>
<Modal :is-modal-open="showMapObjectSettings" @modal:close="showMapObjectSettings = false" :modal-height="320" bg-style="none">
<template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">Map object 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-full">
<label for="name">Name</label>
<input class="input-field" v-model="mapObjectName" name="name" id="name" />
</div>
<div class="form-field-half">
<label for="originX">Origin X</label>
<input class="input-field" v-model="mapObjectOriginX" name="originX" id="originX" type="number" min="0.0" step="0.01" />
</div>
<div class="form-field-half">
<label for="originY">Origin Y</label>
<input class="input-field" v-model="mapObjectOriginY" name="originY" id="originY" type="number" min="0.0" step="0.01" />
</div>
</div>
<button class="btn-cyan px-4 py-1.5 min-w-24" @click="handleUpdate">Save</button>
</form>
</div>
</template>
</Modal>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { PlacedMapObject } from '@/application/types' import { SocketEvent } from '@/application/enums'
import type { MapObject, Map as MapT, PlacedMapObject } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { socketManager } from '@/managers/SocketManager'
import { MapObjectStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore'
import { onMounted, ref } from 'vue'
const props = defineProps<{ const props = defineProps<{
placedMapObject: PlacedMapObject placedMapObject: PlacedMapObject
map: MapT
}>() }>()
const emit = defineEmits(['move', 'rotate', 'delete']) const emit = defineEmits(['move', 'rotate', 'delete'])
const gameStore = useGameStore()
const mapEditor = useMapEditorComposable()
const mapObjectStorage = new MapObjectStorage()
const mapObject = ref<MapObject | null>(null)
const showMapObjectSettings = ref(false)
const mapObjectName = ref('')
const mapObjectOriginX = ref(0)
const mapObjectOriginY = ref(0)
const handleMove = () => { const handleMove = () => {
emit('move', props.placedMapObject.id) emit('move', props.placedMapObject.id, props.map)
} }
const handleRotate = () => { const handleRotate = () => {
emit('rotate', props.placedMapObject.id) emit('rotate', props.placedMapObject.id, props.map)
} }
const handleDelete = () => { const handleDelete = () => {
emit('delete', props.placedMapObject.id) emit('delete', props.placedMapObject.id, props.map)
} }
async function handleUpdate() {
if (!mapObject.value) return
socketManager.emit(
SocketEvent.GM_MAPOBJECT_UPDATE,
{
id: props.placedMapObject.mapObject as string,
name: mapObjectName.value,
originX: mapObjectOriginX.value,
originY: mapObjectOriginY.value
},
async (response: boolean) => {
if (!response) return
await mapObjectStorage.update(mapObject.value!.id, {
name: mapObjectName.value,
originX: mapObjectOriginX.value,
originY: mapObjectOriginY.value
})
mapEditor.triggerMapObjectRefresh()
}
)
}
onMounted(async () => {
if (!props.placedMapObject.mapObject) return
mapObject.value = await mapObjectStorage.getById(props.placedMapObject.mapObject as string)
if (!mapObject.value) return
mapObjectName.value = mapObject.value.name
mapObjectOriginX.value = mapObject.value.originX
mapObjectOriginY.value = mapObject.value.originY
})
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<Modal ref="modalRef" @modal:close="() => mapEditorStore.setTool('move')" :modal-width="300" :modal-height="350" :is-resizable="false" :bg-style="'none'"> <Modal v-if="showTeleportModal" ref="modalRef" @modal:close="() => mapEditor.setTool('move')" :modal-width="300" :modal-height="350" :is-resizable="false" bg-style="none">
<template #modalHeader> <template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">Teleport settings</h3> <h3 class="m-0 font-medium shrink-0 text-white">Teleport settings</h3>
</template> </template>
@ -28,7 +28,7 @@
<label for="toMap">Map to teleport to</label> <label for="toMap">Map to teleport to</label>
<select v-model="toMap" class="input-field" name="toMap" id="toMap"> <select v-model="toMap" class="input-field" name="toMap" id="toMap">
<option :value="null">Select map</option> <option :value="null">Select map</option>
<option v-for="map in mapList" :key="map.id" :value="map">{{ map.name }}</option> <option v-for="map in mapList" :key="map.id" :value="map.id">{{ map.name }}</option>
</select> </select>
</div> </div>
</div> </div>
@ -41,48 +41,48 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Map } from '@/application/types' import type { Map } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useGameStore } from '@/stores/gameStore' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { useMapEditorStore } from '@/stores/mapEditorStore' import { MapStorage } from '@/storage/storages'
import { computed, onMounted, ref, useTemplateRef, watch } from 'vue' import { computed, onMounted, ref, useTemplateRef, watch } from 'vue'
const showTeleportModal = computed(() => mapEditorStore.tool === 'pencil' && mapEditorStore.drawMode === 'teleport') const showTeleportModal = computed(() => mapEditor.tool.value === 'pencil' && mapEditor.drawMode.value === 'teleport')
const mapEditorStore = useMapEditorStore() const mapStorage = new MapStorage()
const gameStore = useGameStore() const mapEditor = useMapEditorComposable()
const mapList = ref<Map[]>([])
const modalRef = useTemplateRef('modalRef') const modalRef = useTemplateRef('modalRef')
const mapList = ref<Map[]>([])
defineExpose({ defineExpose({
open: () => modalRef.value?.open() open: () => modalRef.value?.open()
}) })
onMounted(fetchMaps)
function fetchMaps() {
gameStore.connection?.emit('gm:map:list', {}, (response: Map[]) => {
mapList.value = response
})
}
const { toPositionX, toPositionY, toRotation, toMap } = useRefTeleportSettings() const { toPositionX, toPositionY, toRotation, toMap } = useRefTeleportSettings()
function useRefTeleportSettings() { function useRefTeleportSettings() {
const settings = mapEditorStore.teleportSettings const settings = mapEditor.teleportSettings.value
return { return {
toPositionX: ref(settings.toPositionX), toPositionX: ref(settings.toPositionX),
toPositionY: ref(settings.toPositionY), toPositionY: ref(settings.toPositionY),
toRotation: ref(settings.toRotation), toRotation: ref(settings.toRotation),
toMap: ref(settings.toMapId) toMap: ref(settings.toMap)
} }
} }
watch([toPositionX, toPositionY, toRotation, toMap], updateTeleportSettings) watch([toPositionX, toPositionY, toRotation, toMap], updateTeleportSettings)
function updateTeleportSettings() { function updateTeleportSettings() {
mapEditorStore.setTeleportSettings({ mapEditor.setTeleportSettings({
toPositionX: toPositionX.value, toPositionX: toPositionX.value,
toPositionY: toPositionY.value, toPositionY: toPositionY.value,
toRotation: toRotation.value, toRotation: toRotation.value,
toMapId: toMap.value toMap: toMap.value
}) })
} }
async function fetchMaps() {
mapList.value = await mapStorage.getAll()
}
onMounted(async () => {
await fetchMaps()
})
</script> </script>

View File

@ -1,27 +1,25 @@
<template> <template>
<div class="absolute border-0 border-l-2 border-solid border-gray-500 w-1/4 min-w-80 flex flex-col top-0 right-0 z-10 h-dvh bg-gray-800 z-20" v-if="isOpen"> <div class="absolute border-0 border-l-2 border-solid border-gray-500 w-1/4 min-w-80 flex flex-col top-0 right-0 z-10 h-dvh bg-gray-800" v-if="(mapEditor.tool.value === 'pencil' && mapEditor.drawMode.value === 'tile') || mapEditor.tool.value === 'paint'">
<div class="relative z-10 p-2.5 border-solid border-0 border-b border-gray-500"> <div class="flex flex-col gap-2.5 p-2.5">
<h3 class="text-lg text-white">Tiles</h3> <div class="flex justify-between items-center">
<div class="flex-grow">
<div class="relative flex">
<img src="/assets/icons/mapEditor/search.svg" class="w-4 h-4 py-0.5 absolute top-1/2 -translate-y-1/2 left-2.5" alt="Search icon" />
<label class="mb-1.5 font-titles hidden" for="search">Search</label>
<input @mousedown.stop class="!pl-7 input-field w-full" type="text" name="search" placeholder="Search" v-model="searchQuery" />
</div> </div>
<div class="overflow-hidden grow relative"> </div>
<div class="absolute top-0 left-0 h-full w-full"> <img src="/assets/icons/mapEditor/dropdown-chevron.svg" class="w-12 h-12 ml-2 cursor-pointer hover:opacity-80 -rotate-90" alt="Close" @click="mapEditor.setTool('move')" />
<div class="relative z-10 h-full"> </div>
<div class="flex">
<select class="input-field w-full" name="lists" v-model="mapEditor.drawMode.value" @change="(event: any) => mapEditor.setDrawMode(event.target.value)">
<option value="tile">Tiles</option>
<option value="map_object">Objects</option>
</select>
</div>
</div>
<div class="h-full overflow-auto relative border-0 border-t border-solid border-gray-500 p-2.5">
<div class="h-full" v-if="!selectedGroup"> <div class="h-full" v-if="!selectedGroup">
<div class="flex pt-4 pl-4">
<div class="w-full flex gap-1.5 flex-row">
<div>
<label class="mb-1.5 font-titles hidden" for="search">Search...</label>
<input @mousedown.stop class="input-field" type="text" name="search" placeholder="Search" v-model="searchQuery" />
</div>
</div>
</div>
<div class="flex flex-col h-[calc(100%_-_170px)] p-4 pb-24">
<div class="mb-4 flex flex-wrap gap-2">
<button v-for="tag in uniqueTags" :key="tag" @click="toggleTag(tag)" class="btn-cyan" :class="{ 'opacity-50': !selectedTags.includes(tag) }">
{{ tag }}
</button>
</div>
<div class="h-full flex-grow overflow-y-auto">
<div class="grid grid-cols-4 gap-2 justify-items-center"> <div class="grid grid-cols-4 gap-2 justify-items-center">
<div v-for="group in groupedTiles" :key="group.parent.id" class="flex flex-col items-center justify-center relative"> <div v-for="group in groupedTiles" :key="group.parent.id" class="flex flex-col items-center justify-center relative">
<img <img
@ -42,12 +40,11 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
<div v-else class="h-full overflow-auto"> <div v-else class="h-full overflow-auto">
<div class="p-4"> <div class="p-4">
<button @click="closeGroup" class="btn-cyan mb-4">Back to All Tiles</button> <div class="text-center mb-8">
<h4 class="text-lg mb-4">{{ selectedGroup.parent.name }} Group</h4> <button @click="closeGroup" class="hover:text-white">Back to all tiles</button>
</div>
<div class="grid grid-cols-4 gap-2 justify-items-center"> <div class="grid grid-cols-4 gap-2 justify-items-center">
<div class="flex flex-col items-center justify-center"> <div class="flex flex-col items-center justify-center">
<img <img
@ -79,6 +76,10 @@
</div> </div>
</div> </div>
</div> </div>
<div class="flex flex-col h-40 gap-2.5 p-3.5 border-t border-0 border-solid border-gray-500">
<span>Tags:</span>
<div class="flex grow items-center flex-wrap gap-1.5 overflow-auto">
<span class="m-auto">No tags selected</span>
</div> </div>
</div> </div>
</div> </div>
@ -92,7 +93,6 @@ import { useTileProcessingComposable } from '@/composables/useTileProcessingComp
import { TileStorage } from '@/storage/storages' import { TileStorage } from '@/storage/storages'
import { computed, onMounted, onUnmounted, ref } from 'vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
const isOpen = ref(false)
const tileStorage = new TileStorage() const tileStorage = new TileStorage()
const mapEditor = useMapEditorComposable() const mapEditor = useMapEditorComposable()
const tileProcessor = useTileProcessingComposable() const tileProcessor = useTileProcessingComposable()
@ -102,12 +102,6 @@ const tileCategories = ref<Map<string, string>>(new Map())
const selectedGroup = ref<{ parent: Tile; children: Tile[] } | null>(null) const selectedGroup = ref<{ parent: Tile; children: Tile[] } | null>(null)
const tiles = ref<Tile[]>([]) const tiles = ref<Tile[]>([])
defineExpose({
open: () => (isOpen.value = true),
close: () => (isOpen.value = false),
toggle: () => (isOpen.value = !isOpen.value)
})
const uniqueTags = computed(() => { const uniqueTags = computed(() => {
const allTags = tiles.value.flatMap((tile) => tile.tags || []) const allTags = tiles.value.flatMap((tile) => tile.tags || [])
return Array.from(new Set(allTags)) return Array.from(new Set(allTags))

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="flex justify-center p-5"> <div class="flex justify-between p-5 w-[calc(100%_-_40px)] fixed bottom-0 left-0 z-20" :class="{ 'list-open': listOpen }">
<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 z-20"> <div class="toolbar 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="mapEditor.currentMap.value"> <div ref="toolbar" class="tools flex gap-2.5" v-if="mapEditor.currentMap.value">
<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': mapEditor.tool.value === 'move' }" @click="handleClick('move')"> <button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditor.tool.value === 'move' }" @click="handleClick('move')">
<img class="invert w-5 h-5" src="/assets/icons/mapEditor/move.svg" alt="Move camera" /> <span class="h-5" :class="{ 'ml-2.5': mapEditor.tool.value !== 'move' }">(M)</span> <img class="invert w-5 h-5" src="/assets/icons/mapEditor/move.svg" alt="Move camera" /> <span class="h-5" :class="{ 'ml-2.5': mapEditor.tool.value !== 'move' }">(M)</span>
@ -68,53 +68,59 @@
<div class="w-px bg-cyan"></div> <div class="w-px bg-cyan"></div>
<button class="flex justify-center items-center min-w-10 p-0 relative" @click="handleClick('settings')"><img class="invert w-5 h-5" src="/assets/icons/mapEditor/gear.svg" alt="Map settings" /> <span class="h-5 ml-2.5">(Z)</span></button> <button class="flex justify-center items-center min-w-10 p-0 relative" @click="isMapEditorSettingsModalOpen = !isMapEditorSettingsModalOpen"><img class="invert w-5 h-5" src="/assets/icons/mapEditor/gear.svg" alt="Map settings" /> <span class="h-5 ml-2.5">(Z)</span></button>
<div class="w-px bg-cyan"></div> <div class="w-px bg-cyan"></div>
<label class="my-auto gap-0" for="checkbox">Continuous Drawing</label> <button class="flex justify-center items-center min-w-10 p-0 relative" @click="isMapEditorSettingsModalOpen = !isMapEditorSettingsModalOpen"><img class="invert w-5 h-5" src="/assets/icons/mapEditor/settings.svg" alt="Map settings" /> <span class="h-5 ml-2.5">(S)</span></button>
<input type="checkbox" />
</div> </div>
</div>
<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"> <div class="toolbar 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="() => emit('open-maps')">Load</button> <button class="btn-cyan px-3.5" @click="() => emit('open-maps')">Load</button>
<button class="btn-cyan px-3.5" @click="() => emit('save')" v-if="mapEditor.currentMap.value">Save</button> <button class="btn-cyan px-3.5" @click="() => emit('save')" v-if="mapEditor.currentMap.value">Save</button>
<button class="btn-cyan px-3.5" @click="() => emit('clear')" v-if="mapEditor.currentMap.value">Clear</button> <button class="btn-cyan px-3.5" @click="() => emit('clear')" v-if="mapEditor.currentMap.value">Clear</button>
<button class="btn-cyan px-3.5" @click="() => emit('close-editor')">Exit</button> <button class="btn-cyan px-3.5" @click="() => mapEditor.toggleActive()">Exit</button>
</div> </div>
</div> </div>
<Modal :isModalOpen="isMapEditorSettingsModalOpen" @modal:close="() => (isMapEditorSettingsModalOpen = false)" :modal-width="300" :modal-height="350" :is-resizable="false" bg-style="none">
<template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">Map editor settings</h3>
</template>
<template #modalBody>
<div class="m-4 flex items-center space-x-2">
<input id="continuous-drawing" @change="toggleContinuousDrawing" v-model="isContinuousDrawingEnabled" type="checkbox" class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" />
<label for="continuous-drawing" class="text-sm font-medium text-gray-200 cursor-pointer"> Continuous Drawing </label>
</div> </div>
<div class="m-4 flex items-center space-x-2">
<input id="show-placed-map-object-preview" @change="mapEditor.togglePlacedMapObjectPreview()" v-model="isShowPlacedMapObjectPreviewEnabled" type="checkbox" class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" />
<label for="show-placed-map-object-preview" class="text-sm font-medium text-gray-200 cursor-pointer"> Show placed map object preview </label>
</div>
</template>
</Modal>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Modal from '@/components/utilities/Modal.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { onClickOutside } from '@vueuse/core' import { onClickOutside } from '@vueuse/core'
import { onBeforeUnmount, onMounted, ref } from 'vue' import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
const mapEditor = useMapEditorComposable() const mapEditor = useMapEditorComposable()
const emit = defineEmits(['save', 'clear', 'open-maps', 'open-settings', 'close-editor', 'open-tile-list', 'open-map-object-list', 'close-lists']) const emit = defineEmits(['save', 'clear', 'open-maps', 'open-settings', 'open-teleport'])
// track when clicked outside of toolbar items // States
const toolbar = ref(null) const toolbar = ref(null)
const isMapEditorSettingsModalOpen = ref(false)
// track select state const selectPencilOpen = ref(false)
let selectPencilOpen = ref(false) const selectEraserOpen = ref(false)
let selectEraserOpen = ref(false) const isContinuousDrawingEnabled = ref<Boolean>(false)
const isShowPlacedMapObjectPreviewEnabled = ref<Boolean>(mapEditor.isPlacedMapObjectPreviewEnabled.value)
let tileListShown = ref(false) const listOpen = computed(() => (mapEditor.tool.value === 'pencil' && (mapEditor.drawMode.value === 'tile' || mapEditor.drawMode.value === 'map_object')) || mapEditor.tool.value === 'paint')
let mapObjectListShown = ref(false)
defineExpose({ tileListShown, mapObjectListShown })
// drawMode // drawMode
function setDrawMode(value: string) { function setDrawMode(value: string) {
if (mapEditor.tool.value === 'paint' || mapEditor.tool.value === 'pencil' || mapEditor.tool.value === 'eraser') {
emit('close-lists')
if (value === 'tile') emit('open-tile-list')
if (value === 'map_object') emit('open-map-object-list')
}
mapEditor.setDrawMode(value) mapEditor.setDrawMode(value)
selectPencilOpen.value = false selectPencilOpen.value = false
selectEraserOpen.value = false selectEraserOpen.value = false
@ -131,24 +137,21 @@ function setEraserMode() {
selectEraserOpen.value = false selectEraserOpen.value = false
} }
function toggleContinuousDrawing() {
mapEditor.setInputMode(isContinuousDrawingEnabled.value ? 'hold' : 'tap')
}
function handleModeClick(mode: string, type: 'pencil' | 'eraser') { function handleModeClick(mode: string, type: 'pencil' | 'eraser') {
setDrawMode(mode) setDrawMode(mode)
type === 'pencil' ? setPencilMode() : setEraserMode() type === 'pencil' ? setPencilMode() : setEraserMode()
} }
function handleClick(tool: string) { function handleClick(tool: string) {
if (tool === 'settings') {
emit('open-settings')
emit('close-lists')
} else if (tool === 'move') {
emit('close-lists')
mapEditor.setTool(tool) mapEditor.setTool(tool)
} else { if (tool === 'paint') mapEditor.setDrawMode('tile')
mapEditor.setTool(tool)
}
selectPencilOpen.value = tool === 'pencil' ? !selectPencilOpen.value : false selectPencilOpen.value = tool === 'pencil' ? !selectPencilOpen.value : false
selectEraserOpen.value = tool === 'eraser' ? !selectEraserOpen.value : false selectEraserOpen.value = tool === 'eraser' ? !selectEraserOpen.value : false
if (mapEditor.drawMode.value === 'teleport') emit('open-teleport')
} }
function cycleToolMode(tool: 'pencil' | 'eraser') { function cycleToolMode(tool: 'pencil' | 'eraser') {
@ -164,15 +167,18 @@ function initKeyShortcuts(event: KeyboardEvent) {
// Check if map is set // Check if map is set
if (!mapEditor.currentMap.value) return if (!mapEditor.currentMap.value) return
// prevent if focused on composables // prevent if focused on inputs
if (document.activeElement?.tagName === 'INPUT') return if (document.activeElement?.tagName === 'INPUT') return
if (event.ctrlKey) return
const keyActions: { [key: string]: string } = { const keyActions: { [key: string]: string } = {
m: 'move', m: 'move',
p: 'pencil', p: 'pencil',
e: 'eraser', e: 'eraser',
b: 'paint', b: 'paint',
z: 'settings' z: 'settings',
s: 'mapEditorSettings'
} }
if (keyActions.hasOwnProperty(event.key)) { if (keyActions.hasOwnProperty(event.key)) {

View File

@ -26,7 +26,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { login } from '@/services/authentication' import { socketManager } from '@/managers/SocketManager'
import { login } from '@/services/authenticationService'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useCookies } from '@vueuse/integrations/useCookies' import { useCookies } from '@vueuse/integrations/useCookies'
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
@ -39,15 +40,6 @@ const password = ref('')
const formError = ref('') const formError = ref('')
const showPassword = ref(false) const showPassword = ref(false)
// automatic login because of development
onMounted(async () => {
const token = useCookies().get('token')
if (token) {
gameStore.setToken(token)
gameStore.initConnection()
}
})
async function submit() { async function submit() {
// check if username and password are valid // check if username and password are valid
if (username.value === '' || password.value === '') { if (username.value === '' || password.value === '') {
@ -62,7 +54,7 @@ async function submit() {
formError.value = response.error formError.value = response.error
return return
} }
gameStore.setToken(response.token) socketManager.setToken(response.token)
gameStore.initConnection() gameStore.initConnection()
return true // Indicate success return true // Indicate success
} }

View File

@ -22,7 +22,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { newPassword } from '@/services/authentication' import { newPassword } from '@/services/authenticationService'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useCookies } from '@vueuse/integrations/useCookies' import { useCookies } from '@vueuse/integrations/useCookies'
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
@ -34,15 +34,6 @@ const password = ref('')
const newPasswordError = ref('') const newPasswordError = ref('')
const showPassword = ref(false) const showPassword = ref(false)
// automatic login because of development
onMounted(async () => {
const token = useCookies().get('token')
if (token) {
gameStore.setToken(token)
gameStore.initConnection()
}
})
async function newPasswordFunc() { async function newPasswordFunc() {
// check if username and password are valid // check if username and password are valid
if (password.value === '') { if (password.value === '') {

View File

@ -26,7 +26,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { login, register } from '@/services/authentication' import { socketManager } from '@/managers/SocketManager'
import { login, register } from '@/services/authenticationService'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useCookies } from '@vueuse/integrations/useCookies' import { useCookies } from '@vueuse/integrations/useCookies'
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
@ -40,15 +41,6 @@ const email = ref('')
const formError = ref('') const formError = ref('')
const showPassword = ref(false) const showPassword = ref(false)
// automatic login because of development
onMounted(async () => {
const token = useCookies().get('token')
if (token) {
gameStore.setToken(token)
gameStore.initConnection()
}
})
async function submit() { async function submit() {
// check if username and password are valid // check if username and password are valid
if (username.value === '' || email.value === '' || password.value === '') { if (username.value === '' || email.value === '' || password.value === '') {
@ -76,7 +68,7 @@ async function submit() {
return return
} }
gameStore.setToken(loginResponse.token) socketManager.setToken(loginResponse.token)
gameStore.initConnection() gameStore.initConnection()
} }
</script> </script>

View File

@ -30,7 +30,7 @@
<script setup lang="ts"> <script setup lang="ts">
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { resetPassword } from '@/services/authentication' import { resetPassword } from '@/services/authenticationService'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { ref } from 'vue' import { ref } from 'vue'

View File

@ -18,11 +18,11 @@
<div class="absolute right-[calc(100%_+_16px)] -top-px flex gap-2 flex-col"> <div class="absolute right-[calc(100%_+_16px)] -top-px flex gap-2 flex-col">
<div v-for="character in characters" :key="character.id" class="character relative rounded default-border w-12 h-12 bg-[url('/assets/ui-texture.png')] after:absolute after:w-full after:h-px after:bg-gray-500" :class="{ active: selectedCharacterId === character.id }"> <div v-for="character in characters" :key="character.id" class="character relative rounded default-border w-12 h-12 bg-[url('/assets/ui-texture.png')] after:absolute after:w-full after:h-px after:bg-gray-500" :class="{ active: selectedCharacterId === character.id }">
<img src="/assets/placeholders/head.png" class="w-9 h-9 object-contain center-element" alt="Player head" /> <img src="/assets/placeholders/head.png" class="w-9 h-9 object-contain center-element" alt="Player head" />
<input class="h-full w-full absolute m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0" type="radio" name="character" :value="character.id" v-model="selectedCharacterId" /> <input class="h-full w-full absolute m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0 btn-sound" type="radio" name="character" :value="character.id" v-model="selectedCharacterId" />
</div> </div>
<div class="character relative rounded default-border w-12 h-12 bg-[url('/assets/ui-texture.png')]" :class="{ active: characters.length == 0 }" v-if="characters.length < 4"> <div class="character relative rounded default-border w-12 h-12 bg-[url('/assets/ui-texture.png')]" :class="{ active: characters.length == 0 }" v-if="characters.length < 4">
<button class="p-0 h-full w-full flex flex-col justify-between focus-visible:outline-offset-0" @click="isCreateNewCharacterModalOpen = true"> <button class="p-0 h-full w-full flex flex-col justify-between focus-visible:outline-offset-0 btn-sound" @click="isCreateNewCharacterModalOpen = true">
<img class="w-6 h-6 object-contain center-element" draggable="false" src="/assets/icons/plus-icon.svg" /> <img class="w-6 h-6 object-contain center-element btn-sound" draggable="false" src="/assets/icons/plus-icon.svg" />
</button> </button>
</div> </div>
</div> </div>
@ -122,12 +122,16 @@
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import config from '@/application/config'
import { SocketEvent } from '@/application/enums'
import { type CharacterHair, type Character as CharacterT, type Map } from '@/application/types' import { type CharacterHair, type Character as CharacterT, type Map } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useSoundComposable } from '@/composables/useSoundComposable'
import { socketManager } from '@/managers/SocketManager'
import { CharacterHairStorage } from '@/storage/storages' import { CharacterHairStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { onBeforeUnmount, onMounted, ref, watch } from 'vue' import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
const { playSound } = useSoundComposable()
const gameStore = useGameStore() const gameStore = useGameStore()
const isLoading = ref<boolean>(true) const isLoading = ref<boolean>(true)
const characters = ref<CharacterT[]>([]) const characters = ref<CharacterT[]>([])
@ -139,10 +143,10 @@ const selectedHairId = ref<string | null>(null)
// Fetch characters // Fetch characters
setTimeout(() => { setTimeout(() => {
gameStore.connection?.emit('character:list') socketManager.emit(SocketEvent.CHARACTER_LIST)
}, 750) }, 750)
gameStore.connection?.on('character:list', (data: any) => { socketManager.on(SocketEvent.CHARACTER_LIST, (data: any) => {
characters.value = data characters.value = data
isLoading.value = false isLoading.value = false
}) })
@ -151,8 +155,8 @@ gameStore.connection?.on('character:list', (data: any) => {
function loginWithCharacter() { function loginWithCharacter() {
if (!selectedCharacterId.value) return if (!selectedCharacterId.value) return
gameStore.connection?.emit( socketManager.emit(
'character:connect', SocketEvent.CHARACTER_CONNECT,
{ {
characterId: selectedCharacterId.value, characterId: selectedCharacterId.value,
characterHairId: selectedHairId.value characterHairId: selectedHairId.value
@ -165,27 +169,27 @@ function loginWithCharacter() {
// Create character logics // Create character logics
function createCharacter() { function createCharacter() {
gameStore.connection?.on('character:create:success', (data: CharacterT) => { socketManager.emit(SocketEvent.CHARACTER_CREATE, { name: newCharacterName.value }, (success: boolean) => {
gameStore.setCharacter(data) if (success) return
isCreateNewCharacterModalOpen.value = false isCreateNewCharacterModalOpen.value = false
}) })
gameStore.connection?.emit('character:create', { name: newCharacterName.value })
} }
// Watch changes for selected character and update hairs // Watch changes for selected character and update hairs
watch(selectedCharacterId, (characterId) => { watch(selectedCharacterId, (characterId) => {
if (!characterId) return if (!characterId) return
// selectedHairId.value = characters.value.find((c) => c.id == characterId)?.characterHairId ?? null selectedHairId.value = characters.value.find((c) => c.id == characterId)?.characterHair ?? null
}) })
onMounted(async () => { onMounted(async () => {
playSound('/assets/music/intro.mp3')
const characterHairStorage = new CharacterHairStorage() const characterHairStorage = new CharacterHairStorage()
characterHairs.value = await characterHairStorage.getAll() characterHairs.value = await characterHairStorage.getAll()
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
gameStore.connection?.off('character:list') socketManager.off(SocketEvent.CHARACTER_LIST)
gameStore.connection?.off('character:connect') socketManager.off(SocketEvent.CHARACTER_CONNECT)
gameStore.connection?.off('character:create:success') socketManager.off(SocketEvent.CHARACTER_CREATE)
}) })
</script> </script>

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="flex justify-center items-center h-dvh relative"> <div class="flex justify-center items-center h-dvh relative">
<Game :config="gameConfig" @create="createGame"> <Game :config="gameConfig" @create="createGame">
<Scene name="main" @preload="preloadScene" @create="createScene"> <Scene name="main" @preload="preloadScene">
<Menu /> <Menu />
<Hud /> <Hud />
<Hotkeys /> <Hotkeys />
@ -27,18 +27,23 @@ import Hotkeys from '@/components/game/gui/Hotkeys.vue'
import Hud from '@/components/game/gui/Hud.vue' import Hud from '@/components/game/gui/Hud.vue'
import Menu from '@/components/game/gui/Menu.vue' import Menu from '@/components/game/gui/Menu.vue'
import Map from '@/components/game/map/Map.vue' import Map from '@/components/game/map/Map.vue'
import { useSoundComposable } from '@/composables/useSoundComposable'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { Game, Scene } from 'phavuer' import { Game, Scene } from 'phavuer'
import { onBeforeUnmount } from 'vue' import { onMounted } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const { playSound, stopSound } = useSoundComposable()
const gameConfig = { const gameConfig = {
name: config.name, name: config.name,
width: window.innerWidth, width: window.innerWidth,
height: window.innerHeight, height: window.innerHeight,
type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS
resolution: 5 resolution: 5,
input: {
windowEvents: false
}
} }
const createGame = (game: Phaser.Game) => { const createGame = (game: Phaser.Game) => {
@ -55,6 +60,8 @@ const createGame = (game: Phaser.Game) => {
}) })
gameStore.disconnectSocket() gameStore.disconnectSocket()
} }
playSound('/assets/sounds/connect.wav')
} }
function preloadScene(scene: Phaser.Scene) { function preloadScene(scene: Phaser.Scene) {
@ -63,7 +70,7 @@ function preloadScene(scene: Phaser.Scene) {
scene.load.image('waypoint', '/assets/waypoint.png') scene.load.image('waypoint', '/assets/waypoint.png')
} }
function createScene(scene: Phaser.Scene) {} onMounted(() => {
stopSound('/assets/music/intro.mp3')
onBeforeUnmount(() => {}) })
</script> </script>

View File

@ -44,13 +44,4 @@ function switchToLogin() {
currentForm.value = 'login' currentForm.value = 'login'
doesUrlHaveToken.value = false doesUrlHaveToken.value = false
} }
// automatic login because of development
onMounted(async () => {
const token = useCookies().get('token')
if (token) {
gameStore.setToken(token)
gameStore.initConnection()
}
})
</script> </script>

View File

@ -4,22 +4,11 @@
<Scene name="main" @preload="preloadScene"> <Scene name="main" @preload="preloadScene">
<div v-if="!isLoaded" class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white text-3xl font-ui">Loading...</div> <div v-if="!isLoaded" class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white text-3xl font-ui">Loading...</div>
<div v-else> <div v-else>
<Map :key="mapEditor.currentMap.value?.id" /> <Map v-if="mapEditor.currentMap.value" :key="mapEditor.currentMap.value?.id" />
<Toolbar <Toolbar ref="toolbar" @save="save" @clear="clear" @open-maps="mapModal?.open" @open-settings="mapSettingsModal?.open" @open-teleport="teleportModal?.open" />
ref="toolbar"
@save="save"
@clear="clear"
@open-maps="mapModal?.open"
@open-settings="mapSettingsModal?.open"
@close-editor="mapEditor.toggleActive"
@close-lists="tileList?.close"
@closeLists="objectList?.close"
@open-tile-list="tileList?.open"
@open-map-object-list="objectList?.open"
/>
<MapList ref="mapModal" @open-create-map="mapSettingsModal?.open" /> <MapList ref="mapModal" @open-create-map="mapSettingsModal?.open" />
<TileList ref="tileList" /> <TileList />
<ObjectList ref="objectList" /> <MapObjectList />
<MapSettings ref="mapSettingsModal" /> <MapSettings ref="mapSettingsModal" />
<TeleportModal ref="teleportModal" /> <TeleportModal ref="teleportModal" />
</div> </div>
@ -30,30 +19,29 @@
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import config from '@/application/config'
import { SocketEvent } from '@/application/enums'
import { socketManager } from '@/managers/SocketManager'
import 'phaser' import 'phaser'
import type { Map as MapT } from '@/application/types' import type { Map as MapT } from '@/application/types'
import Map from '@/components/gameMaster/mapEditor/Map.vue' import Map from '@/components/gameMaster/mapEditor/Map.vue'
import MapList from '@/components/gameMaster/mapEditor/partials/MapList.vue' import MapList from '@/components/gameMaster/mapEditor/partials/MapList.vue'
import ObjectList from '@/components/gameMaster/mapEditor/partials/MapObjectList.vue' import MapObjectList from '@/components/gameMaster/mapEditor/partials/MapObjectList.vue'
import MapSettings from '@/components/gameMaster/mapEditor/partials/MapSettings.vue' import MapSettings from '@/components/gameMaster/mapEditor/partials/MapSettings.vue'
import TeleportModal from '@/components/gameMaster/mapEditor/partials/TeleportModal.vue' import TeleportModal from '@/components/gameMaster/mapEditor/partials/TeleportModal.vue'
import TileList from '@/components/gameMaster/mapEditor/partials/TileList.vue' import TileList from '@/components/gameMaster/mapEditor/partials/TileList.vue'
import Toolbar from '@/components/gameMaster/mapEditor/partials/Toolbar.vue' import Toolbar from '@/components/gameMaster/mapEditor/partials/Toolbar.vue'
import { loadAllTilesIntoScene } from '@/composables/mapComposable'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { loadAllTileTextures } from '@/services/mapService'
import { MapStorage } from '@/storage/storages' import { MapStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { Game, Scene } from 'phavuer' import { Game, Scene } from 'phavuer'
import { ref, useTemplateRef, watch } from 'vue' import { ref, useTemplateRef } from 'vue'
const mapStorage = new MapStorage() const mapStorage = new MapStorage()
const mapEditor = useMapEditorComposable() const mapEditor = useMapEditorComposable()
const gameStore = useGameStore() const gameStore = useGameStore()
const toolbar = useTemplateRef('toolbar')
const mapModal = useTemplateRef('mapModal') const mapModal = useTemplateRef('mapModal')
const tileList = useTemplateRef('tileList')
const objectList = useTemplateRef('objectList')
const mapSettingsModal = useTemplateRef('mapSettingsModal') const mapSettingsModal = useTemplateRef('mapSettingsModal')
const teleportModal = useTemplateRef('teleportModal') const teleportModal = useTemplateRef('teleportModal')
@ -64,7 +52,10 @@ const gameConfig = {
width: window.innerWidth, width: window.innerWidth,
height: window.innerHeight, height: window.innerHeight,
type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS
resolution: 5 resolution: 5,
input: {
windowEvents: false
}
} }
const createGame = (game: Phaser.Game) => { const createGame = (game: Phaser.Game) => {
@ -82,7 +73,7 @@ const preloadScene = async (scene: Phaser.Scene) => {
scene.load.image('waypoint', '/assets/waypoint.png') scene.load.image('waypoint', '/assets/waypoint.png')
// Get all tiles from IndexedDB and load them into the scene // Get all tiles from IndexedDB and load them into the scene
await loadAllTilesIntoScene(scene) await loadAllTileTextures(scene)
// Wait for all assets to be loaded before continuing // Wait for all assets to be loaded before continuing
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
@ -98,18 +89,11 @@ function save() {
if (!currentMap) return if (!currentMap) return
const data = { const data = {
mapId: currentMap.id, ...currentMap,
name: currentMap.name, mapId: currentMap.id
width: currentMap.width,
height: currentMap.height,
tiles: currentMap.tiles,
pvp: currentMap.pvp,
mapEffects: currentMap.mapEffects?.map(({ id, effect, strength }) => ({ id, effect, strength })) ?? [],
mapEventTiles: currentMap.mapEventTiles?.map(({ id, type, positionX, positionY, teleport }) => ({ id, type, positionX, positionY, teleport })) ?? [],
placedMapObjects: currentMap.placedMapObjects?.map(({ id, mapObject, depth, isRotated, positionX, positionY }) => ({ id, mapObject, depth, isRotated, positionX, positionY })) ?? []
} }
gameStore.connection?.emit('gm:map:update', data, (response: MapT) => { socketManager.emit(SocketEvent.GM_MAP_UPDATE, data, (response: MapT) => {
mapStorage.update(response.id, response) mapStorage.update(response.id, response)
}) })
} }
@ -118,6 +102,6 @@ function clear() {
if (!mapEditor.currentMap.value) return if (!mapEditor.currentMap.value) return
// Clear placed objects, event tiles and tiles // Clear placed objects, event tiles and tiles
mapEditor.clearMap() mapEditor.triggerClearTiles()
} }
</script> </script>

View File

@ -1,23 +0,0 @@
<template>
<div style="display: none">
<img v-for="(url, index) in imageUrls" :key="index" :src="url" alt="" @load="handleImageLoad(index)" @error="handleImageError(index)" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
// Internal array of images to preload
const imageUrls = ref<string[]>(['/assets/ui-elements/button-ui-box-textured.svg', '/assets/ui-elements/button-ui-frame-empty.svg', '/assets/ui-elements/button-ui-box-textured-small.svg'])
const loadedImages = ref<Set<number>>(new Set())
const handleImageLoad = (index: number) => {
loadedImages.value.add(index)
console.log(`Image ${index} loaded:`, imageUrls.value[index])
}
const handleImageError = (index: number) => {
console.log(`Image ${index} failed to load:`, imageUrls.value[index])
}
</script>

View File

@ -1,10 +1,14 @@
<template></template> <template></template>
<script setup lang="ts"> <script setup lang="ts">
import { CharacterHairStorage, CharacterTypeStorage, MapObjectStorage, MapStorage, SpriteStorage, TileStorage } from '@/storage/storages' import { socketManager } from '@/managers/SocketManager'
import { login } from '@/services/authenticationService'
import { CharacterHairStorage, CharacterTypeStorage, MapObjectStorage, MapStorage, SoundStorage, SpriteStorage, TileStorage } from '@/storage/storages'
import { TextureStorage } from '@/storage/textureStorage' import { TextureStorage } from '@/storage/textureStorage'
import { useGameStore } from '@/stores/gameStore'
import { onMounted, onUnmounted } from 'vue' import { onMounted, onUnmounted } from 'vue'
const gameStore = useGameStore()
const mapStorage = new MapStorage() const mapStorage = new MapStorage()
const tileStorage = new TileStorage() const tileStorage = new TileStorage()
const mapObjectStorage = new MapObjectStorage() const mapObjectStorage = new MapObjectStorage()
@ -12,6 +16,7 @@ const spriteStorage = new SpriteStorage()
const characterTypeStorage = new CharacterTypeStorage() const characterTypeStorage = new CharacterTypeStorage()
const characterHairStorage = new CharacterHairStorage() const characterHairStorage = new CharacterHairStorage()
const textureStorage = new TextureStorage() const textureStorage = new TextureStorage()
const soundStorage = new SoundStorage()
let currentString = '' let currentString = ''
@ -32,9 +37,21 @@ async function handleKeyPress(event: KeyboardEvent) {
await characterTypeStorage.destroy() await characterTypeStorage.destroy()
await characterHairStorage.destroy() await characterHairStorage.destroy()
await textureStorage.destroy() await textureStorage.destroy()
await soundStorage.destroy()
currentString = '' // Reset currentString = '' // Reset
} }
if (currentString.includes('11')) {
if (socketManager.token) return
const response = await login('root', 'password')
if (response.success === undefined) {
return
}
socketManager.setToken(response.token)
gameStore.initConnection()
}
// Reset string after a certain amount of time // Reset string after a certain amount of time
setTimeout(() => { setTimeout(() => {
currentString = '' currentString = ''

View File

@ -14,11 +14,11 @@
<slot name="modalHeader" /> <slot name="modalHeader" />
</div> </div>
<div class="flex gap-2.5"> <div class="flex gap-2.5">
<button v-if="canFullScreen" @click="toggleFullScreen" class="w-5 h-5 m-0 p-0 relative hover:scale-110 transition-transform duration-300 ease-in-out"> <button v-if="canFullScreen" @click="toggleFullScreen" class="w-5 h-5 m-0 p-0 relative hover:scale-110 transition-transform duration-300 ease-in-out btn-sound">
<img :alt="isFullScreen ? 'exit full-screen' : 'full-screen'" :src="isFullScreen ? '/assets/icons/modal/minimize.svg' : '/assets/icons/modal/increase-size-option.svg'" class="w-3.5 h-3.5 invert" draggable="false" /> <img :alt="isFullScreen ? 'exit full-screen' : 'full-screen'" :src="isFullScreen ? '/assets/icons/modal/minimize.svg' : '/assets/icons/modal/increase-size-option.svg'" class="w-3.5 h-3.5 invert" draggable="false" />
</button> </button>
<button v-if="closable" @click="closeModal" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out"> <button v-if="closable" @click="closeModal" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out btn-sound">
<img alt="close" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" draggable="false" /> <img alt="close" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full btn-sound" draggable="false" />
</button> </button>
</div> </div>
</div> </div>
@ -86,7 +86,11 @@ const emit = defineEmits<{
defineExpose({ defineExpose({
open: () => (isModalOpenRef.value = true), open: () => (isModalOpenRef.value = true),
close: () => (isModalOpenRef.value = false), close: () => (isModalOpenRef.value = false),
toggle: () => (isModalOpenRef.value = !isModalOpenRef.value) toggle: () => (isModalOpenRef.value = !isModalOpenRef.value),
setPosition: (a: number, b: number) => {
x.value = a
y.value = b
}
}) })
const isModalOpenRef = ref(props.isModalOpen) const isModalOpenRef = ref(props.isModalOpen)

View File

@ -10,7 +10,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SocketEvent } from '@/application/enums'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { socketManager } from '@/managers/SocketManager'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { onBeforeMount, onBeforeUnmount, onMounted, onUnmounted, watch } from 'vue' import { onBeforeMount, onBeforeUnmount, onMounted, onUnmounted, watch } from 'vue'
@ -26,7 +28,7 @@ type Notification = {
} }
function setupNotificationListener(connection: any) { function setupNotificationListener(connection: any) {
connection.on('notification', (data: Notification) => { connection.on(SocketEvent.NOTIFICATION, (data: Notification) => {
gameStore.addNotification({ gameStore.addNotification({
title: data.title, title: data.title,
message: data.message message: data.message
@ -52,7 +54,7 @@ onMounted(() => {
onUnmounted(() => { onUnmounted(() => {
const connection = gameStore.connection const connection = gameStore.connection
if (connection) { if (connection) {
connection.off('notification') connection.off(SocketEvent.NOTIFICATION)
} }
}) })
</script> </script>

View File

@ -1,5 +1,5 @@
import config from '@/application/config' import config from '@/application/config'
import { getTile, tileToWorldXY } from '@/composables/mapComposable' import { getTile, tileToWorldXY } from '@/services/mapService'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { ref, type Ref } from 'vue' import { ref, type Ref } from 'vue'

View File

@ -1,4 +1,6 @@
import { getTile } from '@/composables/mapComposable' import { SocketEvent } from '@/application/enums'
import { socketManager } from '@/managers/SocketManager'
import { getTile } from '@/services/mapService'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { useBaseControlsComposable } from './useBaseControlsComposable' import { useBaseControlsComposable } from './useBaseControlsComposable'
@ -6,7 +8,77 @@ import { useBaseControlsComposable } from './useBaseControlsComposable'
export function useGameControlsComposable(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) { export function useGameControlsComposable(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
const gameStore = useGameStore() const gameStore = useGameStore()
const baseHandlers = useBaseControlsComposable(scene, layer, waypoint, camera) const baseHandlers = useBaseControlsComposable(scene, layer, waypoint, camera)
const pressedKeys = new Set<string>()
let moveTimeout: NodeJS.Timeout | null = null
let currentPosition = {
x: 0,
y: 0
}
// Movement constants
const MOVEMENT_DELAY = 110 // Milliseconds between moves
const ARROW_KEYS = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'] as const
function updateCurrentPosition() {
if (!gameStore.character) return
currentPosition = {
x: gameStore.character.positionX,
y: gameStore.character.positionY
}
}
function calculateNewPosition() {
let newX = currentPosition.x
let newY = currentPosition.y
if (pressedKeys.has('ArrowLeft')) newX--
if (pressedKeys.has('ArrowRight')) newX++
if (pressedKeys.has('ArrowUp')) newY--
if (pressedKeys.has('ArrowDown')) newY++
return { newX, newY }
}
function emitMovement(x: number, y: number) {
if (x === currentPosition.x && y === currentPosition.y) return
socketManager.emit(SocketEvent.MAP_CHARACTER_MOVE, [x, y])
socketManager.on(SocketEvent.MAP_CHARACTER_MOVE, ([characterId, posX, posY, rot, isMoving]: [string, number, number, number, boolean]) => {
if (characterId !== gameStore.character?.id) return
currentPosition = { x: posX, y: posY }
})
currentPosition = { x, y }
}
function startMovementLoop() {
if (moveTimeout) return
const move = () => {
if (pressedKeys.size === 0) {
stopMovementLoop()
return
}
updateCurrentPosition()
const { newX, newY } = calculateNewPosition()
emitMovement(newX, newY)
moveTimeout = setTimeout(move, MOVEMENT_DELAY)
}
move()
}
function stopMovementLoop() {
if (moveTimeout) {
clearTimeout(moveTimeout)
moveTimeout = null
}
}
// Pointer Handlers
function handlePointerDown(pointer: Phaser.Input.Pointer) { function handlePointerDown(pointer: Phaser.Input.Pointer) {
baseHandlers.startDragging(pointer) baseHandlers.startDragging(pointer)
} }
@ -22,77 +94,37 @@ export function useGameControlsComposable(scene: Phaser.Scene, layer: Phaser.Til
const pointerTile = getTile(layer, pointer.worldX, pointer.worldY) const pointerTile = getTile(layer, pointer.worldX, pointer.worldY)
if (!pointerTile) return if (!pointerTile) return
gameStore.connection?.emit('map:character:move', { emitMovement(pointerTile.x, pointerTile.y)
positionX: pointerTile.x,
positionY: pointerTile.y
})
} }
const pressedKeys = new Set<string>() // Keyboard Handlers
let moveInterval: number | null = null
function handleKeyDown(event: KeyboardEvent) { function handleKeyDown(event: KeyboardEvent) {
if (!gameStore.character) return if (!gameStore.character) return
// console.log(event.key) if (ARROW_KEYS.includes(event.key as (typeof ARROW_KEYS)[number])) {
if (event.repeat) return
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) {
pressedKeys.add(event.key) pressedKeys.add(event.key)
updateCurrentPosition()
// Start movement loop if not already running startMovementLoop()
if (!moveInterval) {
moveInterval = window.setInterval(moveCharacter, 250) // Adjust timing as needed
moveCharacter() // Move immediately on first press
}
} }
// Attack on CTRL
if (event.key === 'Control') { if (event.key === 'Control') {
gameStore.connection?.emit('map:character:attack') socketManager.emit(SocketEvent.MAP_CHARACTER_ATTACK)
} }
} }
function handleKeyUp(event: KeyboardEvent) { function handleKeyUp(event: KeyboardEvent) {
pressedKeys.delete(event.key) pressedKeys.delete(event.key)
// If no movement keys are pressed, clear the interval if (pressedKeys.size === 0) {
if (pressedKeys.size === 0 && moveInterval) { stopMovementLoop()
clearInterval(moveInterval)
moveInterval = null
}
}
function moveCharacter() {
if (!gameStore.character) return
const { positionX, positionY } = gameStore.character
if (pressedKeys.has('ArrowLeft')) {
gameStore.connection?.emit('map:character:move', {
positionX: positionX - 1,
positionY: positionY
})
}
if (pressedKeys.has('ArrowRight')) {
gameStore.connection?.emit('map:character:move', {
positionX: positionX + 1,
positionY: positionY
})
}
if (pressedKeys.has('ArrowUp')) {
gameStore.connection?.emit('map:character:move', {
positionX: positionX,
positionY: positionY - 1
})
}
if (pressedKeys.has('ArrowDown')) {
gameStore.connection?.emit('map:character:move', {
positionX: positionX,
positionY: positionY + 1
})
} }
} }
const setupControls = () => { const setupControls = () => {
updateCurrentPosition() // Initialize position
scene.input.on(Phaser.Input.Events.POINTER_DOWN, handlePointerDown) scene.input.on(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove) scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp) scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
@ -102,6 +134,9 @@ export function useGameControlsComposable(scene: Phaser.Scene, layer: Phaser.Til
} }
const cleanupControls = () => { const cleanupControls = () => {
stopMovementLoop()
pressedKeys.clear()
scene.input.off(Phaser.Input.Events.POINTER_DOWN, handlePointerDown) scene.input.off(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove) scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
scene.input.off(Phaser.Input.Events.POINTER_UP, handlePointerUp) scene.input.off(Phaser.Input.Events.POINTER_UP, handlePointerUp)

View File

@ -1,12 +1,13 @@
import config from '@/application/config'
import { Direction } from '@/application/enums' import { Direction } from '@/application/enums'
import { type MapCharacter } from '@/application/types' import { type MapCharacter } from '@/application/types'
import { loadSpriteTextures } from '@/composables/gameComposable' import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/services/mapService'
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/mapComposable' import { loadSpriteTextures } from '@/services/textureService'
import { CharacterTypeStorage } from '@/storage/storages' import { CharacterTypeStorage } from '@/storage/storages'
import { refObj } from 'phavuer' import { refObj } from 'phavuer'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
export function useCharacterSprite(scene: Phaser.Scene, tilemap: Phaser.Tilemaps.Tilemap, mapCharacter: MapCharacter) { export function useCharacterSpriteComposable(scene: Phaser.Scene, tilemap: Phaser.Tilemaps.Tilemap, mapCharacter: MapCharacter) {
const characterContainer = refObj<Phaser.GameObjects.Container>() const characterContainer = refObj<Phaser.GameObjects.Container>()
const characterSpriteId = ref('') const characterSpriteId = ref('')
const characterSprite = refObj<Phaser.GameObjects.Sprite>() const characterSprite = refObj<Phaser.GameObjects.Sprite>()
@ -17,10 +18,10 @@ export function useCharacterSprite(scene: Phaser.Scene, tilemap: Phaser.Tilemaps
const tween = ref<Phaser.Tweens.Tween | null>(null) const tween = ref<Phaser.Tweens.Tween | null>(null)
const updateIsometricDepth = (positionX: number, positionY: number) => { const updateIsometricDepth = (positionX: number, positionY: number) => {
isometricDepth.value = calculateIsometricDepth(positionX, positionY, 28, 94, true) isometricDepth.value = calculateIsometricDepth(positionX, positionY)
} }
const updatePosition = (positionX: number, positionY: number, direction: Direction) => { const updatePosition = (positionX: number, positionY: number) => {
const newPositionX = tileToWorldX(tilemap, positionX, positionY) const newPositionX = tileToWorldX(tilemap, positionX, positionY)
const newPositionY = tileToWorldY(tilemap, positionX, positionY) const newPositionY = tileToWorldY(tilemap, positionX, positionY)
@ -35,9 +36,10 @@ export function useCharacterSprite(scene: Phaser.Scene, tilemap: Phaser.Tilemaps
tween.value.stop() tween.value.stop()
} }
const distance = Math.sqrt(Math.pow(newPositionX - currentPositionX.value, 2) + Math.pow(newPositionY - currentPositionY.value, 2)) const tileDistance = Math.sqrt(Math.pow((newPositionX - currentPositionX.value) / config.tile_size.width, 2) + Math.pow((newPositionY - currentPositionY.value) / config.tile_size.height, 2))
const baseSpeed = 150 // pixels per second
const duration = (distance / baseSpeed) * 1000 // Convert to milliseconds const baseDuration = 300 // milliseconds per tile
const duration = Math.min(baseDuration * tileDistance, baseDuration)
tween.value = tilemap.scene.tweens.add({ tween.value = tilemap.scene.tweens.add({
targets: characterContainer.value, targets: characterContainer.value,
@ -46,22 +48,42 @@ export function useCharacterSprite(scene: Phaser.Scene, tilemap: Phaser.Tilemaps
duration, duration,
ease: 'Linear', ease: 'Linear',
onStart: () => { onStart: () => {
if (direction === Direction.POSITIVE) {
updateIsometricDepth(positionX, positionY) updateIsometricDepth(positionX, positionY)
}
}, },
onUpdate: () => { onUpdate: () => {
updateIsometricDepth(positionX, positionY)
currentPositionX.value = characterContainer.value?.x ?? currentPositionX.value currentPositionX.value = characterContainer.value?.x ?? currentPositionX.value
currentPositionY.value = characterContainer.value?.y ?? currentPositionY.value currentPositionY.value = characterContainer.value?.y ?? currentPositionY.value
}, },
onComplete: () => { onComplete: () => {
if (direction === Direction.NEGATIVE) {
updateIsometricDepth(positionX, positionY) updateIsometricDepth(positionX, positionY)
} }
}
}) })
} }
const playAnimation = (animation: string, loop = false, ignoreIfPlaying = true) => {
if (!characterSprite.value || !characterSpriteId.value) return
const fullAnimationName = `${characterSpriteId.value}-${animation}_${currentDirection.value}`
// Remove any existing animation complete listeners
characterSprite.value.off(Phaser.Animations.Events.ANIMATION_COMPLETE)
// Add new listener
characterSprite.value.on(Phaser.Animations.Events.ANIMATION_COMPLETE, () => {
characterSprite.value!.setFrame(0)
characterSprite.value!.setTexture(charTexture.value)
})
characterSprite.value.anims.play(
{
key: fullAnimationName,
repeat: loop ? -1 : 0
},
ignoreIfPlaying
)
}
const calcDirection = (oldPositionX: number, oldPositionY: number, newPositionX: number, newPositionY: number): Direction => { const calcDirection = (oldPositionX: number, oldPositionY: number, newPositionX: number, newPositionY: number): Direction => {
if (newPositionY < oldPositionY || newPositionX < oldPositionX) return Direction.NEGATIVE if (newPositionY < oldPositionY || newPositionX < oldPositionX) return Direction.NEGATIVE
if (newPositionX > oldPositionX || newPositionY > oldPositionY) return Direction.POSITIVE if (newPositionX > oldPositionX || newPositionY > oldPositionY) return Direction.POSITIVE
@ -112,7 +134,8 @@ export function useCharacterSprite(scene: Phaser.Scene, tilemap: Phaser.Tilemaps
characterSprite.value.setFlipX(isFlippedX.value) characterSprite.value.setFlipX(isFlippedX.value)
} }
updatePosition(mapCharacter.character.positionX, mapCharacter.character.positionY, mapCharacter.character.rotation) updatePosition(mapCharacter.character.positionX, mapCharacter.character.positionY)
updateIsometricDepth(mapCharacter.character.positionX, mapCharacter.character.positionY)
} }
const cleanup = () => { const cleanup = () => {
@ -128,6 +151,7 @@ export function useCharacterSprite(scene: Phaser.Scene, tilemap: Phaser.Tilemaps
isometricDepth, isometricDepth,
isFlippedX, isFlippedX,
updatePosition, updatePosition,
playAnimation,
calcDirection, calcDirection,
updateSprite, updateSprite,
initializeSprite, initializeSprite,

View File

@ -1,8 +1,9 @@
import type { Map, MapObject } from '@/application/types' import type { Map, MapObject, PlacedMapObject, UUID } from '@/application/types'
import { useGameStore } from '@/stores/gameStore'
import { ref } from 'vue' import { ref } from 'vue'
export type TeleportSettings = { export type TeleportSettings = {
toMapId: string toMap: string
toPositionX: number toPositionX: number
toPositionY: number toPositionY: number
toRotation: number toRotation: number
@ -12,16 +13,26 @@ const currentMap = ref<Map | null>(null)
const active = ref(false) const active = ref(false)
const tool = ref('move') const tool = ref('move')
const drawMode = ref('tile') const drawMode = ref('tile')
const inputMode = ref('tap')
const selectedTile = ref('') const selectedTile = ref('')
const isPlacedMapObjectPreviewEnabled = ref(true)
const selectedMapObject = ref<MapObject | null>(null) const selectedMapObject = ref<MapObject | null>(null)
const movingPlacedObject = ref<PlacedMapObject | null>(null)
const selectedPlacedObject = ref<PlacedMapObject | null>(null)
const shouldClearTiles = ref(false) const shouldClearTiles = ref(false)
const teleportSettings = ref<TeleportSettings>({ const teleportSettings = ref<TeleportSettings>({
toMapId: '', toMap: '1000',
toPositionX: 0, toPositionX: 0,
toPositionY: 0, toPositionY: 0,
toRotation: 0 toRotation: 0
}) })
/**
* We can update origin X and Y in src/components/gameMaster/mapEditor/partials/SelectedPlacedMapObject.vue
* and this will trigger a refresh for spawned mao objects
*/
const refreshMapObject = ref(0)
export function useMapEditorComposable() { export function useMapEditorComposable() {
const loadMap = (map: Map) => { const loadMap = (map: Map) => {
currentMap.value = map currentMap.value = map
@ -33,16 +44,15 @@ export function useMapEditorComposable() {
} }
} }
const clearMap = () => {
if (!currentMap.value) return
currentMap.value.placedMapObjects = []
currentMap.value.mapEventTiles = []
currentMap.value.tiles = []
}
const toggleActive = () => { const toggleActive = () => {
if (active.value) reset() if (active.value) reset()
active.value = !active.value active.value = !active.value
const gameStore = useGameStore()
gameStore.uiSettings.isGmPanelOpen = false
}
const togglePlacedMapObjectPreview = () => {
isPlacedMapObjectPreviewEnabled.value = !isPlacedMapObjectPreviewEnabled.value
} }
const setTool = (newTool: string) => { const setTool = (newTool: string) => {
@ -53,12 +63,16 @@ export function useMapEditorComposable() {
drawMode.value = mode drawMode.value = mode
} }
const setInputMode = (mode: string) => {
inputMode.value = mode
}
const setSelectedTile = (tile: string) => { const setSelectedTile = (tile: string) => {
selectedTile.value = tile selectedTile.value = tile
} }
const setSelectedMapObject = (object: MapObject) => { const setSelectedMapObject = (mapObject: MapObject) => {
selectedMapObject.value = object selectedMapObject.value = mapObject
} }
const setTeleportSettings = (settings: TeleportSettings) => { const setTeleportSettings = (settings: TeleportSettings) => {
@ -73,12 +87,20 @@ export function useMapEditorComposable() {
shouldClearTiles.value = false shouldClearTiles.value = false
} }
function triggerMapObjectRefresh() {
refreshMapObject.value++ // Increment to trigger watchers
}
const reset = () => { const reset = () => {
tool.value = 'move' tool.value = 'move'
drawMode.value = 'tile' drawMode.value = 'tile'
inputMode.value = 'tap'
selectedTile.value = '' selectedTile.value = ''
isPlacedMapObjectPreviewEnabled.value = false
selectedMapObject.value = null selectedMapObject.value = null
selectedPlacedObject.value = null
shouldClearTiles.value = false shouldClearTiles.value = false
refreshMapObject.value = 0
} }
return { return {
@ -87,23 +109,30 @@ export function useMapEditorComposable() {
active, active,
tool, tool,
drawMode, drawMode,
inputMode,
selectedTile, selectedTile,
isPlacedMapObjectPreviewEnabled,
selectedMapObject, selectedMapObject,
movingPlacedObject,
selectedPlacedObject,
shouldClearTiles, shouldClearTiles,
teleportSettings, teleportSettings,
refreshMapObject,
// Methods // Methods
loadMap, loadMap,
updateProperty, updateProperty,
clearMap,
toggleActive, toggleActive,
setTool, setTool,
setDrawMode, setDrawMode,
setInputMode,
setSelectedTile, setSelectedTile,
togglePlacedMapObjectPreview,
setSelectedMapObject, setSelectedMapObject,
setTeleportSettings, setTeleportSettings,
triggerClearTiles, triggerClearTiles,
resetClearTilesFlag, resetClearTilesFlag,
triggerMapObjectRefresh,
reset reset
} }
} }

View File

@ -0,0 +1,164 @@
import { SoundStorage } from '@/storage/storages'
interface CachedSound {
id: string
name: string
base64: string
}
// Core storage instances
const soundStorage = new SoundStorage()
const activeSounds = new Map<string, HTMLAudioElement[]>()
const audioCache = new Map<string, HTMLAudioElement>()
export function useSoundComposable() {
/**
* Converts a sound URL to base64 format
*/
async function soundToBase64(url: string): Promise<string> {
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const blob = await response.blob()
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onloadend = () => resolve((reader.result as string).split(',')[1])
reader.onerror = reject
reader.readAsDataURL(blob)
})
} catch (error) {
console.error('Error converting sound to base64:', error)
throw error
}
}
/**
* Preloads a sound file and caches it for future use
*/
async function preloadSound(soundUrl: string): Promise<HTMLAudioElement> {
if (audioCache.has(soundUrl)) {
return audioCache.get(soundUrl)!
}
let audio: HTMLAudioElement
const cachedSound = await soundStorage.getById(soundUrl)
if (cachedSound) {
audio = new Audio(`data:audio/mpeg;base64,${cachedSound.base64}`)
} else {
try {
const base64 = await soundToBase64(soundUrl)
const soundData: CachedSound = {
id: soundUrl,
name: soundUrl.split('/').pop() || soundUrl,
base64
}
await soundStorage.add(soundData)
audio = new Audio(`data:audio/mpeg;base64,${base64}`)
} catch (error) {
console.error(`Failed to load and cache sound ${soundUrl}:`, error)
audio = new Audio(soundUrl)
}
}
audio.load()
audioCache.set(soundUrl, audio)
return audio
}
/**
* Plays a sound with optional looping and duplicate prevention
*/
async function playSound(soundUrl: string, loop: boolean = false, ignoreIfPlaying: boolean = false): Promise<void> {
try {
const playingSounds = activeSounds.get(soundUrl) || []
if (ignoreIfPlaying && playingSounds.some((audio) => !audio.paused)) {
return
}
stopSound(soundUrl)
const audio = await preloadSound(soundUrl)
const playingAudio = audio.cloneNode() as HTMLAudioElement
playingAudio.loop = loop
if (!activeSounds.has(soundUrl)) {
activeSounds.set(soundUrl, [])
}
activeSounds.get(soundUrl)!.push(playingAudio)
// Cleanup when sound ends
playingAudio.addEventListener(
'ended',
() => {
const sounds = activeSounds.get(soundUrl)
if (!sounds) return
const index = sounds.indexOf(playingAudio)
if (index > -1) {
sounds.splice(index, 1)
}
if (sounds.length === 0) {
activeSounds.delete(soundUrl)
}
},
{ once: true }
)
await playingAudio.play()
} catch (error) {
console.error(`Failed to play sound ${soundUrl}:`, error)
}
}
/**
* Stops all instances of a specific sound
*/
function stopSound(soundUrl: string): void {
const sounds = activeSounds.get(soundUrl)
if (!sounds) return
sounds.forEach((audio) => {
audio.pause()
audio.currentTime = 0
})
activeSounds.delete(soundUrl)
}
/**
* Stops all currently playing sounds
*/
function stopAllSounds(): void {
activeSounds.forEach((_, url) => stopSound(url))
activeSounds.clear()
}
/**
* Clears all cached sounds and stops playback
*/
async function clearSoundCache(): Promise<void> {
stopAllSounds()
audioCache.clear()
await soundStorage.reset()
}
/**
* Preloads multiple sounds simultaneously
*/
async function preloadSounds(soundUrls: string[]): Promise<HTMLAudioElement[]> {
return Promise.all(soundUrls.map(preloadSound))
}
return {
playSound,
stopSound,
stopAllSounds,
clearSoundCache,
preloadSounds
}
}

View File

@ -4,26 +4,32 @@ import type { TileAnalysisResult, TileWorkerMessage } from '@/types/tileTypes'
import { ref } from 'vue' import { ref } from 'vue'
// Constants for image processing // Constants for image processing
const DOWNSCALE_WIDTH = 32 const DOWNSCALE_WIDTH = 16
const DOWNSCALE_HEIGHT = 16 const DOWNSCALE_HEIGHT = 8
const COLOR_SIMILARITY_THRESHOLD = 30 const COLOR_SIMILARITY_THRESHOLD = 30
const EDGE_SIMILARITY_THRESHOLD = 20 const EDGE_SIMILARITY_THRESHOLD = 20
const BATCH_SIZE = 4 const BATCH_SIZE = 8
export function useTileProcessingComposable() { export function useTileProcessingComposable() {
const tileAnalysisCache = ref<Map<string, { color: { r: number; g: number; b: number }; edge: number; namePrefix: string }>>(new Map()) const tileAnalysisCache = ref<Map<string, { color: { r: number; g: number; b: number }; edge: number; namePrefix: string }>>(new Map())
const processingQueue = ref<Tile[]>([]) const processingQueue = ref<Tile[]>([])
let isProcessing = false let isProcessing = false
const worker = new Worker(new URL('@/workers/tileAnalyzerWorker.ts', import.meta.url), { type: 'module' })
const NUM_WORKERS = 4
const workers = Array.from({ length: NUM_WORKERS }, () => new Worker(new URL('@/workers/tileAnalyzerWorker.ts', import.meta.url), { type: 'module' }))
let currentWorker = 0
// Modify worker message handling
workers.forEach((worker) => {
worker.onmessage = (e: MessageEvent<TileAnalysisResult>) => { worker.onmessage = (e: MessageEvent<TileAnalysisResult>) => {
const { tileId, color, edge, namePrefix } = e.data const { tileId, color, edge, namePrefix } = e.data
tileAnalysisCache.value.set(tileId, { color, edge, namePrefix }) tileAnalysisCache.value.set(tileId, { color, edge, namePrefix })
isProcessing = false isProcessing = false
processBatch() processBatch()
} }
})
async function processTileAsync(tile: Tile): Promise<void> { async function processTileAsync(tile: Tile, worker: Worker): Promise<void> {
if (tileAnalysisCache.value.has(tile.id)) return if (tileAnalysisCache.value.has(tile.id)) return
return new Promise((resolve) => { return new Promise((resolve) => {
@ -60,7 +66,12 @@ export function useTileProcessingComposable() {
isProcessing = true isProcessing = true
const batch = processingQueue.value.splice(0, BATCH_SIZE) const batch = processingQueue.value.splice(0, BATCH_SIZE)
Promise.all(batch.map((tile) => processTileAsync(tile))).then(() => { Promise.all(
batch.map((tile) => {
currentWorker = (currentWorker + 1) % NUM_WORKERS
return processTileAsync(tile, workers[currentWorker])
})
).then(() => {
isProcessing = false isProcessing = false
if (processingQueue.value.length > 0) { if (processingQueue.value.length > 0) {
setTimeout(processBatch, 0) setTimeout(processBatch, 0)
@ -87,7 +98,7 @@ export function useTileProcessingComposable() {
} }
function cleanup() { function cleanup() {
worker.terminate() workers.forEach((worker) => worker.terminate())
} }
return { return {

View File

@ -0,0 +1,76 @@
import config from '@/application/config'
import { SocketEvent } from '@/application/enums'
import { useCookies } from '@vueuse/integrations/useCookies'
import { io, Socket } from 'socket.io-client'
import { ref, shallowRef } from 'vue'
class SocketManager {
private static instance: SocketManager
private _connection = shallowRef<Socket | null>(null)
private _token = ref('')
private constructor() {}
public static getInstance(): SocketManager {
if (!SocketManager.instance) {
SocketManager.instance = new SocketManager()
}
return SocketManager.instance
}
public get connection() {
return this._connection.value
}
public get token() {
return this._token.value
}
public setToken(token: string) {
this._token.value = token
}
public initConnection(): Socket {
if (this._connection.value) return this._connection.value
const socket = io(config.server_endpoint, {
secure: config.environment === 'production',
withCredentials: true,
transports: ['websocket'],
reconnectionAttempts: 5
})
this._connection.value = socket
return socket
}
public disconnect(): void {
if (!this._connection.value) return
this._connection.value.off(SocketEvent.CONNECT_ERROR)
this._connection.value.off(SocketEvent.RECONNECT_FAILED)
this._connection.value.off(SocketEvent.DATE)
this._connection.value.disconnect()
useCookies().remove('token', {
domain: config.domain
})
this._connection.value = null
this._token.value = ''
}
public emit(event: string, ...args: any[]): void {
this._connection.value?.emit(event, ...args)
}
public on(event: string, callback: (...args: any[]) => void): void {
this._connection.value?.on(event, callback)
}
public off(event: string, callback?: (...args: any[]) => void): void {
this._connection.value?.off(event, callback)
}
}
export const socketManager = SocketManager.getInstance()

View File

@ -1,5 +1,4 @@
import config from '@/application/config' import config from '@/application/config'
import { getDomain } from '@/application/utilities'
import { useCookies } from '@vueuse/integrations/useCookies' import { useCookies } from '@vueuse/integrations/useCookies'
import axios from 'axios' import axios from 'axios'
@ -22,7 +21,7 @@ export async function login(username: string, password: string) {
try { try {
const response = await axios.post(`${config.server_endpoint}/login`, { username, password }) const response = await axios.post(`${config.server_endpoint}/login`, { username, password })
useCookies().set('token', response.data.data.token as string, { useCookies().set('token', response.data.data.token as string, {
domain: getDomain() domain: config.domain
}) })
return { success: true, token: response.data.data.token } return { success: true, token: response.data.data.token }
} catch (error: any) { } catch (error: any) {

View File

@ -1,7 +1,7 @@
import config from '@/application/config' import config from '@/application/config'
import type { HttpResponse, TextureData, Tile as TileT, UUID } from '@/application/types' import type { MapObject, Map as MapT, TextureData, Tile as TileT, UUID } from '@/application/types'
import { unduplicateArray } from '@/application/utilities' import { unduplicateArray } from '@/application/utilities'
import { loadTexture } from '@/composables/gameComposable' import { loadTexture } from '@/services/textureService'
import { MapStorage, TileStorage } from '@/storage/storages' import { MapStorage, TileStorage } from '@/storage/storages'
import Tilemap = Phaser.Tilemaps.Tilemap import Tilemap = Phaser.Tilemaps.Tilemap
@ -39,11 +39,6 @@ export function tileToWorldY(layer: TilemapLayer | Tilemap, positionX: number, p
/** /**
* Can also be used to replace tiles * Can also be used to replace tiles
* @param map
* @param layer
* @param positionX
* @param positionY
* @param tileName
*/ */
export function placeTile(map: Tilemap, layer: TilemapLayer, positionX: number, positionY: number, tileName: string) { export function placeTile(map: Tilemap, layer: TilemapLayer, positionX: number, positionY: number, tileName: string) {
let tileImg = map.getTileset(tileName) as Tileset let tileImg = map.getTileset(tileName) as Tileset
@ -53,8 +48,8 @@ export function placeTile(map: Tilemap, layer: TilemapLayer, positionX: number,
layer.putTileAt(tileImg.firstgid, positionX, positionY) layer.putTileAt(tileImg.firstgid, positionX, positionY)
} }
export function setLayerTiles(map: Tilemap, layer: TilemapLayer, tiles: string[][]) { export function placeTiles(map: Tilemap, layer: TilemapLayer, tiles: string[][]) {
if (!tiles) return if (!map || !layer || !tiles) return
tiles.forEach((row: string[], y: number) => { tiles.forEach((row: string[], y: number) => {
row.forEach((tile: string, x: number) => { row.forEach((tile: string, x: number) => {
@ -67,18 +62,15 @@ export function createTileArray(width: number, height: number, tile: string = 'b
return Array.from({ length: height }, () => Array.from({ length: width }, () => tile)) return Array.from({ length: height }, () => Array.from({ length: width }, () => tile))
} }
export const calculateIsometricDepth = (positionX: number, positionY: number, width: number = 0, height: number = 0, isCharacter: boolean = false) => { export const calculateIsometricDepth = (positionX: number, positionY: number, pivotPoints: { x: number; y: number }[] = []) => {
const baseDepth = positionX + positionY return Math.max(positionX + positionY)
if (isCharacter) {
return baseDepth // @TODO: Fix collision, this is a hack
}
return baseDepth + (width + height) / (2 * config.tile_size.width)
} }
async function getTiles(tiles: TileT[], scene: Phaser.Scene) { async function loadTileTextures(tiles: TileT[], scene: Phaser.Scene) {
// Load each tile into the scene // Load each tile into the scene
for (const tile of tiles) { for (let tile of tiles) {
if (!tile) continue if (!tile?.id || !tile?.updatedAt) continue
const textureData = { const textureData = {
key: tile.id, key: tile.id,
data: '/textures/tiles/' + tile.id + '.png', data: '/textures/tiles/' + tile.id + '.png',
@ -89,28 +81,74 @@ async function getTiles(tiles: TileT[], scene: Phaser.Scene) {
} }
} }
export async function loadMapTilesIntoScene(map_id: UUID, scene: Phaser.Scene) { export async function loadTileTexturesFromMapTileArray(map_id: string, scene: Phaser.Scene) {
const tileStorage = new TileStorage()
const mapStorage = new MapStorage() const mapStorage = new MapStorage()
const map = await mapStorage.get(map_id) const tileStorage = new TileStorage()
const map = await mapStorage.getById(map_id)
if (!map) return if (!map) return
const tileArray = unduplicateArray(map.tiles) const tileArray = unduplicateArray(map.tiles)
const tiles = await tileStorage.getByIds(tileArray) const tiles = await tileStorage.getByIds(tileArray)
if (!tiles) return
await getTiles(tiles, scene) await loadTileTextures(tiles, scene)
} }
export async function loadTilesIntoScene(tileIds: string[], scene: Phaser.Scene) { export async function loadAllTileTextures(scene: Phaser.Scene) {
const tileStorage = new TileStorage()
const tiles = await tileStorage.getByIds(tileIds)
await getTiles(tiles, scene)
}
export async function loadAllTilesIntoScene(scene: Phaser.Scene) {
const tileStorage = new TileStorage() const tileStorage = new TileStorage()
const tiles = await tileStorage.getAll() const tiles = await tileStorage.getAll()
if (!tiles) return
await getTiles(tiles, scene) await loadTileTextures(tiles, scene)
}
export async function loadMapObjectTextures(mapObjects: MapObject[], scene: Phaser.Scene) {
for (const mapObject of mapObjects) {
const textureData = {
key: mapObject.id,
data: '/textures/map_objects/' + mapObject.id + '.png',
group: 'map_objects',
updatedAt: mapObject.updatedAt,
frameWidth: mapObject.frameWidth,
frameHeight: mapObject.frameHeight
} as TextureData
await loadTexture(scene, textureData)
}
}
export function createTileMap(scene: Phaser.Scene, map: MapT) {
const mapConfig = new Phaser.Tilemaps.MapData({
width: map.width,
height: map.height,
tileWidth: config.tile_size.width,
tileHeight: config.tile_size.height,
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
format: Phaser.Tilemaps.Formats.ARRAY_2D
})
return new Phaser.Tilemaps.Tilemap(scene, mapConfig)
}
export function createTileLayer(tileMap: Phaser.Tilemaps.Tilemap, tilesArray: string[]) {
// Load tiles into tileset
const tilesetImages = tilesArray.map((tile: string, index: number) => {
return tileMap.addTilesetImage(tile, tile, config.tile_size.width, config.tile_size.height, 1, 2, index + 1, { x: 0, y: -config.tile_size.height })
})
// Add blank tile
tilesetImages.push(tileMap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.width, config.tile_size.height, 1, 2, 0, { x: 0, y: -config.tile_size.height }))
// Create layer
const layer = tileMap.createBlankLayer('tiles', tilesetImages as Tileset[], 0, config.tile_size.height) as Phaser.Tilemaps.TilemapLayer
layer.setDepth(0)
layer.setCullPadding(2, 2)
return layer
}
//Recursive Array Clone
export function cloneArray(arr: any[]): any[] {
return arr.map((item) => (item instanceof Array ? cloneArray(item) : item))
} }

View File

@ -60,7 +60,7 @@ export async function loadSpriteTextures(scene: Phaser.Scene, sprite_id: string)
if (!sprite_id) return false if (!sprite_id) return false
const spriteStorage = new SpriteStorage() const spriteStorage = new SpriteStorage()
const sprite = await spriteStorage.get(sprite_id) const sprite = await spriteStorage.getById(sprite_id)
if (!sprite) { if (!sprite) {
console.error('Failed to load sprite:', sprite_id) console.error('Failed to load sprite:', sprite_id)

View File

@ -14,7 +14,7 @@ export class BaseStorage<T extends { id: string }> {
async add(item: T, overwrite = false) { async add(item: T, overwrite = false) {
try { try {
const existing = await this.get(item.id) const existing = await this.getById(item.id)
if (existing && !overwrite) return if (existing && !overwrite) return
await this.dexie.table(this.tableName).put({ ...item }) await this.dexie.table(this.tableName).put({ ...item })
@ -39,7 +39,7 @@ export class BaseStorage<T extends { id: string }> {
} }
} }
async get(id: string): Promise<T | null> { async getById(id: string): Promise<T | null> {
try { try {
const item = await this.dexie.table(this.tableName).get(id) const item = await this.dexie.table(this.tableName).get(id)
return item || null return item || null

View File

@ -31,7 +31,7 @@ export class CharacterTypeStorage extends BaseStorage<any> {
} }
async getSpriteId(characterTypeId: string) { async getSpriteId(characterTypeId: string) {
const characterType = await this.get(characterTypeId) const characterType = await this.getById(characterTypeId)
return characterType?.sprite return characterType?.sprite
} }
} }
@ -42,7 +42,13 @@ export class CharacterHairStorage extends BaseStorage<any> {
} }
async getSpriteId(characterTypeId: string) { async getSpriteId(characterTypeId: string) {
const characterType = await this.get(characterTypeId) const characterType = await this.getById(characterTypeId)
return characterType?.sprite return characterType?.sprite
} }
} }
export class SoundStorage extends BaseStorage<{ id: string; name: string; base64: string }> {
constructor() {
super('sounds', 'id, name, createdAt, updatedAt')
}
}

View File

@ -1,16 +1,12 @@
import config from '@/application/config' import { SocketEvent } from '@/application/enums'
import type { Character, Notification, TextureData, User, WorldSettings } from '@/application/types' import type { Character, Notification, User, WorldSettings } from '@/application/types'
import { getDomain } from '@/application/utilities' import { socketManager } from '@/managers/SocketManager'
import { useCookies } from '@vueuse/integrations/useCookies'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { io, Socket } from 'socket.io-client'
export const useGameStore = defineStore('game', { export const useGameStore = defineStore('game', {
state: () => { state: () => {
return { return {
notifications: [] as Notification[], notifications: [] as Notification[],
token: '',
connection: null as Socket | null,
user: null as User | null, user: null as User | null,
character: null as Character | null, character: null as Character | null,
world: { world: {
@ -51,9 +47,6 @@ export const useGameStore = defineStore('game', {
removeNotification(id: string) { removeNotification(id: string) {
this.notifications = this.notifications.filter((notification: Notification) => notification.id !== id) this.notifications = this.notifications.filter((notification: Notification) => notification.id !== id)
}, },
setToken(token: string) {
this.token = token
},
setUser(user: User | null) { setUser(user: User | null) {
this.user = user this.user = user
}, },
@ -73,40 +66,34 @@ export const useGameStore = defineStore('game', {
this.uiSettings.isCharacterProfileOpen = !this.uiSettings.isCharacterProfileOpen this.uiSettings.isCharacterProfileOpen = !this.uiSettings.isCharacterProfileOpen
}, },
initConnection() { initConnection() {
this.connection = io(config.server_endpoint, { const socket = socketManager.initConnection()
secure: !config.development,
withCredentials: true,
transports: ['websocket'],
reconnectionAttempts: 5
})
// #99 - If we can't connect, disconnect // Handle connect error
this.connection.on('connect_error', () => { socket.on(SocketEvent.CONNECT_ERROR, () => {
this.disconnectSocket() this.disconnectSocket()
}) })
// Let the server know the user is logged in // Handle failed reconnection
this.connection.emit('login') socket.on(SocketEvent.RECONNECT_FAILED, () => {
this.disconnectSocket()
})
// set user // Emit login event
this.connection.on('logged_in', (user: User) => { socketManager.emit(SocketEvent.LOGIN)
// Handle logged in event
socketManager.on(SocketEvent.LOGGED_IN, (user: User) => {
this.setUser(user) this.setUser(user)
}) })
// When we can't reconnect, disconnect // Handle date updates
this.connection.on('reconnect_failed', () => { socketManager.on(SocketEvent.DATE, (data: Date) => {
this.disconnectSocket() this.world.date = new Date(data)
}) })
}, },
disconnectSocket() { disconnectSocket() {
this.connection?.disconnect() socketManager.disconnect()
useCookies().remove('token', {
domain: getDomain()
})
this.connection = null
this.token = ''
this.user = null this.user = null
this.character = null this.character = null

View File

@ -2,7 +2,7 @@ import type { MapObject, Map as MapT } from '@/application/types'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
export type TeleportSettings = { export type TeleportSettings = {
toMapId: string toMap: string
toPositionX: number toPositionX: number
toPositionY: number toPositionY: number
toRotation: number toRotation: number
@ -18,7 +18,7 @@ export const useMapEditorStore = defineStore('mapEditor', {
selectedMapObject: null as MapObject | null, selectedMapObject: null as MapObject | null,
shouldClearTiles: false, shouldClearTiles: false,
teleportSettings: { teleportSettings: {
toMapId: '', toMap: '',
toPositionX: 0, toPositionX: 0,
toPositionY: 0, toPositionY: 0,
toRotation: 0 toRotation: 0

View File

@ -5,8 +5,7 @@ export const useMapStore = defineStore('map', {
state: () => { state: () => {
return { return {
mapId: '', mapId: '',
characters: [] as MapCharacter[], characters: [] as MapCharacter[]
characterLoaded: false
} }
}, },
getters: { getters: {
@ -18,7 +17,7 @@ export const useMapStore = defineStore('map', {
} }
}, },
actions: { actions: {
setMapId(mapId: UUID) { setMapId(mapId: string) {
this.mapId = mapId this.mapId = mapId
}, },
setCharacters(characters: MapCharacter[]) { setCharacters(characters: MapCharacter[]) {
@ -27,12 +26,7 @@ export const useMapStore = defineStore('map', {
addCharacter(character: MapCharacter) { addCharacter(character: MapCharacter) {
this.characters.push(character) this.characters.push(character)
}, },
updateCharacter(updatedCharacter: MapCharacter) { updateCharacterProperty<K extends keyof MapCharacter>(characterId: string, property: K, value: MapCharacter[K]) {
const index = this.characters.findIndex((char) => char.character.id === updatedCharacter.character.id)
if (index !== -1) this.characters[index] = updatedCharacter
},
// Property is mapCharacter key
updateCharacterProperty<K extends keyof MapCharacter>(characterId: UUID, property: K, value: MapCharacter[K]) {
const character = this.characters.find((char) => char.character.id === characterId) const character = this.characters.find((char) => char.character.id === characterId)
if (character) { if (character) {
character[property] = value character[property] = value
@ -41,22 +35,18 @@ export const useMapStore = defineStore('map', {
removeCharacter(characterId: UUID) { removeCharacter(characterId: UUID) {
this.characters = this.characters.filter((char) => char.character.id !== characterId) this.characters = this.characters.filter((char) => char.character.id !== characterId)
}, },
setCharacterLoaded(loaded: boolean) { updateCharacterPosition([characterId, posX, posY, rot, isMoving]: [UUID, number, number, number, boolean]) {
this.characterLoaded = loaded const character = this.characters.find((char) => char.character.id === characterId)
},
updateCharacterPosition(data: { characterId: UUID; positionX: number; positionY: number; rotation: number; isMoving: boolean }) {
const character = this.characters.find((char) => char.character.id === data.characterId)
if (character) { if (character) {
character.character.positionX = data.positionX character.character.positionX = posX
character.character.positionY = data.positionY character.character.positionY = posY
character.character.rotation = data.rotation character.character.rotation = rot
character.isMoving = data.isMoving character.isMoving = isMoving
} }
}, },
reset() { reset() {
this.mapId = '' this.mapId = ''
this.characters = [] this.characters = []
this.characterLoaded = false
} }
} }
}) })

View File

@ -12,7 +12,6 @@ function analyzeTile(imageData: ImageData, tileId: string, tileName: string): Ti
const { r, g, b } = getDominantColorFast(imageData) const { r, g, b } = getDominantColorFast(imageData)
const edge = getEdgeComplexityFast(imageData) const edge = getEdgeComplexityFast(imageData)
const namePrefix = tileName.split('_')[0] const namePrefix = tileName.split('_')[0]
return { return {
tileId, tileId,
color: { r, g, b }, color: { r, g, b },
@ -53,16 +52,14 @@ function getEdgeComplexityFast(imageData: ImageData) {
const height = imageData.height const height = imageData.height
let edgePixels = 0 let edgePixels = 0
for (let y = 0; y < height; y += PIXEL_SAMPLE_RATE) { // Only check every other row/column
for (let x = 0; x < width; x += PIXEL_SAMPLE_RATE) { for (let y = 0; y < height; y += PIXEL_SAMPLE_RATE * 2) {
for (let x = 0; x < width; x += PIXEL_SAMPLE_RATE * 2) {
const i = (y * width + x) * 4 const i = (y * width + x) * 4
if ( if (data[i + 3] > 0 && (x === 0 || y === 0 || x >= width - PIXEL_SAMPLE_RATE || y >= height - PIXEL_SAMPLE_RATE || data[i - 4 * PIXEL_SAMPLE_RATE + 3] === 0)) {
data[i + 3] > 0 &&
(x === 0 || y === 0 || x >= width - PIXEL_SAMPLE_RATE || y >= height - PIXEL_SAMPLE_RATE || data[i - 4 * PIXEL_SAMPLE_RATE + 3] === 0 || data[i + 4 * PIXEL_SAMPLE_RATE + 3] === 0 || data[i - width * 4 * PIXEL_SAMPLE_RATE + 3] === 0 || data[i + width * 4 * PIXEL_SAMPLE_RATE + 3] === 0)
) {
edgePixels++ edgePixels++
} }
} }
} }
return edgePixels * PIXEL_SAMPLE_RATE return edgePixels * PIXEL_SAMPLE_RATE * 2
} }

View File

@ -2,13 +2,37 @@ import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'; import vue from '@vitejs/plugin-vue';
import viteCompression from 'vite-plugin-compression'; import viteCompression from 'vite-plugin-compression';
import {ViteImageOptimizer} from "vite-plugin-image-optimizer";
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
vue(), vue(),
viteCompression() viteCompression({
algorithm: 'gzip',
ext: '.gz',
threshold: 10240 // Only compress files larger than 10KB
}),
ViteImageOptimizer()
], ],
build: {
minify: 'terser', // Better minification
terserOptions: {
compress: {
drop_console: true, // Remove console.log in production
drop_debugger: true
}
},
rollupOptions: {
output: {
manualChunks: {
'vendor': ['vue'], // Split vendor chunks
// Add other large dependencies here
}
}
},
chunkSizeWarningLimit: 1000, // Increase chunk size warning limit if needed
},
resolve: { resolve: {
alias: { alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)) '@': fileURLToPath(new URL('./src', import.meta.url))