Compare commits

..

241 Commits

Author SHA1 Message Date
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
e53e154d16 npm run format 2025-02-01 16:18:33 +01:00
d65ceba66a Temp. depth sorting fix 2025-02-01 16:14:15 +01:00
db426bb03e Fix for chat bubble 2025-02-01 15:48:42 +01:00
af26ca5e89 Added container for character for easier X and Y coord handling 2025-02-01 15:27:14 +01:00
e4b9bb4d61 Refactored Character.vue as preparation for attack anims. 2025-02-01 15:10:52 +01:00
d7f60d7bfc Don't include sprite dimensions in payload sent to server 2025-02-01 14:49:33 +01:00
cfdfa98379 Revert 2025-02-01 14:22:42 +01:00
63889a537a npm update 2025-02-01 14:22:37 +01:00
99bb1555a0 Listen for attack events. TODO: finish anim. handling 2025-02-01 04:30:07 +01:00
ac1396304f Added web worker to improve tile analysis performance 2025-02-01 03:11:13 +01:00
09ee9bf01d Show tile & mapObject list on top of toolbar buttons, re-enabled camera follow 2025-02-01 02:26:54 +01:00
09b458eeef Merge remote-tracking branch 'origin/main' into feature/#321 2025-02-01 01:43:12 +01:00
9d95562679 Implemented logic to walk with arrow keys 2025-02-01 01:43:01 +01:00
a9de031673 poc 2025-02-01 01:31:28 +01:00
8e81ce716b Minor tweaks and fixes 2025-01-31 23:11:15 +01:00
2c1db56cc4 Merge remote-tracking branch 'origin/feature/#321' 2025-01-31 23:02:01 +01:00
4fba3678d6 Added eraser to tool check 2025-01-31 23:01:19 +01:00
d29ca10ba9 Merge remote-tracking branch 'origin/feature/#321' 2025-01-31 23:00:16 +01:00
67f83c3447 Forgor styling on the other tiles, fix tab closing 2025-01-31 22:57:14 +01:00
8f82bad3fa Merge remote-tracking branch 'origin/feature/#321' 2025-01-31 22:39:34 +01:00
d665ac989c #321 - Made mapObjectList & tileList side panels 2025-01-31 22:35:13 +01:00
e389534e30 npm run format 2025-01-31 22:33:45 +01:00
7d3946e274 #96 - Renamed and refactored pointer handler composables in favor of arrow key movement. 2025-01-31 22:26:23 +01:00
0f46e3b6d2 #323 - Added alt to all images, also removed unused templates
Templates for old user panel hadnt been removed yet
2025-01-31 19:35:29 +01:00
6ca82733eb #324 - Fix Character profile image 2025-01-31 19:04:42 +01:00
eb61f45535 npm update 2025-01-31 18:49:03 +01:00
a181fc7fe3 npm update 2025-01-31 17:06:16 +01:00
507d4226ac Bug fix for character profile, greatly improved javascript 2025-01-31 03:23:23 +01:00
5dd9d1e7af Added temp. offset logic for easier sprite management 2025-01-31 02:16:19 +01:00
15f9e9861e Zoom 2025-01-31 01:55:52 +01:00
7fd334d414 Applied styling fix 2025-01-31 01:19:49 +01:00
c7d4b5f2c3 Updates TS hints 2025-01-31 01:18:49 +01:00
5747166822 Don't toggle accordion on button press 2025-01-31 01:17:44 +01:00
c010373e5b Updated loading indicator to match the other one we have 2025-01-30 18:35:51 +01:00
57ad9d4889 Don't update after closing sprite action img offset modal 2025-01-30 18:32:33 +01:00
f268ac9e5b Removed comment, updated types for sprite actions, minor modal component improvement, added components for better sprite management 2025-01-30 18:29:55 +01:00
fb6e2aa742 Undo and redo cycling through map edit history 2025-01-29 19:48:09 -06:00
8befce7ffb Update packages 2025-01-29 23:28:04 +01: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
014c08b17a Set depth to 9999 to always show above character sprite 2025-01-28 17:54:12 +01:00
bdbda6456c CharHair work 2025-01-28 16:32:16 +01:00
85537840ab Improved code 2025-01-28 05:14:37 +01:00
2b7082ac92 Bug fix caused by formatting 2025-01-28 05:01:52 +01:00
abc58bfa38 npm run format, removed container that character was in 2025-01-28 04:57:21 +01:00
027325f2bf Merge remote-tracking branch 'origin/main' into feature/map-refactor 2025-01-28 01:59:29 +01:00
517e92b07b Fully restored tile picker function 2025-01-27 14:49:27 -06:00
6bede8c44e Map editor ui fixes, switching back to game from map editor, draw/tap checkbox, and partial restoration of tile picker function 2025-01-27 14:33:29 -06:00
9e652868ca Prepping to work on Placed map objects 2025-01-27 00:18:46 -06:00
35f0dcca64 Hierarchical pointer handling logic 2025-01-26 21:21:24 -06:00
9618e07bc6 refactoring pointer events and input handling improvements 2025-01-26 20:50:34 -06:00
791830fd6f Refactoring of modalShown booleans 2025-01-26 18:56:13 -06:00
37acf1782b Removed isAnimated and isLooping fields 2025-01-27 01:51:53 +01:00
14aa696197 Changes to mapeditorcomposable, fix pencil and fill tool for tiles
Locked in, made mapeditor my bi-
2025-01-26 23:28:15 +01:00
cfac1d508b Minor change 2025-01-25 16:49:45 +01:00
82cfe5902f Bye 2025-01-25 16:49:25 +01:00
284ca6f64e Finish improving effects component 2025-01-25 15:44:49 +01:00
967cb1893d Re-applied changes 2025-01-25 15:38:20 +01:00
18db005bc1 Merge remote-tracking branch 'origin/main' into feature/map-refactor 2025-01-25 14:48:24 +01:00
c8473fc206 Removed console.log 2025-01-25 14:48:16 +01:00
b5e84c133a Merge remote-tracking branch 'origin/main' into feature/map-refactor
# Conflicts:
#	src/components/game/map/Effects.vue
2025-01-25 14:47:00 +01:00
3f75e4acd8 Merge remote-tracking branch 'origin/main' into feature/#313 2025-01-25 14:46:15 +01:00
765d0986bf Merge remote-tracking branch 'origin/feature/#313' 2025-01-25 14:45:48 +01:00
95c3a1af61 Merge remote-tracking branch 'origin/main' into feature/#313 2025-01-25 14:45:38 +01:00
45a9d8cfdb Formatted code 2025-01-25 14:45:09 +01:00
e0a48a089a Sorted imports 2025-01-25 13:49:52 +01:00
69f9944dc7 Use Dexie instead of Pinia store values in tileList inside mapEditor 2025-01-25 13:49:03 +01:00
9cdfcbcc56 npm run format 2025-01-25 13:24:26 +01:00
a614ee6241 Fixed value could be undefined ts error 2025-01-24 19:43:31 -06:00
7a51323682 Load map data inside a composable instead of Pinia store 2025-01-25 02:38:40 +01:00
807bc2066e Merge remote-tracking branch 'origin/main' into feature/map-refactor 2025-01-25 02:38:13 +01:00
fab0b08425 Load map data inside a composable instead of Pinia store 2025-01-25 02:38:02 +01:00
b074270c75 TS fix 2025-01-25 00:06:25 +01:00
30845b80e9 Minor formatting improvement 2025-01-25 00:02:22 +01:00
bde0f74f19 Re-added required package 2025-01-24 22:58:46 +01:00
bc685c63ef Cleaned code; overwrite cache if newer results are found 2025-01-23 20:40:41 +01:00
7a922261e3 Removed mapList from Pinia store, we fetch them from mapStorage 2025-01-23 20:04:47 +01:00
3936676f2c fog transparency fix 2025-01-23 13:04:43 -06:00
9744083dea Renamed downloadAndStore to downloadCache 2025-01-23 19:46:19 +01:00
176f31d84a Light calculations with minute percision 2025-01-23 02:38:30 -06:00
faad00b2a5 Simplified effects code and implemented smooth interpolation between day an night light values 2025-01-23 01:56:20 -06:00
a61e05592d Removed weather effects booleans, now to disable weather effects, setting the value to 0 is the way 2025-01-22 18:00:39 -06:00
5202251ac7 Tailwind improvement 2025-01-23 00:50:35 +01:00
5e2781b265 Removed placedMapObject depth field, cleaned package.json, creating & deleting maps now works with mapStorage 2025-01-23 00:47:34 +01:00
ebd6d96e54 Started streamlining map editor with regular map logic 2025-01-22 20:34:56 +01:00
41005735f9 Added version to dexie base class 2025-01-22 20:34:16 +01:00
78f1c6e6a0 Added font to loading text 2025-01-22 00:36:00 +01:00
2d48f83802 Load tiles before showing map editor UI 2025-01-22 00:31:14 +01:00
7dccb73698 TS improvement 2025-01-21 23:38:42 +01:00
7171112881 Moved effects.vue into map folder 2025-01-21 23:33:56 +01:00
9a601b7e2e Wait for map load before loading effects
Both command and mapSettings effects work again
2025-01-21 19:59:40 +01:00
5c68b02fff Removed redundant const 2025-01-19 00:06:36 +01:00
b86d9dd4ce Half working effects
Command triggered effects still brokey
2025-01-19 00:03:08 +01:00
93baa10acf Improvements 2025-01-14 02:39:41 +01:00
419cf319be TS fix 2025-01-14 02:17:35 +01:00
1cd7f28402 Removed debugging code 2025-01-13 15:02:53 +01:00
0657dbcb1b Tiny improvements 2025-01-13 14:51:28 +01:00
5cf7423a5c PlacedMapObjects.vue refactor for map 2025-01-13 11:40:41 +01:00
4d88917526 Loading placed map objects works again 2025-01-13 11:02:04 +01:00
8f07cf5093 Map editor tiles are now fully loaded from cache 2025-01-13 10:52:40 +01:00
367d536c52 more shit 2025-01-12 22:11:33 +01:00
3f8c911e9d Clean up 2025-01-12 20:54:59 +01:00
689e443b3d . 2025-01-12 03:24:31 +01:00
4fead371d7 sprite shit 2025-01-11 21:48:08 +01:00
b9bcfc719f Merge remote-tracking branch 'origin/feature/cache' 2025-01-10 23:23:00 +01:00
2b84bfcad2 #305 - Made all list types full height 2025-01-07 21:00:26 +01:00
f829cfb883 #309 - Removed mobile hidden styling from inner ui box 2025-01-07 20:20:52 +01:00
124 changed files with 4331 additions and 4818 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

View File

@ -1,13 +0,0 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
extends: ['plugin:vue/vue3-essential', 'eslint:recommended', '@vue/eslint-config-typescript', '@vue/eslint-config-prettier/skip-formatting'],
parserOptions: {
ecmaVersion: 'latest'
},
rules: {
'vue/multi-word-component-names': 'off'
}
}

View File

@ -1,7 +1,6 @@
{ {
"recommendations": [ "recommendations": [
"Vue.volar", "Vue.volar",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode" "esbenp.prettier-vscode"
] ]
} }

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;
}
}

2884
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,6 @@
"test:unit": "vitest", "test:unit": "vitest",
"build-only": "vite build", "build-only": "vite build",
"type-check": "vue-tsc --build --force", "type-check": "vue-tsc --build --force",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/" "format": "prettier --write src/"
}, },
"dependencies": { "dependencies": {
@ -19,27 +18,24 @@
"@vueuse/integrations": "^10.5.0", "@vueuse/integrations": "^10.5.0",
"axios": "^1.7.7", "axios": "^1.7.7",
"dexie": "^4.0.8", "dexie": "^4.0.8",
"phaser": "^3.86.0", "phaser": "3.87.0",
"pinia": "^2.1.6", "pinia": "^2.1.6",
"sharp": "^0.33.5",
"socket.io-client": "^4.8.0", "socket.io-client": "^4.8.0",
"universal-cookie": "^6.1.3", "universal-cookie": "^6.1.3",
"vite-plugin-image-optimizer": "^1.1.8",
"vue": "^3.5.12", "vue": "^3.5.12",
"zod": "^3.22.2" "zod": "^3.22.2"
}, },
"devDependencies": { "devDependencies": {
"@ianvs/prettier-plugin-sort-imports": "^4.4.0", "@ianvs/prettier-plugin-sort-imports": "^4.4.0",
"@rushstack/eslint-patch": "^1.10.3",
"@tsconfig/node20": "^20.1.4", "@tsconfig/node20": "^20.1.4",
"@types/jsdom": "^21.1.7", "@types/jsdom": "^21.1.7",
"@types/node": "^20.14.11", "@types/node": "^20.14.11",
"@vitejs/plugin-vue": "^5.0.5", "@vitejs/plugin-vue": "^5.0.5",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^13.0.0",
"@vue/test-utils": "^2.4.6", "@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.5.1", "@vue/tsconfig": "^0.5.1",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.27.0",
"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", "phaser3-rex-plugins": "^1.80.8",

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.

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 453 KiB

After

Width:  |  Height:  |  Size: 454 KiB

View File

@ -1,7 +1,6 @@
<template> <template>
<Debug /> <Debug />
<Notifications /> <Notifications />
<BackgroundImageLoader />
<GmPanel v-if="gameStore.character?.role === 'gm'" /> <GmPanel v-if="gameStore.character?.role === 'gm'" />
<component :is="currentScreen" /> <component :is="currentScreen" />
</template> </template>
@ -13,43 +12,43 @@ 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 { useSoundComposable } from '@/composables/useSoundComposable'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore' import { computed, watch } from 'vue'
import { computed, onMounted, onUnmounted, watch } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const mapEditorStore = useMapEditorStore()
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 (!gameStore.connection) return Login
if (!gameStore.token) return Login if (!gameStore.token) return Login
if (!gameStore.character) return Characters if (!gameStore.character) return Characters
if (mapEditorStore.active) return MapEditor if (mapEditor.active.value) return MapEditor
return Game return Game
}) })
// Watch mapEditorStore.active and empty gameStore.game.loadedAssets // Watch mapEditor.active and empty gameStore.game.loadedAssets
watch( watch(
() => mapEditorStore.active, () => mapEditor.active.value,
() => { () => {
gameStore.game.loadedTextures = [] gameStore.game.loadedTextures = []
} }
) )
// #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
@ -57,7 +56,9 @@ addEventListener('keydown', (event) => {
if (gameStore.character?.role !== 'gm') return // Only allow toggling the gm panel if the character is a gm if (gameStore.character?.role !== 'gm') return // Only allow toggling the gm panel if the character is a gm
// Check if no input is active or focus is on an input // Check if no input is active or focus is on an input
if (event.repeat || event.isComposing || event.defaultPrevented || document.activeElement?.tagName.toUpperCase() === 'INPUT' || document.activeElement?.tagName.toUpperCase() === 'TEXTAREA') return if (event.repeat || event.isComposing || event.defaultPrevented || document.activeElement?.tagName.toUpperCase() === 'INPUT' || document.activeElement?.tagName.toUpperCase() === 'TEXTAREA') {
return
}
if (event.key === 'G') { if (event.key === 'G') {
gameStore.toggleGmPanel() gameStore.toggleGmPanel()

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),

61
src/application/enums.ts Normal file
View File

@ -0,0 +1,61 @@
export enum Direction {
POSITIVE,
NEGATIVE,
UNCHANGED
}
export enum SocketEvent {
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 = '16',
USER_DISCONNECT = '15',
LOGIN = '14',
LOGGED_IN = '13',
NOTIFICATION = '12',
DATE = '11',
FAILED = '10',
COMPLETED = '9',
CONNECTION = '8',
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

@ -19,7 +19,6 @@ export type TextureData = {
updatedAt: Date updatedAt: Date
originX?: number originX?: number
originY?: number originY?: number
isAnimated?: boolean
frameRate?: number frameRate?: number
frameWidth?: number frameWidth?: number
frameHeight?: number frameHeight?: number
@ -27,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
@ -35,12 +34,11 @@ export type Tile = {
} }
export type MapObject = { export type MapObject = {
id: UUID id: string
name: string name: string
tags: any | null tags: any | null
originX: number originX: number
originY: number originY: number
isAnimated: boolean
frameRate: number frameRate: number
frameWidth: number frameWidth: number
frameHeight: number frameHeight: number
@ -49,7 +47,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
@ -64,11 +62,11 @@ 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
tiles: any | null tiles: string[][]
pvp: boolean pvp: boolean
mapEffects: MapEffect[] mapEffects: MapEffect[]
mapEventTiles: MapEventTile[] mapEventTiles: MapEventTile[]
@ -80,17 +78,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
@ -104,8 +99,8 @@ export enum MapEventTileType {
} }
export type MapEventTile = { export type MapEventTile = {
id: UUID id: string
map: Map mapid: string
type: MapEventTileType type: MapEventTileType
positionX: number positionX: number
positionY: number positionY: number
@ -113,7 +108,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
@ -122,7 +117,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[]
@ -142,7 +137,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
@ -153,7 +148,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
@ -161,8 +156,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
@ -185,17 +180,18 @@ export type Character = {
export type MapCharacter = { export type MapCharacter = {
character: Character character: Character
isMoving: boolean isMoving: boolean
isAttacking?: boolean
} }
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
} }
@ -210,7 +206,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
@ -218,22 +214,28 @@ export type Sprite = {
characterTypes: CharacterType[] characterTypes: CharacterType[]
} }
export interface SpriteImage {
url: string
offset: {
x: number
y: number
}
}
export type SpriteAction = { export type SpriteAction = {
id: UUID id: string
sprite: Sprite sprite: string
action: string action: string
sprites: string[] sprites: SpriteImage[]
originX: number originX: number
originY: number originY: number
isAnimated: boolean
isLooping: boolean
frameWidth: number frameWidth: number
frameHeight: number frameHeight: number
frameRate: number frameRate: number
} }
export type Chat = { export type Chat = {
id: UUID id: string
character: Character character: Character
map: Map map: Map
message: string message: string
@ -242,19 +244,15 @@ export type Chat = {
export type WorldSettings = { export type WorldSettings = {
date: Date date: Date
isRainEnabled: boolean weatherState: WeatherState
isFogEnabled: boolean
fogDensity: number
} }
export type WeatherState = { export type WeatherState = {
isRainEnabled: boolean
rainPercentage: number rainPercentage: number
isFogEnabled: boolean
fogDensity: number fogDensity: number
} }
export type mapLoadData = { export type mapLoadData = {
mapId: UUID mapId: string
characters: MapCharacter[] characters: MapCharacter[]
} }

View File

@ -1,25 +1,35 @@
import config from '@/application/config'
import type { HttpResponse } from '@/application/types'
import type { BaseStorage } from '@/storage/baseStorage'
export function uuidv4() { export function uuidv4() {
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) => (+c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))).toString(16)) return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) => (+c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))).toString(16))
} }
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() { export async function downloadCache<T extends { id: string; updatedAt: Date }>(endpoint: string, storage: BaseStorage<T>) {
// Check if not localhost const request = await fetch(`${config.server_endpoint}/cache/${endpoint}`)
if (window.location.hostname !== 'localhost') { const response = (await request.json()) as HttpResponse<T[]>
return window.location.hostname
if (!response.success) {
console.error(`Failed to download ${endpoint}:`, response.message)
return
} }
// Check if not IP address const items = response.data ?? []
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) { for (const item of items) {
return window.location.hostname let overwrite = false
} const existingItem = await storage.getById(item.id)
return window.location.hostname.split('.').slice(-2).join('.') if (!existingItem || item.updatedAt > existingItem.updatedAt) {
overwrite = true
}
await storage.add(item, overwrite)
}
} }

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

@ -1,179 +0,0 @@
<template>
<Scene name="effects" @preload="preloadScene" @create="createScene" @update="updateScene" />
</template>
<script setup lang="ts">
import type { WeatherState } from '@/application/types'
import { useGameStore } from '@/stores/gameStore'
import { useMapStore } from '@/stores/mapStore'
import { Scene } from 'phavuer'
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
// Constants
const LIGHT_CONFIG = {
SUNRISE_HOUR: 6,
SUNSET_HOUR: 20,
DAY_STRENGTH: 100,
NIGHT_STRENGTH: 30
}
// Stores and refs
const gameStore = useGameStore()
const mapStore = useMapStore()
const sceneRef = ref<Phaser.Scene | null>(null)
const mapEffectsReady = ref(false)
// Effect objects
const effects = {
light: ref<Phaser.GameObjects.Graphics | null>(null),
rain: ref<Phaser.GameObjects.Particles.ParticleEmitter | null>(null),
fog: ref<Phaser.GameObjects.Sprite | null>(null)
}
// Weather state
const weatherState = ref<WeatherState>({
isRainEnabled: false,
rainPercentage: 0,
isFogEnabled: false,
fogDensity: 0
})
// Scene setup
const preloadScene = (scene: Phaser.Scene) => {
scene.load.image('raindrop', 'assets/raindrop.png')
scene.load.image('fog', 'assets/fog.png')
}
const createScene = (scene: Phaser.Scene) => {
sceneRef.value = scene
initializeEffects(scene)
setupSocketListeners()
}
const initializeEffects = (scene: Phaser.Scene) => {
// Light
effects.light.value = scene.add.graphics().setDepth(1000)
// Rain
effects.rain.value = scene.add
.particles(0, 0, 'raindrop', {
x: { min: 0, max: window.innerWidth },
y: -50,
quantity: 5,
lifespan: 2000,
speedY: { min: 300, max: 500 },
scale: { start: 0.005, end: 0.005 },
alpha: { start: 0.5, end: 0 },
blendMode: 'ADD'
})
.setDepth(900)
effects.rain.value.stop()
// Fog
effects.fog.value = scene.add
.sprite(window.innerWidth / 2, window.innerHeight / 2, 'fog')
.setScale(2)
.setAlpha(0)
.setDepth(950)
}
// Effect updates
const updateScene = () => {
const timeBasedLight = calculateLightStrength(gameStore.world.date)
const mapEffects = mapStore.map?.mapEffects?.reduce(
(acc, curr) => ({
...acc,
[curr.effect]: curr.strength
}),
{}
) as { [key: string]: number }
// Only update effects once mapEffects are loaded
if (!mapEffectsReady.value) {
if (mapEffects && Object.keys(mapEffects).length) {
mapEffectsReady.value = true
} else {
return
}
}
const finalEffects =
mapEffects && Object.keys(mapEffects).length
? mapEffects
: {
light: timeBasedLight,
rain: weatherState.value.isRainEnabled ? weatherState.value.rainPercentage : 0,
fog: weatherState.value.isFogEnabled ? weatherState.value.fogDensity * 100 : 0
}
applyEffects(finalEffects)
}
const applyEffects = (effectValues: any) => {
if (effects.light.value) {
const darkness = 1 - (effectValues.light ?? 100) / 100
effects.light.value.clear().fillStyle(0x000000, darkness).fillRect(0, 0, window.innerWidth, window.innerHeight)
}
if (effects.rain.value) {
const strength = effectValues.rain ?? 0
strength > 0 ? effects.rain.value.start().setQuantity(Math.floor((strength / 100) * 10)) : effects.rain.value.stop()
}
if (effects.fog.value) {
effects.fog.value.setAlpha((effectValues.fog ?? 0) / 100)
}
}
const calculateLightStrength = (time: Date): number => {
const hour = time.getHours()
const minute = time.getMinutes()
if (hour >= LIGHT_CONFIG.SUNSET_HOUR || hour < LIGHT_CONFIG.SUNRISE_HOUR) return LIGHT_CONFIG.NIGHT_STRENGTH
if (hour > LIGHT_CONFIG.SUNRISE_HOUR && hour < LIGHT_CONFIG.SUNSET_HOUR - 2) return LIGHT_CONFIG.DAY_STRENGTH
if (hour === LIGHT_CONFIG.SUNRISE_HOUR) return LIGHT_CONFIG.NIGHT_STRENGTH + ((LIGHT_CONFIG.DAY_STRENGTH - LIGHT_CONFIG.NIGHT_STRENGTH) * minute) / 60
const totalMinutes = (hour - (LIGHT_CONFIG.SUNSET_HOUR - 2)) * 60 + minute
return LIGHT_CONFIG.DAY_STRENGTH - (LIGHT_CONFIG.DAY_STRENGTH - LIGHT_CONFIG.NIGHT_STRENGTH) * (totalMinutes / 120)
}
// Socket and window handlers
const setupSocketListeners = () => {
gameStore.connection?.emit('weather', (response: WeatherState) => {
weatherState.value = response
updateScene()
})
gameStore.connection?.on('weather', (data: WeatherState) => {
weatherState.value = data
updateScene()
})
gameStore.connection?.on('date', updateScene)
}
const handleResize = () => {
if (effects.rain.value) effects.rain.value.updateConfig({ x: { min: 0, max: window.innerWidth } })
if (effects.fog.value) effects.fog.value.setPosition(window.innerWidth / 2, window.innerHeight / 2)
}
// Lifecycle
watch(
() => mapStore.map,
() => {
mapEffectsReady.value = false
updateScene()
},
{ deep: true }
)
onMounted(() => window.addEventListener('resize', handleResize))
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
if (sceneRef.value) sceneRef.value.scene.remove('effects')
gameStore.connection?.off('weather')
})
</script>

View File

@ -1,184 +1,102 @@
<template> <template>
<ChatBubble :mapCharacter="props.mapCharacter" :currentX="currentPositionX" :currentY="currentPositionY" /> <Container ref="characterContainer" :x="currentPositionX" :y="currentPositionY" :depth="isometricDepth">
<Healthbar :mapCharacter="props.mapCharacter" :currentX="currentPositionX" :currentY="currentPositionY" /> <ChatBubble :mapCharacter="props.mapCharacter" />
<Container ref="charContainer" :depth="isometricDepth" :x="currentPositionX" :y="currentPositionY"> <HealthBar :mapCharacter="props.mapCharacter" />
<!-- <CharacterHair :mapCharacter="props.mapCharacter" :currentX="currentX" :currentY="currentY" />--> <CharacterHair :mapCharacter="props.mapCharacter" />
<!-- <CharacterChest :mapCharacter="props.mapCharacter" :currentX="currentX" :currentY="currentY" />--> <Sprite ref="characterSprite" :origin-y="1" :flipX="isFlippedX" />
<Sprite ref="charSprite" :origin-y="1" :flipX="isFlippedX" />
</Container> </Container>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import config from '@/application/config' import { type MapCharacter } from '@/application/types'
import { type MapCharacter, type Sprite as SpriteT } 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 { loadSpriteTextures } from '@/composables/gameComposable' import { useCharacterSpriteComposable } from '@/composables/useCharacterSpriteComposable'
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/mapComposable' import { useSoundComposable } from '@/composables/useSoundComposable'
import { CharacterTypeStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapStore } from '@/stores/mapStore' import { useMapStore } from '@/stores/mapStore'
import { Container, refObj, Sprite, useScene } from 'phavuer' import { Container, Sprite, useScene } from 'phavuer'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue' import { onMounted, onUnmounted, watch } from 'vue'
// import CharacterChest from '@/components/game/character/partials/CharacterChest.vue'
enum Direction {
POSITIVE,
NEGATIVE,
UNCHANGED
}
const props = defineProps<{ const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap tileMap: Phaser.Tilemaps.Tilemap
mapCharacter: MapCharacter mapCharacter: MapCharacter
}>() }>()
const charContainer = refObj<Phaser.GameObjects.Container>()
const charSprite = refObj<Phaser.GameObjects.Sprite>()
const charSpriteId = ref('')
const gameStore = useGameStore() const gameStore = useGameStore()
const mapStore = useMapStore() const mapStore = useMapStore()
const scene = useScene() const scene = useScene()
const currentPositionX = ref(0) const { characterContainer, characterSprite, currentPositionX, currentPositionY, isometricDepth, isFlippedX, updatePosition, playAnimation, updateSprite, initializeSprite, cleanup } = useCharacterSpriteComposable(scene, props.tileMap, props.mapCharacter)
const currentPositionY = ref(0) const { playSound, stopSound } = useSoundComposable()
const isometricDepth = ref(1)
const isInitialPosition = ref(true)
const tween = ref<Phaser.Tweens.Tween | null>(null)
const updateIsometricDepth = (positionX: number, positionY: number) => { const handlePositionUpdate = (newValues: any, oldValues: any) => {
isometricDepth.value = calculateIsometricDepth(positionX, positionY, 28, 94, true) if (!newValues) return
if (!oldValues || newValues.positionX !== oldValues.positionX || newValues.positionY !== oldValues.positionY) {
updatePosition(newValues.positionX, newValues.positionY)
}
if (newValues.isMoving !== oldValues?.isMoving || newValues.rotation !== oldValues?.rotation) {
updateSprite()
}
} }
const updatePosition = (positionX: number, positionY: number, direction: Direction) => { /**
const newPositionX = tileToWorldX(props.tilemap, positionX, positionY) * Plays walk sound when character is moving
const newPositionY = tileToWorldY(props.tilemap, positionX, positionY) */
watch(
if (isInitialPosition.value) { () => props.mapCharacter.isMoving,
currentPositionX.value = newPositionX (newValue) => {
currentPositionY.value = newPositionY if (newValue) {
isInitialPosition.value = false playSound('/assets/sounds/walk.wav', false, true)
return } else {
} stopSound('/assets/sounds/walk.wav')
if (tween.value?.isPlaying()) {
tween.value.stop()
}
const distance = Math.sqrt(Math.pow(newPositionX - currentPositionX.value, 2) + Math.pow(newPositionY - currentPositionY.value, 2))
if (distance >= config.tile_size.width / 1.1) {
currentPositionX.value = newPositionX
currentPositionY.value = newPositionY
return
}
const duration = distance * 5.7
tween.value = props.tilemap.scene.tweens.add({
targets: { x: currentPositionX.value, y: currentPositionY.value },
x: newPositionX,
y: newPositionY,
duration,
ease: 'Linear',
onStart: () => {
if (direction === Direction.POSITIVE) {
updateIsometricDepth(positionX, positionY)
}
},
onUpdate: (tween) => {
// @ts-ignore
currentPositionX.value = tween.targets[0].x
// @ts-ignore
currentPositionY.value = tween.targets[0].y
},
onComplete: () => {
if (direction === Direction.NEGATIVE) {
updateIsometricDepth(positionX, positionY)
}
} }
})
}
const calcDirection = (oldPositionX: number, oldPositionY: number, newPositionX: number, newPositionY: number): Direction => {
if (newPositionY < oldPositionY || newPositionX < oldPositionX) return Direction.NEGATIVE
if (newPositionX > oldPositionX || newPositionY > oldPositionY) return Direction.POSITIVE
return Direction.UNCHANGED
}
const isFlippedX = computed(() => [6, 4].includes(props.mapCharacter.character.rotation ?? 0))
const charTexture = computed(() => {
const spriteId = charSpriteId.value ?? 'idle_right_down'
const action = props.mapCharacter.isMoving ? 'walk' : 'idle'
const direction = [0, 6].includes(props.mapCharacter.character.rotation) ? 'left_up' : 'right_down'
return `${spriteId}-${action}_${direction}`
})
const updateSprite = () => {
if (props.mapCharacter.isMoving) {
charSprite.value!.anims.play(charTexture.value, true)
} else {
charSprite.value!.anims.stop()
charSprite.value!.setFrame(0)
charSprite.value!.setTexture(charTexture.value)
} }
} )
/**
* 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,
positionY: props.mapCharacter.character.positionY, positionY: props.mapCharacter.character.positionY,
isMoving: props.mapCharacter.isMoving, isMoving: props.mapCharacter.isMoving,
rotation: props.mapCharacter.character.rotation rotation: props.mapCharacter.character.rotation,
isAttacking: props.mapCharacter.isAttacking
}), }),
(newValues, oldValues) => { (oldValues, newValues) => {
if (!newValues) return handlePositionUpdate(oldValues, newValues)
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, direction)
}
// Handle animation updates
if (newValues.isMoving !== oldValues?.isMoving || newValues.rotation !== oldValues?.rotation) {
updateSprite()
}
} }
) )
const characterTypeStorage = new CharacterTypeStorage() onMounted(async () => {
characterTypeStorage.getSpriteId(props.mapCharacter.character.characterType!).then((spriteId) => { await initializeSprite()
console.log(spriteId)
charSpriteId.value = spriteId
loadSpriteTextures(scene, spriteId)
.then(() => {
charSprite.value!.setTexture(charTexture.value)
charSprite.value!.setFlipX(isFlippedX.value)
})
.catch((error) => {
console.error('Error loading texture:', error)
})
})
onMounted(() => {
charContainer.value!.setName(props.mapCharacter.character!.name)
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)
// #146 : Set camera position to character, need to be improved still
scene.cameras.main.startFollow(charContainer.value as Phaser.GameObjects.Container)
} }
updatePosition(props.mapCharacter.character.positionX, props.mapCharacter.character.positionY, props.mapCharacter.character.rotation)
}) })
onUnmounted(() => { onUnmounted(() => {
tween.value?.stop() cleanup()
}) })
</script> </script>

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

@ -1,51 +1,54 @@
<template> <template>
<Image v-bind="imageProps" v-if="gameStore.getLoadedAsset(texture)" /> <Image v-bind="imageProps" v-if="gameStore.isTextureLoaded(texture)" />
</template> </template>
<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 { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { Image, useScene } from 'phavuer' import { Image, useScene } from 'phavuer'
import { computed } from 'vue' import { computed, onMounted, ref } from 'vue'
const props = defineProps<{ const props = defineProps<{
mapCharacter: MapCharacter mapCharacter: MapCharacter
currentX: number
currentY: number
}>() }>()
const gameStore = useGameStore() const gameStore = useGameStore()
const scene = useScene() const scene = useScene()
const hairSpriteId = ref('')
const sprite = ref<SpriteT | null>(null)
const texture = computed(() => { const texture = computed(() => {
const { rotation, characterHair } = props.mapCharacter.character const { rotation } = props.mapCharacter.character
const spriteId = characterHair?.sprite?.id
const direction = [0, 6].includes(rotation) ? 'back' : 'front' const direction = [0, 6].includes(rotation) ? 'back' : 'front'
return `${spriteId}-${direction}` return `${hairSpriteId.value}-${direction}`
}) })
const isFlippedX = computed(() => [6, 4].includes(props.mapCharacter.character.rotation ?? 0)) const isFlippedX = computed(() => [6, 4].includes(props.mapCharacter.character.rotation ?? 0))
const imageProps = computed(() => { const imageProps = computed(() => {
// Get the current sprite action based on direction
const direction = [0, 6].includes(props.mapCharacter.character.rotation ?? 0) ? 'back' : 'front' 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) const spriteAction = sprite.value?.spriteActions?.find((spriteAction) => spriteAction.action === direction)
return { return {
depth: 1, depth: 9999,
originX: Number(spriteAction?.originX) ?? 0, originX: Number(spriteAction?.originX) ?? 0,
originY: Number(spriteAction?.originY) ?? 0, originY: Number(spriteAction?.originY) ?? 0,
flipX: isFlippedX.value, flipX: isFlippedX.value,
texture: texture.value, texture: texture.value
y: props.mapCharacter.isMoving ? Math.floor(Date.now() / 250) % 2 : 0
} }
}) })
loadSpriteTextures(scene, props.mapCharacter.character.characterHair?.sprite as SpriteT) onMounted(async () => {
.then(() => {}) const characterHairStorage = new CharacterHairStorage()
.catch((error) => { const spriteId = await characterHairStorage.getSpriteId(props.mapCharacter.character.characterHair!)
console.error('Error loading texture:', error) if (!spriteId) return
})
hairSpriteId.value = spriteId
const spriteStorage = new SpriteStorage()
sprite.value = await spriteStorage.getById(spriteId)
await loadSpriteTextures(scene, spriteId)
})
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<Container ref="charChatContainer" :depth="999" :x="currentX" :y="currentY"> <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>
@ -12,12 +12,10 @@ import { onMounted } from 'vue'
const props = defineProps<{ const props = defineProps<{
mapCharacter: MapCharacter mapCharacter: MapCharacter
currentX: number
currentY: number
}>() }>()
const game = useGame() const game = useGame()
const charChatContainer = refObj<Phaser.GameObjects.Container>() const characterChatContainer = refObj<Phaser.GameObjects.Container>()
const createChatBubble = (container: Phaser.GameObjects.Container) => { const createChatBubble = (container: Phaser.GameObjects.Container) => {
container.setName(`${props.mapCharacter.character.name}_chatBubble`) container.setName(`${props.mapCharacter.character.name}_chatBubble`)
@ -41,7 +39,7 @@ const createChatText = (text: Phaser.GameObjects.Text) => {
} }
onMounted(() => { onMounted(() => {
charChatContainer.value!.setName(`${props.mapCharacter.character!.name}_chatContainer`) characterChatContainer.value!.setName(`${props.mapCharacter.character!.name}_chatContainer`)
charChatContainer.value!.setVisible(false) characterChatContainer.value!.setVisible(false)
}) })
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<Container :depth="999" :x="currentX" :y="currentY"> <Container :depth="999">
<Text @create="createNicknameText" :text="props.mapCharacter.character.name" /> <Text @create="createNicknameText" :text="props.mapCharacter.character.name" />
<RoundRectangle :origin-x="0.5" :origin-y="18.5" :fillColor="0xffffff" :width="74" :height="6" :radius="5" /> <RoundRectangle :origin-x="0.5" :origin-y="18.5" :fillColor="0xffffff" :width="74" :height="6" :radius="5" />
<RoundRectangle :origin-x="0.5" :origin-y="36.4" :fillColor="0x00b3b3" :width="70" :height="3" :radius="5" /> <RoundRectangle :origin-x="0.5" :origin-y="36.4" :fillColor="0x00b3b3" :width="70" :height="3" :radius="5" />
@ -12,8 +12,6 @@ import { Container, RoundRectangle, Text, useGame } from 'phavuer'
const props = defineProps<{ const props = defineProps<{
mapCharacter: MapCharacter mapCharacter: MapCharacter
currentX: number
currentY: number
}>() }>()
const game = useGame() const game = useGame()

View File

@ -1,12 +1,12 @@
<template> <template>
<div class="absolute" v-if="gameStore.uiSettings.isCharacterProfileOpen" :style="modalStyle"> <div class="absolute" v-if="gameStore.uiSettings.isCharacterProfileOpen" :style="modalStyle">
<div class="relative"> <div class="relative">
<img src="/assets/ui-elements/profile-ui-box-outer.svg" class="absolute w-full h-full" /> <img src="/assets/ui-elements/profile-ui-box-outer.svg" class="absolute w-full h-full" alt="" />
<img src="/assets/ui-elements/profile-ui-box-inner.svg" class="absolute left-2 bottom-2 w-[calc(100%_-_16px)] h-[calc(100%_-_40px)]" /> <img src="/assets/ui-elements/profile-ui-box-inner.svg" class="absolute left-2 bottom-2 w-[calc(100%_-_16px)] h-[calc(100%_-_40px)]" alt="" />
<div @mousedown="startDrag" class="cursor-move px-5 py-2.5 flex justify-between items-center relative"> <div @mousedown="startDrag" class="cursor-move px-5 py-2.5 flex justify-between items-center relative">
<span class="text-xs text-white font-thin">Character Profile [Alt+C]</span> <span class="text-xs text-white font-thin">Character Profile [Alt+C]</span>
<button @click="gameStore.uiSettings.isCharacterProfileOpen = false" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out"> <button @click="gameStore.uiSettings.isCharacterProfileOpen = false" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
<img alt="close" draggable="false" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" /> <img draggable="false" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" alt="Close button icon" />
</button> </button>
</div> </div>
<div class="py-4 px-6 flex flex-col gap-7 relative z-10"> <div class="py-4 px-6 flex flex-col gap-7 relative z-10">
@ -17,7 +17,7 @@
<span class="text-xs">{{ gameStore.character?.experience }} / 18.600XP</span> <span class="text-xs">{{ gameStore.character?.experience }} / 18.600XP</span>
</div> </div>
<a class="hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured-small.svg')] bg-no-repeat block w-8 h-8 relative mx-[3px]"> <a class="hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured-small.svg')] bg-no-repeat block w-8 h-8 relative mx-[3px]">
<img class="hover:drop-shadow-default w-3.5 h-3.5 m-[9px] object-contain" draggable="false" src="/assets/icons/plus-green-icon.svg" /> <img class="hover:drop-shadow-default w-3.5 h-3.5 m-[9px] object-contain" draggable="false" src="/assets/icons/plus-green-icon.svg" alt="Plus button icon" />
</a> </a>
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
@ -37,20 +37,20 @@
</div> </div>
</div> </div>
</div> </div>
<img src="/assets/placeholders/inventory_player.png" class="w-8 h-auto" /> <img src="/assets/placeholders/inventory_player.png" class="w-8 h-auto" alt="Player character sprite" />
<div class="flex flex-col items-end gap-0.5"> <div class="flex flex-col items-end gap-0.5">
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600"> <div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
<img class="w-6 h-6 center-element" src="/assets/icons/profile/helmet.svg" /> <img class="w-6 h-6 center-element" src="/assets/icons/profile/helmet.svg" alt="Helmet icon" />
</div> </div>
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600"> <div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
<img class="w-6 h-6 center-element" src="/assets/icons/profile/chestplate.svg" /> <img class="w-6 h-6 center-element" src="/assets/icons/profile/chestplate.svg" alt="Chestplate icon" />
</div> </div>
<div class="flex gap-0.5 items-end"> <div class="flex gap-0.5 items-end">
<div class="w-6 h-6 default-border rounded-sm bg-gray relative hover:bg-gray-600"> <div class="w-6 h-6 default-border rounded-sm bg-gray relative hover:bg-gray-600">
<img class="w-4 h-4 center-element" src="/assets/icons/profile/boots.svg" /> <img class="w-4 h-4 center-element" src="/assets/icons/profile/boots.svg" alt="Boots icon" />
</div> </div>
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600"> <div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
<img class="w-6 h-6 center-element" src="/assets/icons/profile/legs.svg" /> <img class="w-6 h-6 center-element" src="/assets/icons/profile/legs.svg" alt="Legs icon" />
</div> </div>
</div> </div>
</div> </div>
@ -119,111 +119,44 @@
<script setup lang="ts"> <script setup lang="ts">
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
let startX = 0 const width = ref(286)
let startY = 0 const height = ref(483)
let initialX = 0 const x = ref(window.innerWidth / 2 - 143)
let initialY = 0 const y = ref(window.innerHeight / 2 - 241)
let modalPositionX = 0
let modalPositionY = 0
let modalWidth = 286
let modalHeight = 483
const width = ref(modalWidth)
const height = ref(modalHeight)
const x = ref(0)
const y = ref(0)
const isDragging = ref(false) const isDragging = ref(false)
const modalStyle = computed(() => ({ const modalStyle = computed(() => ({
top: `${y.value}px`, top: `${y.value}px`,
left: `${x.value}px`, left: `${x.value}px`,
width: `${width.value}px`, width: `${width.value}px`,
height: `${height.value}px`, height: `${height.value}px`
maxWidth: '100vw',
maxHeight: '100vh'
})) }))
function startDrag(event: MouseEvent) { function startDrag(event: MouseEvent) {
isDragging.value = true isDragging.value = true
startX = event.clientX const startX = event.clientX - x.value
startY = event.clientY const startY = event.clientY - y.value
initialX = x.value
initialY = y.value function drag(event: MouseEvent) {
event.preventDefault() if (!isDragging.value) return
x.value = event.clientX - startX
y.value = event.clientY - startY
}
function stopDrag() {
isDragging.value = false
removeEventListener('mousemove', drag)
removeEventListener('mouseup', stopDrag)
}
addEventListener('mousemove', drag)
addEventListener('mouseup', stopDrag)
} }
function drag(event: MouseEvent) {
if (!isDragging.value) return
const dx = event.clientX - startX
const dy = event.clientY - startY
x.value = initialX + dx
y.value = initialY + dy
adjustPosition()
}
function stopDrag() {
isDragging.value = false
}
function adjustPosition() {
x.value = Math.min(x.value, window.innerWidth - width.value)
y.value = Math.min(y.value, window.innerHeight - height.value)
}
function initializePosition() {
width.value = Math.min(modalWidth, window.innerWidth)
height.value = Math.min(modalHeight, window.innerHeight)
if (modalPositionX !== 0 && modalPositionY !== 0) {
x.value = modalPositionX
y.value = modalPositionY
} else {
x.value = (window.innerWidth - width.value) / 2
y.value = (window.innerHeight - height.value) / 2
}
}
watch(
() => gameStore.uiSettings.isCharacterProfileOpen,
(value) => {
gameStore.uiSettings.isCharacterProfileOpen = value
if (value) {
initializePosition()
}
}
)
watch(
() => modalWidth,
(value) => {
width.value = Math.min(value, window.innerWidth)
}
)
watch(
() => modalHeight,
(value) => {
height.value = Math.min(value, window.innerHeight)
}
)
watch(
() => modalPositionX,
(value) => {
x.value = value
}
)
watch(
() => modalPositionY,
(value) => {
y.value = value
}
)
function keyPress(event: KeyboardEvent) { function keyPress(event: KeyboardEvent) {
if (event.altKey && event.key === 'c') { if (event.altKey && event.key === 'c') {
gameStore.toggleCharacterProfile() gameStore.toggleCharacterProfile()
@ -232,14 +165,9 @@ function keyPress(event: KeyboardEvent) {
onMounted(() => { onMounted(() => {
addEventListener('keydown', keyPress) addEventListener('keydown', keyPress)
addEventListener('mousemove', drag)
addEventListener('mouseup', stopDrag)
initializePosition()
}) })
onUnmounted(() => { onUnmounted(() => {
removeEventListener('keydown', keyPress) removeEventListener('keydown', keyPress)
removeEventListener('mousemove', drag)
removeEventListener('mouseup', stopDrag)
}) })
</script> </script>

View File

@ -2,12 +2,12 @@
<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>
<div class="w-96 mx-auto relative"> <div class="w-96 mx-auto relative">
<img src="/assets/icons/ingameUI/chat-icon.svg" class="absolute top-1/2 -translate-y-1/2 left-2.5 h-4 w-4 opacity-50" /> <img src="/assets/icons/ingameUI/chat-icon.svg" class="absolute top-1/2 -translate-y-1/2 left-2.5 h-4 w-4 opacity-50" alt="" />
<input <input
class="w-[332px] h-8 rounded-sm text-xs font-default pl-8 pr-4 py-0 bg-gray-600 border-2 border-solid border-gray-500 text-gray-300 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover focus:outline-none focus:ring-0 focus:border-cyan-800" class="w-[332px] h-8 rounded-sm text-xs font-default pl-8 pr-4 py-0 bg-gray-600 border-2 border-solid border-gray-500 text-gray-300 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover focus:outline-none focus:ring-0 focus:border-cyan-800"
placeholder="Type something..." placeholder="Type something..."
@ -21,7 +21,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { Chat } from '@/application/types' import { SocketEvent } from '@/application/enums'
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'
@ -33,7 +33,7 @@ const gameStore = useGameStore()
const mapStore = useMapStore() 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) => {}) gameStore.connection?.emit(SocketEvent.CHAT_MESSAGE, { message: message.value }, (response: boolean) => {})
message.value = '' message.value = ''
} }
@ -79,18 +79,30 @@ const scrollToBottom = () => {
}) })
} }
gameStore.connection?.on('chat:message', (data: Chat) => { gameStore.connection?.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 charChatContainer = scene.children.getByName(data.character.name + '_chatContainer') as Phaser.GameObjects.Container const characterChatContainer = characterContainer.getByName(data.character + '_chatContainer') as Phaser.GameObjects.Container
if (!charChatContainer) return if (!characterChatContainer) {
console.log('No character chat container found')
return
}
const chatBubble = charChatContainer.getByName(data.character.name + '_chatBubble') as Phaser.GameObjects.Container const chatBubble = characterChatContainer.getByName(data.character + '_chatBubble') as Phaser.GameObjects.Container
const chatText = charChatContainer.getByName(data.character.name + '_chatText') as Phaser.GameObjects.Text const chatText = characterChatContainer.getByName(data.character + '_chatText') as Phaser.GameObjects.Text
if (!chatText || !chatBubble) return if (!chatText || !chatBubble) {
console.log('No chat text or bubble found')
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
@ -115,24 +127,24 @@ gameStore.connection?.on('chat:message', (data: Chat) => {
// setText but with max. char limit of 90 // setText but with max. char limit of 90
chatText.setText(data.message.substring(0, 90)) chatText.setText(data.message.substring(0, 90))
charChatContainer.setVisible(true) characterChatContainer.setVisible(true)
/** /**
* Hide chat bubble after a few seconds * Hide chat bubble after a few seconds
*/ */
// Clear any existing hide timer // Clear any existing hide timer
if (charChatContainer.getData('hideTimer')) { if (characterChatContainer.getData('hideTimer')) {
clearTimeout(charChatContainer.getData('hideTimer')) clearTimeout(characterChatContainer.getData('hideTimer'))
} }
// Set a new hide timer // Set a new hide timer
const hideTimer = setTimeout(() => { const hideTimer = setTimeout(() => {
charChatContainer.setVisible(false) characterChatContainer.setVisible(false)
}, 3000) }, 3000)
// Store the timer on the container itself // Store the timer on the container itself
charChatContainer.setData('hideTimer', hideTimer) characterChatContainer.setData('hideTimer', hideTimer)
}) })
scrollToBottom() scrollToBottom()
@ -141,7 +153,7 @@ onMounted(() => {
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
gameStore.connection?.off('chat:message') gameStore.connection?.off(SocketEvent.CHAT_MESSAGE)
removeEventListener('keydown', focusChat) removeEventListener('keydown', focusChat)
}) })
</script> </script>

View File

@ -0,0 +1,20 @@
<template>
<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">
{{ useDateFormat(gameStore.world.date, 'YYYY/MM/DD HH:mm') }}
</p>
</div>
</template>
<script setup lang="ts">
import { SocketEvent } from '@/application/enums'
import { useGameStore } from '@/stores/gameStore'
import { useDateFormat } from '@vueuse/core'
import { onUnmounted } from 'vue'
const gameStore = useGameStore()
onUnmounted(() => {
gameStore.connection?.off(SocketEvent.DATE)
})
</script>

View File

@ -6,7 +6,7 @@
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div> <div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
</div> </div>
<a class="group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured.svg')] bg-no-repeat block w-[42px] h-[42px] relative"> <a class="group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured.svg')] bg-no-repeat block w-[42px] h-[42px] relative">
<img class="group-hover:drop-shadow-default w-6 h-5 mx-[9px] my-[11px] object-contain" draggable="false" src="/assets/icons/ingameUI/menu-icon.svg" /> <img class="group-hover:drop-shadow-default w-6 h-5 mx-[9px] my-[11px] object-contain" draggable="false" src="/assets/icons/ingameUI/menu-icon.svg" alt="Menu button icon" />
</a> </a>
</li> </li>
<li class="menu-item group relative" @click="gameStore.toggleCharacterProfile"> <li class="menu-item group relative" @click="gameStore.toggleCharacterProfile">
@ -15,7 +15,7 @@
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div> <div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
</div> </div>
<a class="group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured.svg')] bg-no-repeat block w-[42px] h-[42px] relative"> <a class="group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured.svg')] bg-no-repeat block w-[42px] h-[42px] relative">
<img class="group-hover:drop-shadow-default w-8 h-8 m-[5px] object-contain" draggable="false" src="/assets/placeholders/head.png" /> <img class="group-hover:drop-shadow-default w-8 h-8 m-[5px] object-contain" draggable="false" src="/assets/placeholders/head.png" alt="User profile button icon" />
<p class="absolute bottom-0 -right-1.5 m-0 max-w-4 font-ui z-10 text-white text-[12px] leading-[6px] drop-shadow-pixel"><span class="font-ui text-white text-[8px] ml-0.5">LVL</span> {{ characterLevel }}</p> <p class="absolute bottom-0 -right-1.5 m-0 max-w-4 font-ui z-10 text-white text-[12px] leading-[6px] drop-shadow-pixel"><span class="font-ui text-white text-[8px] ml-0.5">LVL</span> {{ characterLevel }}</p>
</a> </a>
</li> </li>
@ -25,7 +25,7 @@
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div> <div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
</div> </div>
<a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border-2 border-solid rounded border-gray-500 block w-[34px] h-[31px]"> <a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border-2 border-solid rounded border-gray-500 block w-[34px] h-[31px]">
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/chat-icon.svg" /> <img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/chat-icon.svg" alt="Open chat button icon" />
</a> </a>
</li> </li>
<li class="menu-item group relative"> <li class="menu-item group relative">
@ -34,7 +34,7 @@
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div> <div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
</div> </div>
<a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border-2 border-solid rounded border-gray-500 block w-[34px] h-[31px]"> <a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border-2 border-solid rounded border-gray-500 block w-[34px] h-[31px]">
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/map-icon.svg" /> <img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/map-icon.svg" alt="World map button icon" />
</a> </a>
</li> </li>
<li class="menu-item group relative"> <li class="menu-item group relative">
@ -43,7 +43,7 @@
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div> <div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
</div> </div>
<a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border-2 border-solid rounded border-gray-500 block w-[34px] h-[31px]"> <a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border-2 border-solid rounded border-gray-500 block w-[34px] h-[31px]">
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/socials-icon.svg" /> <img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/socials-icon.svg" alt="Users button icon" />
</a> </a>
</li> </li>
</ul> </ul>

View File

@ -5,12 +5,12 @@
</div> </div>
<div class="absolute -bottom-3 left-1/2 -translate-x-1/2 flex gap-1"> <div class="absolute -bottom-3 left-1/2 -translate-x-1/2 flex gap-1">
<button class="w-6 h-6 relative p-0"> <button class="w-6 h-6 relative p-0">
<img class="w-3 h-3 center-element" src="/assets/icons/plus-icon.svg" /> <img class="w-3 h-3 center-element" src="/assets/icons/plus-icon.svg" alt="Zoom-in button icon" />
<img class="w-full h-full" src="/assets/ui-elements/button-ui-box-textured.svg" /> <img class="w-full h-full" src="/assets/ui-elements/button-ui-box-textured.svg" alt="" />
</button> </button>
<button class="w-6 h-6 relative p-0"> <button class="w-6 h-6 relative p-0">
<img class="w-3 h-3 center-element" src="/assets/icons/minus-icon.svg" /> <img class="w-3 h-3 center-element" src="/assets/icons/minus-icon.svg" alt="Zoom-out button icon" />
<img class="w-full h-full" src="/assets/ui-elements/button-ui-box-textured.svg" /> <img class="w-full h-full" src="/assets/ui-elements/button-ui-box-textured.svg" alt="" />
</button> </button>
</div> </div>
</div> </div>

View File

@ -1,14 +1,48 @@
<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 { 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
}>() }>()
gameStore.connection?.on(SocketEvent.MAP_CHARACTER_JOIN, (data: MapCharacter) => {
mapStore.addCharacter(data)
})
gameStore.connection?.on(SocketEvent.MAP_CHARACTER_LEAVE, (characterId: UUID) => {
mapStore.removeCharacter(characterId)
})
gameStore.connection?.on(SocketEvent.MAP_CHARACTER_MOVE, (data: { characterId: UUID; positionX: number; positionY: number; rotation: number; isMoving: boolean }) => {
mapStore.updateCharacterPosition(data)
// @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
}
})
gameStore.connection?.on(SocketEvent.MAP_CHARACTER_ATTACK, (characterId: UUID) => {
mapStore.updateCharacterProperty(characterId, 'isAttacking', true)
})
onUnmounted(() => {
gameStore.connection?.off(SocketEvent.MAP_CHARACTER_ATTACK)
gameStore.connection?.off(SocketEvent.MAP_CHARACTER_MOVE)
gameStore.connection?.off(SocketEvent.MAP_CHARACTER_JOIN)
gameStore.connection?.off(SocketEvent.MAP_CHARACTER_LEAVE)
})
</script> </script>

View File

@ -1,46 +1,70 @@
<template> <template>
<MapTiles :key="mapStore.mapId" @tileMap:create="tileMap = $event" /> <MapTiles v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer />
<!-- <MapObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />--> <PlacedMapObjects v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer />
<Characters v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.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 MapObjects from '@/components/game/map/PlacedMapObjects.vue' import PlacedMapObjects from '@/components/game/map/PlacedMapObjects.vue'
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 tileMap = shallowRef<Phaser.Tilemaps.Tilemap>() const mapStorage = new MapStorage()
onUnmounted(() => { const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
mapStore.reset() const tileMapLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
gameStore.connection?.off('map:character:teleport')
gameStore.connection?.off('map:character:join')
gameStore.connection?.off('map:character:leave')
gameStore.connection?.off('map:character:move')
})
// Event listeners // Event listeners
gameStore.connection?.on('map:character:teleport', async (data: mapLoadData) => { gameStore.connection?.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
const map = await mapStorage.getById(mapStore.mapId)
if (!map) return
await loadTileTexturesFromMapTileArray(mapStore.mapId, scene)
tileMap.value = createTileMap(scene, map)
tileMapLayer.value = createTileLayer(tileMap.value, unduplicateArray(map.tiles))
}
watch(
() => mapStore.mapId,
async () => {
await initialize()
}
)
onMounted(async () => {
if (!mapStore.mapId) return
await initialize()
}) })
gameStore.connection?.on('map:character:leave', (characterId: UUID) => { onUnmounted(() => {
mapStore.removeCharacter(characterId) if (tileMap.value) {
}) tileMap.value.destroyLayer('tiles')
tileMap.value.removeAllLayers()
tileMap.value.destroy()
}
gameStore.connection?.on('map:character:move', (data: { characterId: UUID; positionX: number; positionY: number; rotation: number; isMoving: boolean }) => { gameStore.connection?.off(SocketEvent.MAP_CHARACTER_TELEPORT)
mapStore.updateCharacterPosition(data)
}) })
</script> </script>

View File

@ -1,75 +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 { UUID } from '@/application/types'
import { unduplicateArray } from '@/application/utilities'
import Controls from '@/components/utilities/Controls.vue' import Controls from '@/components/utilities/Controls.vue'
import { FlattenMapArray, 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, onMounted, 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(mapData: any) { onMounted(async () => {
const mapConfig = new Phaser.Tilemaps.MapData({ if (!mapStore.mapId) return
width: mapData?.width,
height: mapData?.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(FlattenMapArray(mapData?.tiles ?? []))
const tilesetImages = tilesArray.map((tile: any, 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
}
onMounted(() => {
loadMapTilesIntoScene(mapStore.mapId as UUID, scene)
.then(() => mapStorage.get(mapStore.mapId))
.then((mapData) => {
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,14 +1,31 @@
<template> <template>
<PlacedMapObject v-for="placedMapObject in mapStore.map?.placedMapObjects" :tilemap="tilemap" :placedMapObject /> <PlacedMapObject v-for="placedMapObject in items" :tileMap :tileMapLayer :placedMapObject />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { PlacedMapObject as PlacedMapObjectT } from '@/application/types'
import PlacedMapObject from '@/components/game/map/partials/PlacedMapObject.vue' import PlacedMapObject from '@/components/game/map/partials/PlacedMapObject.vue'
import { MapStorage } from '@/storage/storages'
import { useMapStore } from '@/stores/mapStore' import { useMapStore } from '@/stores/mapStore'
import { onMounted, ref } from 'vue'
const mapStore = useMapStore() import TilemapLayer = Phaser.Tilemaps.TilemapLayer
defineProps<{ defineProps<{
tilemap: Phaser.Tilemaps.Tilemap tileMap: Phaser.Tilemaps.Tilemap
tileMapLayer: TilemapLayer
}>() }>()
const mapStore = useMapStore()
const mapStorage = new MapStorage()
const items = ref<PlacedMapObjectT[]>([])
onMounted(async () => {
if (!mapStore.mapId) return
const map = await mapStorage.getById(mapStore.mapId)
if (!map) return
items.value = map.placedMapObjects
})
</script> </script>

View File

@ -1,41 +1,82 @@
<template> <template>
<Image v-if="gameStore.isAssetLoaded(props.placedMapObject.mapObject)" 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 } 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, mapObject.value!.frameWidth, mapObject.value!.frameHeight, mapObject.value!.originX, mapObject.value!.originY),
...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) => { onMounted(async () => {
console.error('Error loading texture:', error) 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="() => mapEditorStore.toggleActive()">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>
@ -20,12 +20,12 @@
<script setup lang="ts"> <script setup lang="ts">
import AssetManager from '@/components/gameMaster/assetManager/AssetManager.vue' import AssetManager from '@/components/gameMaster/assetManager/AssetManager.vue'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { ref } from 'vue' import { ref } from 'vue'
const mapEditor = useMapEditorComposable()
const gameStore = useGameStore() const gameStore = useGameStore()
const mapEditorStore = useMapEditorStore()
let toggle = ref('asset-manager') let toggle = ref('asset-manager')
</script> </script>

View File

@ -34,6 +34,7 @@
</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 { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
@ -65,7 +66,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) => { gameStore.connection?.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 +76,7 @@ function removeCharacterHair() {
} }
function refreshCharacterHairList(unsetSelectedCharacterHair = true) { function refreshCharacterHairList(unsetSelectedCharacterHair = true) {
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => { gameStore.connection?.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
assetManagerStore.setCharacterHairList(response) assetManagerStore.setCharacterHairList(response)
if (unsetSelectedCharacterHair) { if (unsetSelectedCharacterHair) {
@ -93,7 +94,7 @@ function saveCharacterHair() {
spriteId: characterSpriteId.value spriteId: characterSpriteId.value
} }
gameStore.connection?.emit('gm:characterHair:update', characterHairData, (response: boolean) => { gameStore.connection?.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 +114,7 @@ watch(selectedCharacterHair, (characterHair: CharacterHair | null) => {
onMounted(() => { onMounted(() => {
if (!selectedCharacterHair.value) return if (!selectedCharacterHair.value) return
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => { gameStore.connection?.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
assetManagerStore.setSpriteList(response) assetManagerStore.setSpriteList(response)
}) })
}) })

View File

@ -9,7 +9,7 @@
</button> </button>
</label> </label>
</div> </div>
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll"> <div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5"> <div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
<a <a
v-for="{ data: characterHair } in list" v-for="{ data: characterHair } in list"
@ -32,6 +32,7 @@
</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 { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
@ -52,13 +53,13 @@ const handleSearch = () => {
} }
const createNewCharacterHair = () => { const createNewCharacterHair = () => {
gameStore.connection?.emit('gm:characterHair:create', {}, (response: boolean) => { gameStore.connection?.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[]) => { gameStore.connection?.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
assetManagerStore.setCharacterHairList(response) assetManagerStore.setCharacterHairList(response)
}) })
}) })
@ -92,8 +93,7 @@ function toTop() {
} }
onMounted(() => { onMounted(() => {
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => { gameStore.connection?.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
console.log(response)
assetManagerStore.setCharacterHairList(response) assetManagerStore.setCharacterHairList(response)
}) })
}) })

View File

@ -40,6 +40,7 @@
</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 { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
@ -74,7 +75,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) => { gameStore.connection?.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 +85,7 @@ function removeCharacterType() {
} }
function refreshCharacterTypeList(unsetSelectedCharacterType = true) { function refreshCharacterTypeList(unsetSelectedCharacterType = true) {
gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => { gameStore.connection?.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
assetManagerStore.setCharacterTypeList(response) assetManagerStore.setCharacterTypeList(response)
if (unsetSelectedCharacterType) { if (unsetSelectedCharacterType) {
@ -103,7 +104,7 @@ function saveCharacterType() {
spriteId: characterSpriteId.value spriteId: characterSpriteId.value
} }
gameStore.connection?.emit('gm:characterType:update', characterTypeData, (response: boolean) => { gameStore.connection?.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 +125,7 @@ watch(selectedCharacterType, (characterType: CharacterType | null) => {
onMounted(() => { onMounted(() => {
if (!selectedCharacterType.value) return if (!selectedCharacterType.value) return
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => { gameStore.connection?.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
assetManagerStore.setSpriteList(response) assetManagerStore.setSpriteList(response)
}) })
}) })

View File

@ -9,7 +9,7 @@
</button> </button>
</label> </label>
</div> </div>
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll"> <div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5"> <div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
<a <a
v-for="{ data: characterType } in list" v-for="{ data: characterType } in list"
@ -32,6 +32,7 @@
</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 { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
@ -52,13 +53,13 @@ const handleSearch = () => {
} }
const createNewCharacterType = () => { const createNewCharacterType = () => {
gameStore.connection?.emit('gm:characterType:create', {}, (response: boolean) => { gameStore.connection?.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[]) => { gameStore.connection?.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
assetManagerStore.setCharacterTypeList(response) assetManagerStore.setCharacterTypeList(response)
}) })
}) })
@ -92,8 +93,7 @@ function toTop() {
} }
onMounted(() => { onMounted(() => {
gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => { gameStore.connection?.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
console.log(response)
assetManagerStore.setCharacterTypeList(response) assetManagerStore.setCharacterTypeList(response)
}) })
}) })

View File

@ -44,6 +44,7 @@
</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 { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
@ -80,7 +81,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) => { gameStore.connection?.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 +91,7 @@ function removeItem() {
} }
function refreshItemList(unsetSelectedItem = true) { function refreshItemList(unsetSelectedItem = true) {
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => { gameStore.connection?.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
assetManagerStore.setItemList(response) assetManagerStore.setItemList(response)
if (unsetSelectedItem) { if (unsetSelectedItem) {
@ -110,7 +111,7 @@ function saveItem() {
spriteId: itemSpriteId.value spriteId: itemSpriteId.value
} }
gameStore.connection?.emit('gm:item:update', itemData, (response: boolean) => { gameStore.connection?.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 +133,7 @@ watch(selectedItem, (item: Item | null) => {
onMounted(() => { onMounted(() => {
if (!selectedItem.value) return if (!selectedItem.value) return
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => { gameStore.connection?.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
assetManagerStore.setSpriteList(response) assetManagerStore.setSpriteList(response)
}) })
}) })

View File

@ -9,7 +9,7 @@
</button> </button>
</label> </label>
</div> </div>
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll"> <div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5"> <div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
<a v-for="{ data: item } in list" :key="item.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedItem?.id === item.id }" @click="assetManagerStore.setSelectedItem(item as Item)"> <a v-for="{ data: item } in list" :key="item.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedItem?.id === item.id }" @click="assetManagerStore.setSelectedItem(item as Item)">
<div class="flex items-center gap-2.5"> <div class="flex items-center gap-2.5">
@ -29,6 +29,7 @@
</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 { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
@ -48,13 +49,13 @@ const handleSearch = () => {
} }
const createNewItem = () => { const createNewItem = () => {
gameStore.connection?.emit('gm:item:create', {}, (response: boolean) => { gameStore.connection?.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[]) => { gameStore.connection?.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
assetManagerStore.setItemList(response) assetManagerStore.setItemList(response)
}) })
}) })
@ -88,7 +89,7 @@ function toTop() {
} }
onMounted(() => { onMounted(() => {
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => { gameStore.connection?.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
assetManagerStore.setItemList(response) assetManagerStore.setItemList(response)
}) })
}) })

View File

@ -21,13 +21,6 @@
<label for="tags">Tags</label> <label for="tags">Tags</label>
<ChipsInput v-model="mapObjectTags" @update:modelValue="mapObjectTags = $event" /> <ChipsInput v-model="mapObjectTags" @update:modelValue="mapObjectTags = $event" />
</div> </div>
<div class="form-field-full">
<label for="is-animated">Is animated</label>
<select v-model="mapObjectIsAnimated" class="input-field" name="is-animated">
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>
</div>
<div class="form-field-full"> <div class="form-field-full">
<label for="frame-speed">Frame rate</label> <label for="frame-speed">Frame rate</label>
<input v-model="mapObjectFrameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" /> <input v-model="mapObjectFrameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
@ -51,16 +44,15 @@
<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 { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore() const assetManagerStore = useAssetManagerStore()
const mapEditorStore = useMapEditorStore()
const selectedMapObject = computed(() => assetManagerStore.selectedMapObject) const selectedMapObject = computed(() => assetManagerStore.selectedMapObject)
@ -68,7 +60,6 @@ const mapObjectName = ref('')
const mapObjectTags = ref<string[]>([]) const mapObjectTags = ref<string[]>([])
const mapObjectOriginX = ref(0) const mapObjectOriginX = ref(0)
const mapObjectOriginY = ref(0) const mapObjectOriginY = ref(0)
const mapObjectIsAnimated = ref(false)
const mapObjectFrameRate = ref(0) const mapObjectFrameRate = ref(0)
const mapObjectFrameWidth = ref(0) const mapObjectFrameWidth = ref(0)
const mapObjectFrameHeight = ref(0) const mapObjectFrameHeight = ref(0)
@ -82,14 +73,13 @@ if (selectedMapObject.value) {
mapObjectTags.value = selectedMapObject.value.tags mapObjectTags.value = selectedMapObject.value.tags
mapObjectOriginX.value = selectedMapObject.value.originX mapObjectOriginX.value = selectedMapObject.value.originX
mapObjectOriginY.value = selectedMapObject.value.originY mapObjectOriginY.value = selectedMapObject.value.originY
mapObjectIsAnimated.value = selectedMapObject.value.isAnimated
mapObjectFrameRate.value = selectedMapObject.value.frameRate mapObjectFrameRate.value = selectedMapObject.value.frameRate
mapObjectFrameWidth.value = selectedMapObject.value.frameWidth mapObjectFrameWidth.value = selectedMapObject.value.frameWidth
mapObjectFrameHeight.value = selectedMapObject.value.frameHeight mapObjectFrameHeight.value = selectedMapObject.value.frameHeight
} }
function removeObject() { function removeObject() {
gameStore.connection?.emit('gm:mapObject:remove', { mapObject: selectedMapObject.value?.id }, (response: boolean) => { gameStore.connection?.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
@ -99,16 +89,12 @@ function removeObject() {
} }
function refreshObjectList(unsetSelectedMapObject = true) { function refreshObjectList(unsetSelectedMapObject = true) {
gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => { gameStore.connection?.emit(SocketEvent.GM_MAPOBJECT_LIST, {}, (response: MapObject[]) => {
assetManagerStore.setMapObjectList(response) assetManagerStore.setMapObjectList(response)
if (unsetSelectedMapObject) { if (unsetSelectedMapObject) {
assetManagerStore.setSelectedMapObject(null) assetManagerStore.setSelectedMapObject(null)
} }
if (mapEditorStore.active) {
mapEditorStore.setMapObjectList(response)
}
}) })
} }
@ -126,7 +112,6 @@ function saveObject() {
tags: mapObjectTags.value, tags: mapObjectTags.value,
originX: mapObjectOriginX.value, originX: mapObjectOriginX.value,
originY: mapObjectOriginY.value, originY: mapObjectOriginY.value,
isAnimated: mapObjectIsAnimated.value,
frameRate: mapObjectFrameRate.value, frameRate: mapObjectFrameRate.value,
frameWidth: mapObjectFrameWidth.value, frameWidth: mapObjectFrameWidth.value,
frameHeight: mapObjectFrameHeight.value frameHeight: mapObjectFrameHeight.value
@ -147,7 +132,6 @@ watch(selectedMapObject, (mapObject: MapObject | null) => {
mapObjectTags.value = mapObject.tags mapObjectTags.value = mapObject.tags
mapObjectOriginX.value = mapObject.originX mapObjectOriginX.value = mapObject.originX
mapObjectOriginY.value = mapObject.originY mapObjectOriginY.value = mapObject.originY
mapObjectIsAnimated.value = mapObject.isAnimated
mapObjectFrameRate.value = mapObject.frameRate mapObjectFrameRate.value = mapObject.frameRate
mapObjectFrameWidth.value = mapObject.frameWidth mapObjectFrameWidth.value = mapObject.frameWidth
mapObjectFrameHeight.value = mapObject.frameHeight mapObjectFrameHeight.value = mapObject.frameHeight

View File

@ -8,7 +8,7 @@
</svg> </svg>
</label> </label>
</div> </div>
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll"> <div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5"> <div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
<a v-for="{ data: mapObject } in list" :key="mapObject.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedMapObject?.id === mapObject.id }" @click="assetManagerStore.setSelectedMapObject(mapObject as MapObject)"> <a v-for="{ data: mapObject } in list" :key="mapObject.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedMapObject?.id === mapObject.id }" @click="assetManagerStore.setSelectedMapObject(mapObject as MapObject)">
<div class="flex items-center gap-2.5"> <div class="flex items-center gap-2.5">
@ -29,6 +29,7 @@
<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 { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
@ -47,13 +48,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) => { gameStore.connection?.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[]) => { gameStore.connection?.emit(SocketEvent.GM_MAPOBJECT_LIST, {}, (response: MapObject[]) => {
assetManagerStore.setMapObjectList(response) assetManagerStore.setMapObjectList(response)
}) })
}) })
@ -92,7 +93,7 @@ function toTop() {
} }
onMounted(() => { onMounted(() => {
gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => { gameStore.connection?.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>
@ -21,9 +21,12 @@
<button class="btn-cyan py-2 my-4" type="button" @click.prevent="addNewImage">New action</button> <button class="btn-cyan py-2 my-4" type="button" @click.prevent="addNewImage">New action</button>
<Accordion v-for="action in spriteActions" :key="action.id"> <Accordion v-for="action in spriteActions" :key="action.id">
<template #header> <template #header>
<div class="flex justify-between items-center"> <div class="flex items-center">
{{ action.action }} {{ action.action }}
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="() => spriteActions.splice(spriteActions.indexOf(action), 1)">Delete</button> <div class="ml-auto space-x-2">
<button class="btn-cyan px-4 py-1.5 min-w-24" type="button" @click.stop.prevent="openPreviewModal(action)">View</button>
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.stop.prevent="() => spriteActions.splice(spriteActions.indexOf(action), 1)">Delete</button>
</div>
</div> </div>
</template> </template>
<template #content> <template #content>
@ -40,38 +43,36 @@
<label for="origin-y">Origin Y</label> <label for="origin-y">Origin Y</label>
<input v-model.number="action.originY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" /> <input v-model.number="action.originY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
</div> </div>
<div class="form-field-half"> <div class="form-field-full">
<label for="is-animated">Is animated</label>
<select v-model="action.isAnimated" class="input-field" name="is-animated">
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>
</div>
<div class="form-field-half" v-if="action.isAnimated">
<label for="is-looping">Is looping</label>
<select v-model="action.isLooping" class="input-field" name="is-looping">
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>
</div>
<div class="form-field-full" v-if="action.isAnimated">
<label for="frame-speed">Frame rate</label> <label for="frame-speed">Frame rate</label>
<input v-model.number="action.frameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" /> <input v-model.number="action.frameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
</div> </div>
<div class="form-field-full"> <div class="form-field-full">
<SpriteActionsInput v-model="action.sprites" /> <SpriteActionsInput v-model="action.sprites" @tempOffsetChange="(index, offset) => handleTempOffsetChange(action, index, offset)" />
</div> </div>
</form> </form>
</template> </template>
</Accordion> </Accordion>
<SpritePreview
v-if="selectedAction"
:sprites="selectedAction.sprites"
:frame-rate="selectedAction.frameRate"
:is-modal-open="isModalOpen"
:temp-offset-index="tempOffsetData.index"
:temp-offset="tempOffsetData.offset"
@update:frame-rate="updateFrameRate"
@update:is-modal-open="isModalOpen = $event"
/>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { Sprite, SpriteAction } from '@/application/types' import { SocketEvent } from '@/application/enums'
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 Accordion from '@/components/utilities/Accordion.vue' import Accordion from '@/components/utilities/Accordion.vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
@ -84,6 +85,8 @@ const selectedSprite = computed(() => assetManagerStore.selectedSprite)
const spriteName = ref('') const spriteName = ref('')
const spriteActions = ref<SpriteAction[]>([]) const spriteActions = ref<SpriteAction[]>([])
const isModalOpen = ref(false)
const selectedAction = ref<SpriteAction | null>(null)
if (!selectedSprite.value) { if (!selectedSprite.value) {
console.error('No sprite selected') console.error('No sprite selected')
@ -95,7 +98,7 @@ if (selectedSprite.value) {
} }
function deleteSprite() { function deleteSprite() {
gameStore.connection?.emit('gm:sprite:delete', { id: selectedSprite.value?.id }, (response: boolean) => { gameStore.connection?.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
@ -105,7 +108,7 @@ function deleteSprite() {
} }
function copySprite() { function copySprite() {
gameStore.connection?.emit('gm:sprite:copy', { id: selectedSprite.value?.id }, (response: boolean) => { gameStore.connection?.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
@ -115,7 +118,7 @@ function copySprite() {
} }
function refreshSpriteList(unsetSelectedSprite = true) { function refreshSpriteList(unsetSelectedSprite = true) {
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => { gameStore.connection?.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
assetManagerStore.setSpriteList(response) assetManagerStore.setSpriteList(response)
if (unsetSelectedSprite) { if (unsetSelectedSprite) {
@ -140,8 +143,6 @@ function saveSprite() {
sprites: action.sprites, sprites: action.sprites,
originX: action.originX, originX: action.originX,
originY: action.originY, originY: action.originY,
isAnimated: action.isAnimated,
isLooping: action.isLooping,
frameRate: action.frameRate, frameRate: action.frameRate,
frameWidth: action.frameWidth, frameWidth: action.frameWidth,
frameHeight: action.frameHeight frameHeight: action.frameHeight
@ -149,7 +150,7 @@ function saveSprite() {
}) ?? [] }) ?? []
} }
gameStore.connection?.emit('gm:sprite:update', updatedSprite, (response: boolean) => { gameStore.connection?.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
@ -163,14 +164,11 @@ function addNewImage() {
const newImage: SpriteAction = { const newImage: SpriteAction = {
id: uuidv4(), id: uuidv4(),
spriteId: selectedSprite.value.id, sprite: selectedSprite.value.id,
sprite: selectedSprite.value,
action: 'new_action', action: 'new_action',
sprites: [], sprites: [],
originX: 0, originX: 0,
originY: 0, originY: 0,
isAnimated: false,
isLooping: false,
frameRate: 0, frameRate: 0,
frameWidth: 0, frameWidth: 0,
frameHeight: 0 frameHeight: 0
@ -188,12 +186,40 @@ function sortSpriteActions(actions: SpriteAction[]): SpriteAction[] {
return [...actions].sort((a, b) => a.action.localeCompare(b.action)) return [...actions].sort((a, b) => a.action.localeCompare(b.action))
} }
function openPreviewModal(action: SpriteAction) {
selectedAction.value = action
isModalOpen.value = true
}
function updateFrameRate(value: number) {
if (selectedAction.value) {
selectedAction.value.frameRate = value
}
}
const tempOffsetData = ref<{ index: number | undefined; offset: { x: number; y: number } | undefined }>({
index: undefined,
offset: undefined
})
function handleTempOffsetChange(action: SpriteAction, index: number, offset: { x: number; y: number }) {
if (selectedAction.value === action) {
tempOffsetData.value = { index, offset }
}
}
watch(selectedSprite, (sprite: Sprite | null) => { watch(selectedSprite, (sprite: Sprite | null) => {
if (!sprite) return if (!sprite) return
spriteName.value = sprite.name spriteName.value = sprite.name
spriteActions.value = sortSpriteActions(sprite.spriteActions) spriteActions.value = sortSpriteActions(sprite.spriteActions)
}) })
watch(isModalOpen, (newValue) => {
if (!newValue) {
selectedAction.value = null
}
})
onMounted(() => { onMounted(() => {
if (!selectedSprite.value) return if (!selectedSprite.value) return
}) })

View File

@ -7,7 +7,7 @@
</svg> </svg>
</button> </button>
</div> </div>
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll"> <div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5"> <div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
<a v-for="{ data: sprite } in list" :key="sprite.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedSprite?.id === sprite.id }" @click="assetManagerStore.setSelectedSprite(sprite as Sprite)"> <a v-for="{ data: sprite } in list" :key="sprite.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedSprite?.id === sprite.id }" @click="assetManagerStore.setSelectedSprite(sprite as Sprite)">
<div class="flex items-center gap-2.5"> <div class="flex items-center gap-2.5">
@ -25,6 +25,7 @@
<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 { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
@ -40,13 +41,13 @@ const hasScrolled = ref(false)
const elementToScroll = ref() const elementToScroll = ref()
function newButtonClickHandler() { function newButtonClickHandler() {
gameStore.connection?.emit('gm:sprite:create', {}, (response: boolean) => { gameStore.connection?.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[]) => { gameStore.connection?.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
assetManagerStore.setSpriteList(response) assetManagerStore.setSpriteList(response)
}) })
}) })
@ -85,7 +86,7 @@ function toTop() {
} }
onMounted(() => { onMounted(() => {
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => { gameStore.connection?.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
assetManagerStore.setSpriteList(response) assetManagerStore.setSpriteList(response)
}) })
}) })

View File

@ -1,19 +1,44 @@
<template> <template>
<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" class="max-w-full max-h-full object-contain pointer-events-none" alt="Uploaded image" /> <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">{{ 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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
</button> </button>
<button class="bg-blue-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="Scope image"> <button @click.stop="openOffsetModal(index)" class="bg-blue-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="Scope image">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg width="50px" height="50px" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> <path d="M8.29289 3.70711L1 11V15H5L12.2929 7.70711L8.29289 3.70711Z" fill="white" />
<path d="M9.70711 2.29289L13.7071 6.29289L15.1716 4.82843C15.702 4.29799 16 3.57857 16 2.82843C16 1.26633 14.7337 0 13.1716 0C12.4214 0 11.702 0.297995 11.1716 0.828428L9.70711 2.29289Z" fill="white" />
</svg> </svg>
</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">
<template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">Action offset ({{ selectedImageIndex }})</h3>
</template>
<template #modalBody>
<div class="m-4">
<form method="post" @submit.prevent="saveOffset(index)" class="inline">
<div class="gap-2.5 flex flex-wrap">
<div class="form-field-half">
<label for="offsetX">Offset X</label>
<input class="input-field max-w-64" v-model="tempOffset.x" name="offsetX" id="offsetX" type="number" />
</div>
<div class="form-field-half">
<label for="offsetY">Offset Y</label>
<input class="input-field max-w-64" v-model="tempOffset.y" name="offsetY" id="offsetY" type="number" />
</div>
</div>
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
</form>
</div>
</template>
</Modal>
</div> </div>
<div class="h-20 w-20 p-4 bg-gray-200 bg-opacity-50 rounded justify-center items-center flex hover:cursor-pointer" @click="triggerFileInput" @drop.prevent="onDrop" @dragover.prevent> <div class="h-20 w-20 p-4 bg-gray-200 bg-opacity-50 rounded justify-center items-center flex hover:cursor-pointer" @click="triggerFileInput" @drop.prevent="onDrop" @dragover.prevent>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 invert" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 invert" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@ -25,10 +50,19 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import Modal from '@/components/utilities/Modal.vue'
import { ref, watch } from 'vue'
interface SpriteImage {
url: string
offset: {
x: number
y: number
}
}
interface Props { interface Props {
modelValue: string[] modelValue: SpriteImage[]
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@ -36,11 +70,15 @@ const props = withDefaults(defineProps<Props>(), {
}) })
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:modelValue', value: string[]): void (e: 'update:modelValue', value: SpriteImage[]): void
(e: 'close'): void
(e: 'tempOffsetChange', index: number, offset: { x: number; y: number }): void
}>() }>()
const fileInput = ref<HTMLInputElement | null>(null) const fileInput = ref<HTMLInputElement | null>(null)
const draggedIndex = ref<number | null>(null) const draggedIndex = ref<number | null>(null)
const selectedImageIndex = ref<number | null>(null)
const tempOffset = ref({ x: 0, y: 0 })
const triggerFileInput = () => { const triggerFileInput = () => {
fileInput.value?.click() fileInput.value?.click()
@ -61,19 +99,25 @@ const onDrop = (event: DragEvent) => {
const handleFiles = (files: FileList) => { const handleFiles = (files: FileList) => {
Array.from(files).forEach((file) => { Array.from(files).forEach((file) => {
if (file.type.startsWith('image/')) { if (!file.type.startsWith('image/')) {
const reader = new FileReader() return
reader.onload = (e) => {
if (typeof e.target?.result === 'string') {
updateImages([...props.modelValue, e.target.result])
}
}
reader.readAsDataURL(file)
} }
const reader = new FileReader()
reader.onload = (e) => {
if (typeof e.target?.result === 'string') {
const newImage: SpriteImage = {
url: e.target.result,
offset: { x: 0, y: 0 }
}
updateImages([...props.modelValue, newImage])
}
}
reader.readAsDataURL(file)
}) })
} }
const updateImages = (newImages: string[]) => { const updateImages = (newImages: SpriteImage[]) => {
emit('update:modelValue', newImages) emit('update:modelValue', newImages)
} }
@ -101,4 +145,41 @@ const drop = (event: DragEvent, dropIndex: number) => {
} }
draggedIndex.value = null draggedIndex.value = null
} }
const openOffsetModal = (index: number) => {
selectedImageIndex.value = index
tempOffset.value = { ...props.modelValue[index].offset }
}
const closeOffsetModal = () => {
selectedImageIndex.value = null
}
const saveOffset = (index: number) => {
const newImages = [...props.modelValue]
newImages[index] = {
...newImages[index],
offset: { ...tempOffset.value }
}
updateImages(newImages)
closeOffsetModal()
}
const onOffsetChange = () => {
if (selectedImageIndex.value !== null) {
emit('tempOffsetChange', selectedImageIndex.value, tempOffset.value)
}
}
watch(tempOffset, onOffsetChange, { deep: true })
const imageDimensions = ref<{ [key: number]: { width: number; height: number } }>({})
const updateImageDimensions = (event: Event, index: number) => {
const img = event.target as HTMLImageElement
imageDimensions.value[index] = {
width: img.naturalWidth,
height: img.naturalHeight
}
}
</script> </script>

View File

@ -0,0 +1,151 @@
<template>
<Modal :is-modal-open="isModalOpen" :modal-width="700" :modal-height="330" bg-style="none" @modal:close="closeModal">
<template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">View sprite</h3>
</template>
<template #modalBody>
<div class="m-4 flex gap-8">
<div class="relative">
<div
class="sprite-container bg-gray-800"
:style="{
width: `${maxWidth}px`,
height: `${maxHeight}px`,
position: 'relative',
overflow: 'hidden'
}"
>
<img
v-for="(sprite, index) in spritesWithTempOffset"
:key="index"
:src="sprite.url"
alt="Sprite"
:style="{
position: 'absolute',
left: `${sprite.offset?.x || 0}px`,
bottom: `${sprite.offset?.y || 0}px`,
display: currentFrame === index ? 'block' : 'none',
transform: `scale(${zoomLevel / 100})`,
transformOrigin: 'bottom left'
}"
@load="updateContainerSize"
/>
</div>
</div>
<div class="flex flex-col justify-center gap-8 flex-1">
<div class="flex flex-col">
<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" />
</div>
<div class="flex flex-col">
<label class="block mb-2 text-white">Frame: {{ currentFrame + 1 }} of {{ sprites.length }}</label>
<input type="range" v-model.number="currentFrame" :min="0" :max="sprites.length - 1" step="1" class="w-full accent-cyan-500" @input="stopAnimation" />
</div>
<div class="flex flex-col">
<label class="block mb-2 text-white">Zoom: {{ zoomLevel }}%</label>
<input type="range" v-model.number="zoomLevel" min="10" max="200" step="10" class="w-full accent-cyan-500" />
</div>
</div>
</div>
</template>
</Modal>
</template>
<script setup lang="ts">
import type { SpriteImage } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
const props = defineProps<{
sprites: SpriteImage[]
frameRate: number
isModalOpen?: boolean
tempOffsetIndex?: number
tempOffset?: { x: number; y: number }
}>()
const emit = defineEmits<{
(e: 'update:frameRate', value: number): void
(e: 'update:isModalOpen', value: boolean): void
}>()
const currentFrame = ref(0)
const maxWidth = ref(250)
const maxHeight = ref(250)
const localFrameRate = ref(props.frameRate)
const zoomLevel = ref(100)
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(() => {
return props.sprites.map((sprite, index) => {
if (index === props.tempOffsetIndex && props.tempOffset) {
return { ...sprite, offset: props.tempOffset }
}
return sprite
})
})
function updateContainerSize(event: Event) {
const img = event.target as HTMLImageElement
maxWidth.value = Math.max(maxWidth.value, img.naturalWidth)
maxHeight.value = Math.max(maxHeight.value, img.naturalHeight)
}
function updateAnimation() {
stopAnimation()
if (props.frameRate <= 0 || props.sprites.length === 0) {
currentFrame.value = 0
return
}
animationInterval = window.setInterval(() => {
currentFrame.value = (currentFrame.value + 1) % props.sprites.length
}, 1000 / props.frameRate)
}
function stopAnimation() {
if (animationInterval) {
clearInterval(animationInterval)
animationInterval = null
}
}
function updateFrameRate() {
emit('update:frameRate', localFrameRate.value)
}
function closeModal() {
emit('update:isModalOpen', false)
}
watch(
() => props.frameRate,
(newValue) => {
localFrameRate.value = newValue
updateAnimation()
},
{ immediate: true }
)
watch(() => props.sprites, updateAnimation, { immediate: true })
onMounted(() => {
updateAnimation()
})
onUnmounted(() => {
stopAnimation()
})
</script>
<style scoped>
.sprite-container {
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
</style>

View File

@ -24,16 +24,17 @@
<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 { TileStorage } from '@/storage/storages'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { computed, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue' import { computed, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore() const assetManagerStore = useAssetManagerStore()
const mapEditorStore = useMapEditorStore() const tileStorage = new TileStorage()
const selectedTile = computed(() => assetManagerStore.selectedTile) const selectedTile = computed(() => assetManagerStore.selectedTile)
@ -55,27 +56,24 @@ watch(selectedTile, (tile: Tile | null) => {
tileTags.value = tile.tags tileTags.value = tile.tags
}) })
function deleteTile() { async function deleteTile() {
gameStore.connection?.emit('gm:tile:delete', { id: selectedTile.value?.id }, (response: boolean) => { gameStore.connection?.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
} }
await tileStorage.delete(selectedTile.value!.id)
refreshTileList() refreshTileList()
}) })
} }
function refreshTileList(unsetSelectedTile = true) { function refreshTileList(unsetSelectedTile = true) {
gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => { gameStore.connection?.emit(SocketEvent.GM_TILE_LIST, {}, (response: Tile[]) => {
assetManagerStore.setTileList(response) assetManagerStore.setTileList(response)
if (unsetSelectedTile) { if (unsetSelectedTile) {
assetManagerStore.setSelectedTile(null) assetManagerStore.setSelectedTile(null)
} }
if (mapEditorStore.active) {
mapEditorStore.setTileList(response)
}
}) })
} }

View File

@ -8,7 +8,7 @@
</svg> </svg>
</label> </label>
</div> </div>
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll"> <div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5"> <div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
<a v-for="{ data: tile } in list" :key="tile.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedTile?.id === tile.id }" @click="assetManagerStore.setSelectedTile(tile)"> <a v-for="{ data: tile } in list" :key="tile.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedTile?.id === tile.id }" @click="assetManagerStore.setSelectedTile(tile)">
<div class="flex items-center gap-2.5"> <div class="flex items-center gap-2.5">
@ -29,6 +29,7 @@
<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 { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
@ -47,13 +48,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) => { gameStore.connection?.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[]) => { gameStore.connection?.emit(SocketEvent.GM_TILE_LIST, {}, (response: Tile[]) => {
assetManagerStore.setTileList(response) assetManagerStore.setTileList(response)
}) })
}) })
@ -92,7 +93,7 @@ function toTop() {
} }
onMounted(() => { onMounted(() => {
gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => { gameStore.connection?.emit(SocketEvent.GM_TILE_LIST, {}, (response: Tile[]) => {
assetManagerStore.setTileList(response) assetManagerStore.setTileList(response)
}) })
}) })

View File

@ -0,0 +1,240 @@
<template>
<MapTiles ref="mapTiles" @createCommand="addCommand" v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer />
<PlacedMapObjects ref="mapObjects" @update="updateMapObjects" @updateAndCommit="updateAndCommit" @pauseObjectTracking="pause" @resumeObjectTracking="resume" v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer />
<MapEventTiles ref="eventTiles" @createCommand="addCommand" v-if="tileMap" :tileMap />
</template>
<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 MapTiles from '@/components/gameMaster/mapEditor/mapPartials/MapTiles.vue'
import PlacedMapObjects from '@/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.vue'
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 { onBeforeUnmount, onMounted, onUnmounted, ref, shallowRef, useTemplateRef, watch } from 'vue'
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
const tileMapLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
const mapEditor = useMapEditorComposable()
const scene = useScene()
const mapTiles = useTemplateRef('mapTiles')
const mapObjects = useTemplateRef('mapObjects')
const eventTiles = useTemplateRef('eventTiles')
//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
// 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 draw mode is tile
switch (mapEditor.drawMode.value) {
case 'tile':
mapTiles.value.handlePointer(pointer)
break
case 'map_object':
mapObjects.value.handlePointer(pointer)
break
case 'teleport':
eventTiles.value.handlePointer(pointer)
break
case 'blocking tile':
eventTiles.value.handlePointer(pointer)
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>

View File

@ -1,75 +0,0 @@
<template>
<MapTiles @tileMap:create="tileMap = $event" />
<MapObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
<MapEventTiles v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
<Toolbar @save="save" @clear="clear" />
<MapList />
<TileList />
<ObjectList />
<MapSettings />
<TeleportModal />
</template>
<script setup lang="ts">
import { type Map } from '@/application/types'
import MapEventTiles from '@/components/gameMaster/mapEditor/mapPartials/MapEventTiles.vue'
import MapTiles from '@/components/gameMaster/mapEditor/mapPartials/MapTiles.vue'
import MapObjects from '@/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.vue'
import MapList from '@/components/gameMaster/mapEditor/partials/MapList.vue'
import ObjectList from '@/components/gameMaster/mapEditor/partials/MapObjectList.vue'
import MapSettings from '@/components/gameMaster/mapEditor/partials/MapSettings.vue'
import TeleportModal from '@/components/gameMaster/mapEditor/partials/TeleportModal.vue'
import TileList from '@/components/gameMaster/mapEditor/partials/TileList.vue'
// Components
import Toolbar from '@/components/gameMaster/mapEditor/partials/Toolbar.vue'
import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { onUnmounted, ref, shallowRef } from 'vue'
const gameStore = useGameStore()
const mapEditorStore = useMapEditorStore()
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
function clear() {
if (!mapEditorStore.map) return
// Clear objects, event tiles and tiles
mapEditorStore.map.placedMapObjects = []
mapEditorStore.map.mapEventTiles = []
mapEditorStore.triggerClearTiles()
}
function save() {
if (!mapEditorStore.map) return
const data = {
mapId: mapEditorStore.map.id,
name: mapEditorStore.mapSettings.name,
width: mapEditorStore.mapSettings.width,
height: mapEditorStore.mapSettings.height,
tiles: mapEditorStore.map.tiles,
pvp: mapEditorStore.map.pvp,
mapEffects: mapEditorStore.map.mapEffects?.map(({ id, effect, strength }) => ({ id, effect, strength })) ?? [],
mapEventTiles: mapEditorStore.map.mapEventTiles?.map(({ id, type, positionX, positionY, teleport }) => ({ id, type, positionX, positionY, teleport })) ?? [],
placedMapObjects: mapEditorStore.map.placedMapObjects?.map(({ id, mapObject, depth, isRotated, positionX, positionY }) => ({ id, mapObject, depth, isRotated, positionX, positionY })) ?? []
}
console.log(data.mapEventTiles)
if (mapEditorStore.isSettingsModalShown) {
mapEditorStore.toggleSettingsModal()
}
gameStore.connection?.emit('gm:map:update', data, (response: Map) => {
mapEditorStore.setMap(response)
})
}
onUnmounted(() => {
mapEditorStore.reset()
})
</script>

View File

@ -1,118 +1,156 @@
<template> <template>
<Image v-for="tile in mapEditorStore.map?.mapEventTiles" v-bind="getImageProps(tile)" /> <Image v-for="tile in mapEditor.currentMap.value?.mapEventTiles" v-bind="getImageProps(tile)" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { MapEventTileType, type MapEventTile } 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 { useMapEditorStore } from '@/stores/mapEditorStore' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { Image, useScene } from 'phavuer' import { cloneArray, getTile, tileToWorldX, tileToWorldY } from '@/services/mapService'
import { onMounted, onUnmounted } from 'vue' import { Image } from 'phavuer'
const scene = useScene() const mapEditor = useMapEditorComposable()
const mapEditorStore = useMapEditorStore()
defineExpose({ handlePointer, finalizeCommand, clearTiles })
const emit = defineEmits(['createCommand'])
const props = defineProps<{ const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap tileMap: Phaser.Tilemaps.Tilemap
}>() }>()
// *** 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, operation: 'draw' | 'erase' | 'clear') {
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 {
x: tileToWorldX(props.tilemap, tile.positionX, tile.positionY), x: tileToWorldX(props.tileMap, tile.positionX, tile.positionY),
y: tileToWorldY(props.tilemap, tile.positionX, tile.positionY), y: tileToWorldY(props.tileMap, tile.positionX, tile.positionY),
texture: tile.type, texture: tile.type,
depth: 999 depth: 999
} }
} }
function pencil(pointer: Phaser.Input.Pointer) { function pencil(pointer: Phaser.Input.Pointer, map: MapT) {
// Check if map is set
if (!mapEditorStore.map) return
// Check if tool is pencil
if (mapEditorStore.tool !== 'pencil') return
// Check if draw mode is blocking tile or teleport
if (mapEditorStore.drawMode !== 'blocking tile' && mapEditorStore.drawMode !== 'teleport') 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 there is a tile // Check if there is a tile
const tile = getTile(props.tilemap, 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 = mapEditorStore.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 teleport, check if there is a selected map // If teleport, check if there is a selected map
if (mapEditorStore.drawMode === 'teleport' && !mapEditorStore.teleportSettings.toMap) return if (mapEditor.drawMode.value === 'teleport' && !mapEditor.teleportSettings.value.toMapId) return
const newEventTile = { const newEventTile = {
id: uuidv4(), id: uuidv4() as UUID,
mapId: mapEditorStore.map.id, mapId: map.id,
map: mapEditorStore.map, map: map,
type: mapEditorStore.drawMode === '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:
mapEditorStore.drawMode === 'teleport' mapEditor.drawMode.value === 'teleport'
? { ? {
toMap: mapEditorStore.teleportSettings.toMap, toMap: mapEditor.teleportSettings.value.toMapId,
toPositionX: mapEditorStore.teleportSettings.toPositionX, toPositionX: mapEditor.teleportSettings.value.toPositionX,
toPositionY: mapEditorStore.teleportSettings.toPositionY, toPositionY: mapEditor.teleportSettings.value.toPositionY,
toRotation: mapEditorStore.teleportSettings.toRotation toRotation: mapEditor.teleportSettings.value.toRotation
} }
: undefined : undefined
} }
mapEditorStore.map.mapEventTiles = mapEditorStore.map.mapEventTiles.concat(newEventTile as MapEventTile) createCommandUpdate(newEventTile, 'draw')
map.mapEventTiles.push(newEventTile)
} }
function eraser(pointer: Phaser.Input.Pointer) { function erase(pointer: Phaser.Input.Pointer, map: MapT) {
// Check if map is set
if (!mapEditorStore.map) return
// Check if tool is pencil
if (mapEditorStore.tool !== 'eraser') return
// Check if draw mode is blocking tile or teleport
if (mapEditorStore.eraserMode !== 'blocking tile' && mapEditorStore.eraserMode !== 'teleport') 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 there is a tile // Check if there is a tile
const tile = getTile(props.tilemap, 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 = mapEditorStore.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
mapEditorStore.map.mapEventTiles = mapEditorStore.map.mapEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id) map.mapEventTiles = map.mapEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id)
} }
onMounted(() => { function handlePointer(pointer: Phaser.Input.Pointer) {
scene.input.on(Phaser.Input.Events.POINTER_DOWN, pencil) const map = mapEditor.currentMap.value
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil) if (!map) return
scene.input.on(Phaser.Input.Events.POINTER_DOWN, eraser)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
})
onUnmounted(() => { if (pointer.event.altKey) return
scene.input.off(Phaser.Input.Events.POINTER_DOWN, pencil)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil) switch (mapEditor.tool.value) {
scene.input.off(Phaser.Input.Events.POINTER_DOWN, eraser) case 'pencil':
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser) pencil(pointer, map)
}) break
case 'eraser':
erase(pointer, map)
break
}
}
function clearTiles() {
if (mapEditor.currentMap.value.mapEventTiles.length === 0) return
createCommandUpdate(null, 'clear')
finalizeCommand()
}
</script> </script>

View File

@ -1,227 +1,157 @@
<template> <template>
<Controls :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 type { TextureData } from '@/application/types'
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 { useGameStore } from '@/stores/gameStore' import { cloneArray, createTileArray, getTile, placeTile, placeTiles } from '@/services/mapService'
import { useMapEditorStore } from '@/stores/mapEditorStore' import { onMounted, ref, watch } from 'vue'
import { useScene } from 'phavuer'
import { onMounted, onUnmounted, watch } from 'vue'
const emit = defineEmits(['tileMap:create']) const mapEditor = useMapEditorComposable()
const scene = useScene() defineExpose({ handlePointer, finalizeCommand, clearTiles })
const gameStore = useGameStore()
const mapEditorStore = useMapEditorStore()
const tileMap = createTileMap()
const tileLayer = createTileLayer()
/** const emit = defineEmits(['createCommand'])
* A Tilemap is a container for Tilemap data.
* This isn't a display object, rather, it holds data about the map and allows you to add tilesets and tilemap layers to it.
* A map can have one or more tilemap layers, which are the display objects that actually render the tiles.
*/
function createTileMap() {
const mapData = new Phaser.Tilemaps.MapData({
width: mapEditorStore.map?.width,
height: mapEditorStore.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, mapData) const props = defineProps<{
emit('tileMap:create', newTileMap) tileMap: Phaser.Tilemaps.Tilemap
tileMapLayer: Phaser.Tilemaps.TilemapLayer
}>()
return newTileMap // *** COMMAND STATE ***
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
}
constructor(operation: 'draw' | 'erase' | 'clear', tileName: string) {
this.operation = operation
this.tileName = tileName
}
} }
/** function createCommandUpdate(x: number, y: number, tileName: string, operation: 'draw' | 'erase' | 'clear') {
* A Tileset is a combination of a single image containing the tiles and a container for data about each tile. if (!currentCommand) {
*/ currentCommand = new TileCommand(operation, tileName)
function createTileLayer() { }
const tilesArray = gameStore.getLoadedAssetsByGroup('tiles')
const tilesetImages = Array.from(tilesArray).map((tile: TextureData, index: number) => { //If position is already in, do not proceed
return tileMap.addTilesetImage(tile.key, tile.key, config.tile_size.width, config.tile_size.height, 1, 2, index + 1, { x: 0, y: -config.tile_size.height }) for (const vec of currentCommand.affectedTiles) {
}) as any if (vec[0] === x && vec[1] === y) return
}
// Add blank tile currentCommand.affectedTiles.push([x, y])
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 }))
const layer = tileMap.createBlankLayer('tiles', tilesetImages, 0, config.tile_size.height) as Phaser.Tilemaps.TilemapLayer
layer.setDepth(0)
layer.setCullPadding(2, 2)
return layer
} }
function pencil(pointer: Phaser.Input.Pointer) { function finalizeCommand() {
// Check if map is set if (!currentCommand) return
if (!mapEditorStore.map) return emit('createCommand', currentCommand)
currentCommand = null
}
// Check if tool is pencil // *** HANDLERS ***
if (mapEditorStore.tool !== 'pencil') return
// Check if draw mode is tile function draw(pointer: Phaser.Input.Pointer, tileName: string) {
if (mapEditorStore.drawMode !== 'tile') return let map = mapEditor.currentMap.value
if (!map) return
// Check if there is a selected tile
if (!mapEditorStore.selectedTile) 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 there is a tile // Check if there is a tile
const tile = getTile(tileLayer, 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, tileLayer, tile.x, tile.y, mapEditorStore.selectedTile) 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
mapEditorStore.map.tiles[tile.y][tile.x] = mapEditorStore.selectedTile map.tiles[tile.y][tile.x] = tileName
}
function eraser(pointer: Phaser.Input.Pointer) {
// Check if map is set
if (!mapEditorStore.map) return
// Check if tool is pencil
if (mapEditorStore.tool !== 'eraser') return
// Check if draw mode is tile
if (mapEditorStore.eraserMode !== 'tile') 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
if (pointer.event.altKey) return
// Check if there is a tile
const tile = getTile(tileLayer, pointer.worldX, pointer.worldY)
if (!tile) return
// Place tile
placeTile(tileMap, tileLayer, tile.x, tile.y, 'blank_tile')
// Adjust mapEditorStore.map.tiles
mapEditorStore.map.tiles[tile.y][tile.x] = 'blank_tile'
} }
function paint(pointer: Phaser.Input.Pointer) { function paint(pointer: Phaser.Input.Pointer) {
// Check if map is set let map = mapEditor.currentMap.value
if (!mapEditorStore.map) return if (!map) return
// Check if tool is pencil
if (mapEditorStore.tool !== 'paint') return
// Check if there is a selected tile
if (!mapEditorStore.selectedTile) 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
if (pointer.event.altKey) return
// Set new tileArray with selected tile // Set new tileArray with selected tile
setLayerTiles(tileMap, tileLayer, createTileArray(tileMap.width, tileMap.height, mapEditorStore.selectedTile)) const tileArray = createTileArray(props.tileMap.width, props.tileMap.height, mapEditor.selectedTile.value)
placeTiles(props.tileMap, props.tileMapLayer, tileArray)
// Adjust mapEditorStore.map.tiles // Adjust mapEditorStore.map.tiles
mapEditorStore.map.tiles = createTileArray(tileMap.width, tileMap.height, mapEditorStore.selectedTile) map.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
function tilePicker(pointer: Phaser.Input.Pointer) { function tilePicker(pointer: Phaser.Input.Pointer) {
// Check if map is set let map = mapEditor.currentMap.value
if (!mapEditorStore.map) return if (!map) return
// Check if tool is pencil // Check if there is a tile
if (mapEditorStore.tool !== 'pencil') return const tile = getTile(props.tileMapLayer, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if draw mode is tile // Select the tile
if (mapEditorStore.drawMode !== 'tile') return mapEditor.setSelectedTile(map.tiles[tile.y][tile.x])
}
function handlePointer(pointer: Phaser.Input.Pointer) {
// 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
// Check if alt is pressed // Check if alt is pressed
if (!pointer.event.altKey) return if (pointer.event.altKey) {
tilePicker(pointer)
// Check if there is a tile
const tile = getTile(tileLayer, pointer.worldX, pointer.worldY)
if (!tile) return
// Select the tile
mapEditorStore.setSelectedTile(mapEditorStore.map.tiles[tile.y][tile.x])
}
watch(
() => mapEditorStore.shouldClearTiles,
(shouldClear) => {
if (shouldClear && mapEditorStore.map) {
const blankTiles = createTileArray(tileMap.width, tileMap.height, 'blank_tile')
setLayerTiles(tileMap, tileLayer, blankTiles)
mapEditorStore.map.tiles = blankTiles
mapEditorStore.resetClearTilesFlag()
}
}
)
onMounted(() => {
if (!mapEditorStore.map?.tiles) {
return return
} }
// First fill the entire map with blank tiles using current map dimensions // Check if draw mode is tile
const blankTiles = createTileArray(mapEditorStore.map.width, mapEditorStore.map.height, 'blank_tile') switch (mapEditor.tool.value) {
case 'pencil':
// Then overlay the map tiles, but only within the current map dimensions draw(pointer, mapEditor.selectedTile.value!)
const mapTiles = mapEditorStore.map.tiles break
for (let y = 0; y < mapEditorStore.map.height; y++) { case 'eraser':
for (let x = 0; x < mapEditorStore.map.width; x++) { draw(pointer, 'blank_tile')
// Only copy if the source tiles array has this position break
if (mapTiles[y] && mapTiles[y][x] !== undefined) { case 'paint':
blankTiles[y][x] = mapTiles[y][x] paint(pointer)
} break
}
} }
}
setLayerTiles(tileMap, tileLayer, blankTiles) // *** LIFECYCLE ***
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil) function clearTiles() {
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser) const tileArray = createTileArray(props.tileMap.width, props.tileMap.height, 'blank_tile')
scene.input.on(Phaser.Input.Events.POINTER_DOWN, paint) placeTiles(props.tileMap, props.tileMapLayer, tileArray)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, tilePicker) createCommandUpdate(0, 0, 'blank_tile', 'clear')
}) finalizeCommand()
}
onUnmounted(() => { onMounted(async () => {
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil) if (!mapEditor.currentMap.value) return
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser) const mapState = mapEditor.currentMap.value
scene.input.off(Phaser.Input.Events.POINTER_DOWN, paint)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, tilePicker)
tileMap.destroyLayer('tiles') placeTiles(props.tileMap, props.tileMapLayer, mapState.tiles)
tileMap.removeAllLayers()
tileMap.destroy()
}) })
</script> </script>

View File

@ -1,45 +0,0 @@
<template>
<Image v-if="gameStore.getLoadedAsset(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,246 +1,201 @@
<template> <template>
<SelectedPlacedMapObjectComponent v-if="selectedPlacedMapObject" :placedMapObject="selectedPlacedMapObject" @move="moveMapObject" @rotate="rotatePlacedMapObject" @delete="deletePlacedMapObject" /> <PlacedMapObject
<PlacedMapObject v-for="placedMapObject in mapEditorStore.map?.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 { MapObject, 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 { getTile } from '@/composables/mapComposable' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { useMapEditorStore } from '@/stores/mapEditorStore' import { getTile } from '@/services/mapService'
import { useScene } from 'phavuer' import { useScene } from 'phavuer'
import { onMounted, onUnmounted, 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 mapEditorStore = useMapEditorStore() const mapEditor = useMapEditorComposable()
const selectedPlacedMapObject = ref<PlacedMapObjectT | null>(null) const map = computed(() => mapEditor.currentMap.value!)
const movingPlacedMapObject = ref<PlacedMapObjectT | null>(null)
const emit = defineEmits<{ (e: 'update', map: MapT): void; (e: 'updateAndCommit', map: MapT): void; (e: 'pauseObjectTracking'): void; (e: 'resumeObjectTracking'): void }>()
defineExpose({ handlePointer })
const props = defineProps<{ const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap tileMap: Tilemap
tileMapLayer: TilemapLayer
}>() }>()
function pencil(pointer: Phaser.Input.Pointer) { const previewPosition = ref({ x: 0, y: 0 })
// Check if map is set const previewPlacedMapObject = computed(() => ({
if (!mapEditorStore.map) return id: mapEditor.selectedMapObject.value!.id,
mapObject: mapEditor.selectedMapObject.value!.id,
isRotated: false,
positionX: previewPosition.value.x,
positionY: previewPosition.value.y
}))
// Check if tool is pencil function updatePreviewPosition(pointer: Phaser.Input.Pointer) {
if (mapEditorStore.tool !== 'pencil') return const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
if (!tile || (previewPosition.value.x === tile.x && previewPosition.value.y === tile.y)) return
// Check if draw mode is map_object previewPosition.value = { x: tile.x, y: tile.y }
if (mapEditorStore.drawMode !== 'map_object') return }
// Check if there is a selected object function pencil(pointer: Phaser.Input.Pointer, map: MapT) {
if (!mapEditorStore.selectedMapObject) return emit('pauseObjectTracking')
const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
// 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
if (pointer.event.altKey) return
// Check if there is a tile
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return if (!tile) return
// Check if object already exists on position // Check if object already exists on position
const existingPlacedMapObject = mapEditorStore.map.placedMapObjects.find((placedMapObject) => placedMapObject.positionX === tile.x && placedMapObject.positionY === tile.y) const existingPlacedMapObject = findObjectByPointer(pointer, mapEditor.currentMap.value!)
if (existingPlacedMapObject) return if (existingPlacedMapObject) return
const newPlacedMapObject = { if (!mapEditor.selectedMapObject.value) return
const newPlacedMapObject: PlacedMapObjectT = {
id: uuidv4(), id: uuidv4(),
map: mapEditorStore.map, mapObject: mapEditor.selectedMapObject.value.id,
mapObject: mapEditorStore.selectedMapObject,
depth: 0,
isRotated: false, isRotated: false,
positionX: tile.x, positionX: tile.x,
positionY: tile.y positionY: tile.y
} }
// Add new object to mapObjects // Add new object to mapObjects
mapEditorStore.map.placedMapObjects = mapEditorStore.map.placedMapObjects.concat(newPlacedMapObject as PlacedMapObjectT) mapEditor.selectedPlacedObject.value = newPlacedMapObject
map.placedMapObjects.push(newPlacedMapObject)
emit('update', map)
} }
function eraser(pointer: Phaser.Input.Pointer) { function eraser(pointer: Phaser.Input.Pointer, map: MapT) {
// Check if map is set emit('pauseObjectTracking')
if (!mapEditorStore.map) return
// Check if tool is eraser // Check if object already exists on position
if (mapEditorStore.tool !== 'eraser') return const existingPlacedMapObject = findObjectByPointer(pointer, map)
if (!existingPlacedMapObject) return
// Check if draw mode is map_object // Remove existing object
if (mapEditorStore.eraserMode !== 'map_object') return map.placedMapObjects = map.placedMapObjects.filter((placedMapObject) => placedMapObject.id !== existingPlacedMapObject.id)
// Check if left mouse button is pressed emit('update', map)
if (!pointer.isDown) return }
// Check if shift is not pressed, this means we are moving the camera function findObjectByPointer(pointer: Phaser.Input.Pointer, map: MapT): PlacedMapObjectT | undefined {
if (pointer.event.shiftKey) return 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) {
// Check if object already exists on position
const existingPlacedMapObject = findObjectByPointer(pointer, map)
if (!existingPlacedMapObject) return
// Select the object
mapEditor.setSelectedMapObject(existingPlacedMapObject.mapObject as MapObject)
}
function moveMapObject(id: string, map: MapT) {
mapEditor.movingPlacedObject.value = map.placedMapObjects.find((object) => object.id === id) as PlacedMapObjectT
emit('pauseObjectTracking')
function handlePointerMove(pointer: Phaser.Input.Pointer) {
if (!mapEditor.movingPlacedObject.value) return
const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
if (!tile) return
mapEditor.movingPlacedObject.value.positionX = tile.x
mapEditor.movingPlacedObject.value.positionY = tile.y
}
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
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
console.log(id)
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) {
map.placedMapObjects.map((placed) => {
if (placed.id === id) {
placed.isRotated = !placed.isRotated
}
})
emit('updateAndCommit', map)
}
function deletePlacedMapObject(id: string, map: MapT) {
map.placedMapObjects = map.placedMapObjects.filter((object) => object.id !== id)
mapEditor.selectedPlacedObject.value = null
emit('updateAndCommit', map)
}
function clickPlacedMapObject(placedMapObject: PlacedMapObjectT) {
mapEditor.selectedPlacedObject.value = placedMapObject
// If alt is pressed, select the object
if (scene.input.activePointer.event.altKey) {
mapEditor.setSelectedMapObject(placedMapObject.mapObject as MapObject)
}
}
function handlePointer(pointer: Phaser.Input.Pointer) {
const map = mapEditor.currentMap.value
if (!map) 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 there is a tile
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if object already exists on position
const existingPlacedMapObject = mapEditorStore.map.placedMapObjects.find((placedMapObject) => placedMapObject.positionX === tile.x && placedMapObject.positionY === tile.y)
if (!existingPlacedMapObject) return
// Remove existing object
mapEditorStore.map.placedMapObjects = mapEditorStore.map.placedMapObjects.filter((placedMapObject) => placedMapObject.id !== existingPlacedMapObject.id)
}
function objectPicker(pointer: Phaser.Input.Pointer) {
// Check if map is set
if (!mapEditorStore.map) return
// Check if tool is pencil // Check if tool is pencil
if (mapEditorStore.tool !== 'pencil') return switch (mapEditor.tool.value) {
case 'pencil':
// Check if draw mode is map_object pencil(pointer, map)
if (mapEditorStore.drawMode !== 'map_object') return break
case 'eraser':
// Check if left mouse button is pressed eraser(pointer, map)
if (!pointer.isDown) return break
case 'object picker':
// Check if shift is not pressed, this means we are moving the camera objectPicker(pointer, map)
if (pointer.event.shiftKey) return break
// If alt is not pressed, return
if (!pointer.event.altKey) return
// Check if there is a tile
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if object already exists on position
const existingPlacedMapObject = mapEditorStore.map.placedMapObjects.find((placedMapObject) => placedMapObject.positionX === tile.x && placedMapObject.positionY === tile.y)
if (!existingPlacedMapObject) return
// Select the object
mapEditorStore.setSelectedMapObject(existingPlacedMapObject.mapObject)
}
function moveMapObject(id: string) {
// Check if map is set
if (!mapEditorStore.map) return
movingPlacedMapObject.value = mapEditorStore.map.placedMapObjects.find((object) => object.id === id) as PlacedMapObjectT
function handlePointerMove(pointer: Phaser.Input.Pointer) {
if (!movingPlacedMapObject.value) return
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
movingPlacedMapObject.value.positionX = tile.x
movingPlacedMapObject.value.positionY = tile.y
}
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)
}
function rotatePlacedMapObject(id: string) {
// Check if map is set
if (!mapEditorStore.map) return
mapEditorStore.map.placedMapObjects = mapEditorStore.map.placedMapObjects.map((placedMapObject) => {
if (placedMapObject.id === id) {
return {
...placedMapObject,
isRotated: !placedMapObject.isRotated
}
}
return placedMapObject
})
}
function deletePlacedMapObject(id: string) {
// Check if map is set
if (!mapEditorStore.map) return
mapEditorStore.map.placedMapObjects = mapEditorStore.map.placedMapObjects.filter((object) => object.id !== id)
selectedPlacedMapObject.value = null
}
function clickPlacedMapObject(placedMapObject: PlacedMapObjectT) {
selectedPlacedMapObject.value = placedMapObject
// If alt is pressed, select the object
if (scene.input.activePointer.event.altKey) {
mapEditorStore.setSelectedMapObject(placedMapObject.mapObject)
} }
} }
onMounted(() => { onMounted(() => {
scene.input.on(Phaser.Input.Events.POINTER_DOWN, pencil) scene.input.on(Phaser.Input.Events.POINTER_MOVE, updatePreviewPosition)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, eraser)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, objectPicker)
}) })
onUnmounted(() => { onUnmounted(() => {
scene.input.off(Phaser.Input.Events.POINTER_DOWN, pencil) scene.input.off(Phaser.Input.Events.POINTER_MOVE, updatePreviewPosition)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, eraser)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, objectPicker)
}) })
// watch mapEditorStore.mapObjectList and update originX and originY of objects in mapObjects
watch(
() => mapEditorStore.mapObjectList,
(newMapObjects) => {
if (!mapEditorStore.map) return
const updatedMapObjects = mapEditorStore.map.placedMapObjects.map((mapObject) => {
const updatedMapObject = newMapObjects.find((obj) => obj.id === mapObject.mapObject.id)
if (updatedMapObject) {
return {
...mapObject,
mapObject: {
...mapObject.mapObject,
originX: updatedMapObject.originX,
originY: updatedMapObject.originY
}
}
}
return mapObject
})
// Update the map with the new mapObjects
mapEditorStore.setMap({
...mapEditorStore.map,
placedMapObjects: updatedMapObjects
})
// Update selectedMapObject if it's set
if (mapEditorStore.selectedMapObject) {
const updatedMapObject = newMapObjects.find((obj) => obj.id === mapEditorStore.selectedMapObject?.id)
if (updatedMapObject) {
mapEditorStore.setSelectedMapObject({
...mapEditorStore.selectedMapObject,
originX: updatedMapObject.originX,
originY: updatedMapObject.originY
})
}
}
}
// { deep: true }
)
</script> </script>

View File

@ -1,9 +1,8 @@
<template> <template>
<Modal :isModalOpen="true" @modal:close="() => mapEditorStore.toggleCreateMapModal()" :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>
<template #modalBody> <template #modalBody>
<div class="m-4"> <div class="m-4">
<form method="post" @submit.prevent="submit" class="inline"> <form method="post" @submit.prevent="submit" class="inline">
@ -14,15 +13,15 @@
</div> </div>
<div class="form-field-half"> <div class="form-field-half">
<label for="name">Width</label> <label for="name">Width</label>
<input class="input-field max-w-64" v-model="width" name="name" id="name" type="number" /> <input class="input-field max-w-64" v-model="width" name="width" id="width" type="number" />
</div> </div>
<div class="form-field-half"> <div class="form-field-half">
<label for="name">Height</label> <label for="name">Height</label>
<input class="input-field max-w-64" v-model="height" name="name" id="name" type="number" /> <input class="input-field max-w-64" v-model="height" name="height" id="height" type="number" />
</div> </div>
<div class="form-field-full"> <div class="form-field-full">
<label for="name">PVP enabled</label> <label for="name">PVP enabled</label>
<select class="input-field" name="pvp" id="pvp"> <select class="input-field" v-model="pvp" name="pvp" id="pvp">
<option :value="false">No</option> <option :value="false">No</option>
<option :value="true">Yes</option> <option :value="true">Yes</option>
</select> </select>
@ -36,23 +35,47 @@
</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 { MapStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore' import { ref, useTemplateRef } from 'vue'
import { ref } from 'vue'
const emit = defineEmits(['create'])
const gameStore = useGameStore() const gameStore = useGameStore()
const mapEditorStore = useMapEditorStore() const mapStorage = new MapStorage()
const modalRef = useTemplateRef('modalRef')
const name = ref('') const name = ref('')
const width = ref(0) const width = ref(0)
const height = ref(0) const height = ref(0)
const pvp = ref(false)
function submit() { defineExpose({ open: () => modalRef.value?.open() })
gameStore.connection?.emit('gm:map:create', { name: name.value, width: width.value, height: height.value }, (response: Map[]) => {
mapEditorStore.setMapList(response) async function submit() {
gameStore.connection?.emit(SocketEvent.GM_MAP_CREATE, { name: name.value, width: width.value, height: height.value }, async (response: Map | false) => {
if (!response) {
return
}
// Reset form
name.value = ''
width.value = 0
height.value = 0
pvp.value = false
// Add map to storage
await mapStorage.add(response)
// Let list know to fetch new maps
emit('create')
}) })
mapEditorStore.toggleCreateMapModal()
// Close modal
modalRef.value?.close()
} }
</script> </script>

View File

@ -1,61 +1,84 @@
<template> <template>
<CreateMap v-if="mapEditorStore.isCreateMapModalShown" /> <Modal ref="modalRef" :is-resizable="false" :modal-width="300" :modal-height="360" bg-style="none">
<Modal :is-modal-open="mapEditorStore.isMapListModalShown" @modal:close="() => mapEditorStore.toggleMapListModal()" :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>
<template #modalBody> <template #modalBody>
<div class="my-4 mx-auto"> <div class="my-4 mx-auto h-full">
<div class="text-center mb-4 px-2 flex gap-2.5"> <div class="text-center mb-4 px-2 flex gap-2.5">
<button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="fetchMaps">Refresh</button> <button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="fetchMaps">Refresh</button>
<button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="() => mapEditorStore.toggleCreateMapModal()">New</button> <button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="createMapModal?.open">New</button>
</div> </div>
<div class="relative p-2.5 cursor-pointer flex gap-y-2.5 gap-x-5 flex-wrap" v-for="(map, index) in mapEditorStore.mapList" :key="map.id"> <div class="overflow-y-auto h-[calc(100%-20px)]">
<div class="absolute left-0 top-0 w-full h-px bg-gray-500" v-if="index === 0"></div> <div class="relative p-2.5 cursor-pointer flex gap-y-2.5 gap-x-5 flex-wrap" v-for="(map, index) in mapList" :key="map.id">
<div class="flex gap-3 items-center w-full" @click="() => loadMap(map.id)"> <div class="absolute left-0 top-0 w-full h-px bg-gray-500" v-if="index === 0"></div>
<span>{{ map.name }}</span> <div class="flex gap-3 items-center w-full" @click="() => loadMap(map.id)">
<span class="ml-auto gap-1 flex"> <span>{{ map.name }}</span>
<button class="btn-red w-7 h-7 z-50 flex items-center justify-center" @click.stop="() => deleteMap(map.id)">x</button> <span class="ml-auto gap-1 flex">
</span> <button class="btn-red w-7 h-7 z-50 flex items-center justify-center" @click.stop="() => deleteMap(map.id)">x</button>
</span>
</div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</div> </div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</div> </div>
</div> </div>
</template> </template>
</Modal> </Modal>
<CreateMap ref="createMapModal" @create="fetchMaps" />
</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 { 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 } from 'vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const mapEditorStore = useMapEditorStore()
onMounted(async () => { const mapEditor = useMapEditorComposable()
fetchMaps() const mapStorage = new MapStorage()
const mapList = ref<Map[]>([])
const modalRef = useTemplateRef('modalRef')
const createMapModal = useTemplateRef('createMapModal')
defineEmits(['open-create-map'])
defineExpose({
open: () => modalRef.value?.open()
}) })
function fetchMaps() { onMounted(async () => {
gameStore.connection?.emit('gm:map:list', {}, (response: Map[]) => { await fetchMaps()
mapEditorStore.setMapList(response) })
})
async function fetchMaps() {
mapList.value = await mapStorage.getAll()
} }
function loadMap(id: UUID) { function loadMap(id: string) {
gameStore.connection?.emit('gm:map:request', { mapId: id }, (response: Map) => { gameStore.connection?.emit(SocketEvent.GM_MAP_REQUEST, { mapId: id }, (response: Map) => {
mapEditorStore.setMap(response) mapEditor.loadMap(response)
}) })
mapEditorStore.toggleMapListModal() modalRef.value?.close()
} }
function deleteMap(id: UUID) { async function deleteMap(id: string) {
gameStore.connection?.emit('gm:map:delete', { mapId: id }, () => { gameStore.connection?.emit(SocketEvent.GM_MAP_DELETE, { mapId: id }, async (response: boolean) => {
fetchMaps() if (!response) {
gameStore.addNotification({
title: 'Error',
message: 'Failed to delete map.'
})
return
}
await mapStorage.delete(id)
await fetchMaps()
}) })
} }
</script> </script>

View File

@ -1,72 +1,65 @@
<template> <template>
<Modal :isModalOpen="mapEditorStore.isMapObjectListModalShown" :modal-width="645" :modal-height="260" @modal:close="() => (mapEditorStore.isMapObjectListModalShown = false)" :bg-style="'none'"> <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'">
<template #modalHeader> <div class="flex flex-col gap-2.5 p-2.5">
<h3 class="text-lg text-white">Map objects</h3> <div class="relative flex">
</template> <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" />
<template #modalBody> <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>
<div> <div class="flex">
<label class="mb-1.5 font-titles hidden" for="search">Search...</label> <select class="input-field w-full" name="lists" v-model="mapEditor.drawMode.value" @change="(event: any) => mapEditor.setDrawMode(event.target.value)">
<input @mousedown.stop class="input-field" type="text" name="search" placeholder="Search" v-model="searchQuery" /> <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 overflow-auto">
<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">
<img
class="border-2 border-solid rounded max-w-full"
:src="`${config.server_endpoint}/textures/map_objects/${mapObject.id}.png`"
alt="Object"
@click="mapEditor.setSelectedMapObject(mapObject)"
:class="{
'cursor-pointer transition-all duration-300': true,
'border-cyan shadow-lg': mapEditor.selectedMapObject.value?.id === mapObject.id,
'border-transparent hover:border-gray-300': mapEditor.selectedMapObject.value?.id !== mapObject.id
}"
/>
</div> </div>
</div> </div>
</div> </div>
<div class="flex flex-col h-full p-4"> </div>
<div class="mb-4 flex flex-wrap gap-2"> <div class="flex flex-col h-40 gap-2.5 p-3.5 border-t border-0 border-solid border-gray-500">
<button v-for="tag in uniqueTags" :key="tag" @click="toggleTag(tag)" class="btn-cyan" :class="{ 'opacity-50': !selectedTags.includes(tag) }"> <span>Tags:</span>
{{ tag }} <div class="flex grow items-center flex-wrap gap-1.5 overflow-auto">
</button> <span class="m-auto">No tags selected</span>
</div>
<div class="h-full overflow-auto">
<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">
<img
class="border-2 border-solid max-w-full"
:src="`${config.server_endpoint}/textures/map_objects/${mapObject.id}.png`"
alt="Object"
@click="mapEditorStore.setSelectedMapObject(mapObject)"
:class="{
'cursor-pointer transition-all duration-300': true,
'border-cyan shadow-lg scale-105': mapEditorStore.selectedMapObject?.id === mapObject.id,
'border-transparent hover:border-gray-300': mapEditorStore.selectedMapObject?.id !== mapObject.id
}"
/>
</div>
</div>
</div>
</div> </div>
</template> </div>
</Modal> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import config from '@/application/config'
import type { MapObject } from '@/application/types' import type { MapObject } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { useGameStore } from '@/stores/gameStore' import { MapObjectStorage } from '@/storage/storages'
import { useMapEditorStore } from '@/stores/mapEditorStore' import { liveQuery } from 'dexie'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
const gameStore = useGameStore() const mapObjectStorage = new MapObjectStorage()
const isModalOpen = ref(false) const mapEditor = useMapEditorComposable()
const mapEditorStore = useMapEditorStore()
const searchQuery = ref('') const searchQuery = ref('')
const selectedTags = ref<string[]>([]) const selectedTags = ref<string[]>([])
const mapObjectList = ref<MapObject[]>([])
const uniqueTags = computed(() => { const uniqueTags = computed(() => {
const allTags = mapEditorStore.mapObjectList.flatMap((obj) => obj.tags || []) const allTags = mapObjectList.value.flatMap((obj) => obj.tags || [])
return Array.from(new Set(allTags)) return Array.from(new Set(allTags))
}) })
const filteredMapObjects = computed(() => {
return mapEditorStore.mapObjectList.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)
@ -75,10 +68,29 @@ const toggleTag = (tag: string) => {
} }
} }
onMounted(async () => { const filteredMapObjects = computed(() => {
isModalOpen.value = true return mapObjectList.value.filter((object) => {
gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => { const matchesSearch = !searchQuery.value || object.name.toLowerCase().includes(searchQuery.value.toLowerCase())
mapEditorStore.setMapObjectList(response) const matchesTags = selectedTags.value.length === 0 || (object.tags && selectedTags.value.some((tag) => object.tags.includes(tag)))
return matchesSearch && matchesTags
}) })
}) })
let subscription: any = null
onMounted(() => {
subscription = liveQuery(() => mapObjectStorage.liveQuery()).subscribe({
next: (result) => {
mapObjectList.value = result
},
error: (error) => {
console.error('Failed to fetch tiles:', error)
}
})
})
onUnmounted(() => {
if (!subscription) return
subscription.unsubscribe()
})
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<Modal :is-modal-open="mapEditorStore.isSettingsModalShown" @modal:close="() => mapEditorStore.toggleSettingsModal()" :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>
@ -46,61 +43,56 @@
</Modal> </Modal>
</template> </template>
<script setup> <script setup lang="ts">
import type { UUID } from '@/application/types'
import { uuidv4 } from '@/application/utilities'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useMapEditorStore } from '@/stores/mapEditorStore' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { ref, watch } from 'vue' import { onMounted, ref, useTemplateRef, watch } from 'vue'
const mapEditorStore = useMapEditorStore() const mapEditor = useMapEditorComposable()
const screen = ref('settings') const screen = ref('settings')
mapEditorStore.setMapName(mapEditorStore.map?.name) const name = ref<string | undefined>('Map')
mapEditorStore.setMapWidth(mapEditorStore.map?.width) const width = ref<number>(0)
mapEditorStore.setMapHeight(mapEditorStore.map?.height) const height = ref<number>(0)
mapEditorStore.setMapPvp(mapEditorStore.map?.pvp) const pvp = ref<boolean>(false)
mapEditorStore.setMapEffects(mapEditorStore.map?.mapEffects) const mapEffects = ref(mapEditor.currentMap.value?.mapEffects || [])
const modalRef = useTemplateRef('modalRef')
const name = ref(mapEditorStore.mapSettings?.name) defineExpose({
const width = ref(mapEditorStore.mapSettings?.width) open: () => modalRef.value?.open()
const height = ref(mapEditorStore.mapSettings?.height)
const pvp = ref(mapEditorStore.mapSettings?.pvp)
const mapEffects = ref(mapEditorStore.mapSettings?.mapEffects || [])
watch(name, (value) => {
mapEditorStore.setMapName(value)
}) })
watch(width, (value) => { function updateValue(event: Event) {
mapEditorStore.setMapWidth(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(height, (value) => { }
mapEditorStore.setMapHeight(value)
})
watch(pvp, (value) => {
mapEditorStore.setMapPvp(value)
})
watch( watch(
mapEffects, () => mapEditor.currentMap.value,
(value) => { (map) => {
mapEditorStore.setMapEffects(value) if (!map) return
}, name.value = map.name
{ deep: true } width.value = map.width
height.value = map.height
pvp.value = map.pvp
mapEffects.value = map.mapEffects
}
) )
const addEffect = () => { const addEffect = () => {
mapEffects.value.push({ mapEffects.value.push({
id: Date.now().toString(), // Simple unique id generation id: uuidv4(),
mapId: mapEditorStore.map?.id,
map: mapEditorStore.map,
effect: '', effect: '',
strength: 1 strength: 1
}) })
mapEditor.updateProperty('mapEffects', mapEffects.value)
} }
const removeEffect = (index) => { const removeEffect = (index: number) => {
mapEffects.value.splice(index, 1) mapEffects.value.splice(index, 1)
mapEditor.updateProperty('mapEffects', mapEffects.value)
} }
</script> </script>

View File

@ -1,35 +1,114 @@
<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 { 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
}>() }>()
console.log(props.placedMapObject)
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
gameStore.connection?.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 :is-modal-open="showTeleportModal" @modal:close="() => mapEditorStore.setTool('move')" :modal-width="300" :modal-height="350" :is-resizable="false" :bg-style="'none'"> <Modal ref="modalRef" @modal:close="() => mapEditorStore.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 mapEditorStore.mapList" :key="map.id" :value="map">{{ map.name }}</option> <option v-for="map in mapList" :key="map.id" :value="map">{{ map.name }}</option>
</select> </select>
</div> </div>
</div> </div>
@ -39,22 +39,29 @@
</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 { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore' import { useMapEditorStore } from '@/stores/mapEditorStore'
import { computed, onMounted, ref, watch } from 'vue' import { computed, onMounted, ref, useTemplateRef, watch } from 'vue'
const showTeleportModal = computed(() => mapEditorStore.tool === 'pencil' && mapEditorStore.drawMode === 'teleport') const showTeleportModal = computed(() => mapEditorStore.tool === 'pencil' && mapEditorStore.drawMode === 'teleport')
const mapEditorStore = useMapEditorStore() const mapEditorStore = useMapEditorStore()
const gameStore = useGameStore() const gameStore = useGameStore()
const mapList = ref<Map[]>([])
const modalRef = useTemplateRef('modalRef')
defineExpose({
open: () => modalRef.value?.open()
})
onMounted(fetchMaps) onMounted(fetchMaps)
function fetchMaps() { function fetchMaps() {
gameStore.connection?.emit('gm:map:list', {}, (response: Map[]) => { // gameStore.connection?.emit(SocketEvent.GM_MAP_LIST, {}, (response: Map[]) => {
mapEditorStore.setMapList(response) // mapList.value = response
}) // })
} }
const { toPositionX, toPositionY, toRotation, toMap } = useRefTeleportSettings() const { toPositionX, toPositionY, toRotation, toMap } = useRefTeleportSettings()
@ -65,7 +72,7 @@ function useRefTeleportSettings() {
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.toMap) toMap: ref(settings.toMapId)
} }
} }
@ -76,7 +83,7 @@ function updateTeleportSettings() {
toPositionX: toPositionX.value, toPositionX: toPositionX.value,
toPositionY: toPositionY.value, toPositionY: toPositionY.value,
toRotation: toRotation.value, toRotation: toRotation.value,
toMap: toMap.value toMapId: toMap.value
}) })
} }
</script> </script>

View File

@ -1,60 +1,54 @@
<template> <template>
<Modal :isModalOpen="mapEditorStore.isTileListModalShown" :modal-width="645" :modal-height="600" @modal:close="() => (mapEditorStore.isTileListModalShown = false)" :bg-style="'none'"> <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'">
<template #modalHeader> <div class="flex flex-col gap-2.5 p-2.5">
<h3 class="text-lg text-white">Tiles</h3> <div class="relative flex">
</template> <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" />
<template #modalBody> <label class="mb-1.5 font-titles hidden" for="search">Search</label>
<div class="h-full overflow-auto" v-if="!selectedGroup"> <input @mousedown.stop class="!pl-7 input-field w-full" type="text" name="search" placeholder="Search" v-model="searchQuery" />
<div class="flex pt-4 pl-4"> </div>
<div class="w-full flex gap-1.5 flex-row"> <div class="flex">
<div> <select class="input-field w-full" name="lists" v-model="mapEditor.drawMode.value" @change="(event: any) => mapEditor.setDrawMode(event.target.value)">
<label class="mb-1.5 font-titles hidden" for="search">Search...</label> <option value="tile">Tiles</option>
<input @mousedown.stop class="input-field" type="text" name="search" placeholder="Search" v-model="searchQuery" /> <option value="map_object">Objects</option>
</div> </select>
</div> </div>
</div> </div>
<div class="flex flex-col h-full p-4"> <div class="h-full overflow-auto relative border-0 border-t border-solid border-gray-500 p-2.5">
<div class="mb-4 flex flex-wrap gap-2"> <div class="h-full" v-if="!selectedGroup">
<button v-for="tag in uniqueTags" :key="tag" @click="toggleTag(tag)" class="btn-cyan" :class="{ 'opacity-50': !selectedTags.includes(tag) }"> <div class="grid grid-cols-4 gap-2 justify-items-center">
{{ tag }} <div v-for="group in groupedTiles" :key="group.parent.id" class="flex flex-col items-center justify-center relative">
</button> <img
</div> class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300"
<div class="h-[calc(100%_-_60px)] flex-grow overflow-y-auto"> :src="`${config.server_endpoint}/textures/tiles/${group.parent.id}.png`"
<div class="grid grid-cols-8 gap-2 justify-items-center"> :alt="group.parent.name"
<div v-for="group in groupedTiles" :key="group.parent.id" class="flex flex-col items-center justify-center relative"> @click="openGroup(group)"
<img @load="() => tileProcessor.processTile(group.parent)"
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300" :class="{
:src="`${config.server_endpoint}/textures/tiles/${group.parent.id}.png`" 'border-cyan shadow-lg': isActiveTile(group.parent),
:alt="group.parent.name" 'border-transparent hover:border-gray-300': !isActiveTile(group.parent)
@click="openGroup(group)" }"
@load="() => processTile(group.parent)" />
:class="{ <span class="text-xs mt-1">{{ getTileCategory(group.parent) }}</span>
'border-cyan shadow-lg scale-105': isActiveTile(group.parent), <span v-if="group.children.length > 0" class="absolute top-0 right-0 bg-cyan text-white rounded-full w-5 h-5 flex items-center justify-center text-xs">
'border-transparent hover:border-gray-300': !isActiveTile(group.parent) {{ group.children.length + 1 }}
}" </span>
/>
<span class="text-xs mt-1">{{ getTileCategory(group.parent) }}</span>
<span v-if="group.children.length > 0" class="absolute top-0 right-0 bg-cyan text-white rounded-full w-5 h-5 flex items-center justify-center text-xs">
{{ group.children.length + 1 }}
</span>
</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 class="grid grid-cols-8 gap-2 justify-items-center"> </div>
<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
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300" class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300"
:src="`${config.server_endpoint}/textures/tiles/${selectedGroup.parent.id}.png`" :src="`${config.server_endpoint}/textures/tiles/${selectedGroup.parent.id}.png`"
:alt="selectedGroup.parent.name" :alt="selectedGroup.parent.name"
@click="selectTile(selectedGroup.parent.id)" @click="selectTile(selectedGroup.parent.id)"
:class="{ :class="{
'border-cyan shadow-lg scale-105': isActiveTile(selectedGroup.parent), 'border-cyan shadow-lg': isActiveTile(selectedGroup.parent),
'border-transparent hover:border-gray-300': !isActiveTile(selectedGroup.parent) 'border-transparent hover:border-gray-300': !isActiveTile(selectedGroup.parent)
}" }"
/> />
@ -62,12 +56,12 @@
</div> </div>
<div v-for="childTile in selectedGroup.children" :key="childTile.id" class="flex flex-col items-center justify-center"> <div v-for="childTile in selectedGroup.children" :key="childTile.id" class="flex flex-col items-center justify-center">
<img <img
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300" class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300"
:src="`${config.server_endpoint}/textures/tiles/${childTile.id}.png`" :src="`${config.server_endpoint}/textures/tiles/${childTile.id}.png`"
:alt="childTile.name" :alt="childTile.name"
@click="selectTile(childTile.id)" @click="selectTile(childTile.id)"
:class="{ :class="{
'border-cyan shadow-lg scale-105': isActiveTile(childTile), 'border-cyan shadow-lg': isActiveTile(childTile),
'border-transparent hover:border-gray-300': !isActiveTile(childTile) 'border-transparent hover:border-gray-300': !isActiveTile(childTile)
}" }"
/> />
@ -76,41 +70,48 @@
</div> </div>
</div> </div>
</div> </div>
</template> </div>
</Modal> <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>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import config from '@/application/config'
import type { Tile } from '@/application/types' import type { Tile } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue' import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { useGameStore } from '@/stores/gameStore' import { useTileProcessingComposable } from '@/composables/useTileProcessingComposable'
import { useMapEditorStore } from '@/stores/mapEditorStore' import { TileStorage } from '@/storage/storages'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
const gameStore = useGameStore() const tileStorage = new TileStorage()
const isModalOpen = ref(false) const mapEditor = useMapEditorComposable()
const mapEditorStore = useMapEditorStore() const tileProcessor = useTileProcessingComposable()
const searchQuery = ref('') const searchQuery = ref('')
const selectedTags = ref<string[]>([]) const selectedTags = ref<string[]>([])
const tileCategories = ref<Map<string, string>>(new Map()) 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 uniqueTags = computed(() => { const uniqueTags = computed(() => {
const allTags = mapEditorStore.tileList.flatMap((tile) => tile.tags || []) const allTags = tiles.value.flatMap((tile) => tile.tags || [])
return Array.from(new Set(allTags)) return Array.from(new Set(allTags))
}) })
const groupedTiles = computed(() => { const groupedTiles = computed(() => {
const groups: { parent: Tile; children: Tile[] }[] = [] const groups: { parent: Tile; children: Tile[] }[] = []
const filteredTiles = mapEditorStore.tileList.filter((tile) => { const filteredTiles = tiles.value.filter((tile) => {
const matchesSearch = !searchQuery.value || tile.name.toLowerCase().includes(searchQuery.value.toLowerCase()) const matchesSearch = !searchQuery.value || tile.name.toLowerCase().includes(searchQuery.value.toLowerCase())
const matchesTags = selectedTags.value.length === 0 || (tile.tags && selectedTags.value.some((tag) => tile.tags.includes(tag))) const matchesTags = selectedTags.value.length === 0 || (tile.tags && selectedTags.value.some((tag) => tile.tags.includes(tag)))
return matchesSearch && matchesTags return matchesSearch && matchesTags
}) })
filteredTiles.forEach((tile) => { filteredTiles.forEach((tile) => {
const parentGroup = groups.find((group) => areTilesRelated(group.parent, tile)) const parentGroup = groups.find((group) => tileProcessor.areTilesRelated(group.parent, tile))
if (parentGroup && parentGroup.parent.id !== tile.id) { if (parentGroup && parentGroup.parent.id !== tile.id) {
parentGroup.children.push(tile) parentGroup.children.push(tile)
} else { } else {
@ -121,32 +122,6 @@ const groupedTiles = computed(() => {
return groups return groups
}) })
const tileColorData = ref<Map<string, { r: number; g: number; b: number }>>(new Map())
const tileEdgeData = ref<Map<string, number>>(new Map())
function areTilesRelated(tile1: Tile, tile2: Tile): boolean {
const colorSimilarityThreshold = 30 // Adjust this value as needed
const edgeComplexitySimilarityThreshold = 20 // Adjust this value as needed
const color1 = tileColorData.value.get(tile1.id)
const color2 = tileColorData.value.get(tile2.id)
const edge1 = tileEdgeData.value.get(tile1.id)
const edge2 = tileEdgeData.value.get(tile2.id)
if (!color1 || !color2 || edge1 === undefined || edge2 === undefined) {
return false
}
const colorDifference = Math.sqrt(Math.pow(color1.r - color2.r, 2) + Math.pow(color1.g - color2.g, 2) + Math.pow(color1.b - color2.b, 2))
const edgeComplexityDifference = Math.abs(edge1 - edge2)
const namePrefix1 = tile1.name.split('_')[0]
const namePrefix2 = tile2.name.split('_')[0]
return colorDifference <= colorSimilarityThreshold && edgeComplexityDifference <= edgeComplexitySimilarityThreshold && namePrefix1 === namePrefix2
}
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)
@ -155,57 +130,6 @@ const toggleTag = (tag: string) => {
} }
} }
function processTile(tile: Tile) {
const img = new Image()
img.crossOrigin = 'Anonymous'
img.onload = () => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = img.width
canvas.height = img.height
ctx!.drawImage(img, 0, 0, img.width, img.height)
const imageData = ctx!.getImageData(0, 0, canvas.width, canvas.height)
tileColorData.value.set(tile.id, getDominantColor(imageData))
tileEdgeData.value.set(tile.id, getEdgeComplexity(imageData))
}
img.src = `${config.server_endpoint}/textures/tiles/${tile.id}.png`
}
function getDominantColor(imageData: ImageData) {
let r = 0,
g = 0,
b = 0,
total = 0
for (let i = 0; i < imageData.data.length; i += 4) {
if (imageData.data[i + 3] > 0) {
// Only consider non-transparent pixels
r += imageData.data[i]
g += imageData.data[i + 1]
b += imageData.data[i + 2]
total++
}
}
return {
r: Math.round(r / total),
g: Math.round(g / total),
b: Math.round(b / total)
}
}
function getEdgeComplexity(imageData: ImageData) {
let edgePixels = 0
for (let y = 0; y < imageData.height; y++) {
for (let x = 0; x < imageData.width; x++) {
const i = (y * imageData.width + x) * 4
if (imageData.data[i + 3] > 0 && (x === 0 || y === 0 || x === imageData.width - 1 || y === imageData.height - 1 || imageData.data[i - 1] === 0 || imageData.data[i + 7] === 0)) {
edgePixels++
}
}
}
return edgePixels
}
function getTileCategory(tile: Tile): string { function getTileCategory(tile: Tile): string {
return tileCategories.value.get(tile.id) || '' return tileCategories.value.get(tile.id) || ''
} }
@ -219,18 +143,26 @@ function closeGroup() {
} }
function selectTile(tile: string) { function selectTile(tile: string) {
mapEditorStore.setSelectedTile(tile) mapEditor.setSelectedTile(tile)
} }
function isActiveTile(tile: Tile): boolean { function isActiveTile(tile: Tile): boolean {
return mapEditorStore.selectedTile === tile.id return mapEditor.selectedTile.value === tile.id
} }
onMounted(async () => { onMounted(async () => {
isModalOpen.value = true tiles.value = await tileStorage.getAll()
gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => { const initialBatchSize = 20
mapEditorStore.setTileList(response) const initialTiles = tiles.value.slice(0, initialBatchSize)
response.forEach((tile) => processTile(tile)) initialTiles.forEach((tile) => tileProcessor.processTile(tile))
})
// Process remaining tiles in background
setTimeout(() => {
tiles.value.slice(initialBatchSize).forEach((tile) => tileProcessor.processTile(tile))
}, 1000)
})
onUnmounted(() => {
tileProcessor.cleanup()
}) })
</script> </script>

View File

@ -1,160 +1,188 @@
<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"> <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="mapEditorStore.map"> <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': mapEditorStore.tool === 'move' }" @click="handleClick('move')"> <button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': 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': mapEditorStore.tool !== '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>
</button> </button>
<div class="w-px bg-cyan"></div> <div class="w-px bg-cyan"></div>
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditorStore.tool === 'pencil' }" @click="handleClick('pencil')"> <button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditor.tool.value === 'pencil' }" @click="handleClick('pencil')">
<img class="invert w-5 h-5" src="/assets/icons/mapEditor/pencil.svg" alt="Pencil" /> <span class="h-5" :class="{ 'ml-2.5': mapEditorStore.tool !== 'pencil' }">(P)</span> <img class="invert w-5 h-5" src="/assets/icons/mapEditor/pencil.svg" alt="Pencil" /> <span class="h-5" :class="{ 'ml-2.5': mapEditor.tool.value !== 'pencil' }">(P)</span>
<div class="select" v-if="mapEditorStore.tool === 'pencil'"> <div class="select" v-if="mapEditor.tool.value === 'pencil'">
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectPencilOpen }"> <div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectPencilOpen }">
{{ mapEditorStore.drawMode.replace('_', ' ') }} {{ mapEditor.drawMode.value.replace('_', ' ') }}
<img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/mapEditor/chevron.svg" alt="" /> <img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/mapEditor/chevron.svg" alt="" />
</div> </div>
<div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectPencilOpen && mapEditorStore.tool === 'pencil'"> <div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectPencilOpen && mapEditor.tool.value === 'pencil'">
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('tile')"> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('tile', 'pencil')">
Tile Tile
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div> <div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span> </span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('map_object')"> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('map_object', 'pencil')">
Map object Map object
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div> <div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span> </span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('teleport')"> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('teleport', 'pencil')">
Teleport Teleport
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div> <div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span> </span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('blocking tile')">Blocking tile</span> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('blocking tile', 'pencil')">Blocking tile</span>
</div> </div>
</div> </div>
</button> </button>
<div class="w-px bg-cyan"></div> <div class="w-px bg-cyan"></div>
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditorStore.tool === 'eraser' }" @click="handleClick('eraser')"> <button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditor.tool.value === 'eraser' }" @click="handleClick('eraser')">
<img class="invert w-5 h-5" src="/assets/icons/mapEditor/eraser.svg" alt="Eraser" /> <span class="h-5" :class="{ 'ml-2.5': mapEditorStore.tool !== 'eraser' }">(E)</span> <img class="invert w-5 h-5" src="/assets/icons/mapEditor/eraser.svg" alt="Eraser" /> <span class="h-5" :class="{ 'ml-2.5': mapEditor.tool.value !== 'eraser' }">(E)</span>
<div class="select" v-if="mapEditorStore.tool === 'eraser'"> <div class="select" v-if="mapEditor.tool.value === 'eraser'">
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectEraserOpen }"> <div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectEraserOpen }">
{{ mapEditorStore.eraserMode.replace('_', ' ') }} {{ mapEditor.drawMode.value.replace('_', ' ') }}
<img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/mapEditor/chevron.svg" /> <img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/mapEditor/chevron.svg" alt="" />
</div> </div>
<div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectEraserOpen"> <div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectEraserOpen">
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('tile')"> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('tile', 'eraser')">
Tile Tile
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div> <div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span> </span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('map_object')"> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('map_object', 'eraser')">
Map object Map object
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div> <div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span> </span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('teleport')"> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('teleport', 'eraser')">
Teleport Teleport
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div> <div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span> </span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('blocking tile')">Blocking tile</span> <span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('blocking tile', 'eraser')">Blocking tile</span>
</div> </div>
</div> </div>
</button> </button>
<div class="w-px bg-cyan"></div> <div class="w-px bg-cyan"></div>
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditorStore.tool === 'paint' }" @click="handleClick('paint')"> <button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditor.tool.value === 'paint' }" @click="handleClick('paint')">
<img class="invert w-5 h-5" src="/assets/icons/mapEditor/paint.svg" alt="Paint bucket" /> <span class="h-5" :class="{ 'ml-2.5': mapEditorStore.tool !== 'paint' }">(B)</span> <img class="invert w-5 h-5" src="/assets/icons/mapEditor/paint.svg" alt="Paint bucket" /> <span class="h-5" :class="{ 'ml-2.5': mapEditor.tool.value !== 'paint' }">(B)</span>
</button> </button>
<div class="w-px bg-cyan"></div> <div class="w-px bg-cyan"></div>
<button class="flex justify-center items-center min-w-10 p-0 relative" @click="handleClick('settings')" v-if="mapEditorStore.map"><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>
<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="w-px bg-cyan"></div>
<button class="btn-cyan px-3.5" @click="() => mapEditorStore.toggleMapListModal()">Load</button>
<button class="btn-cyan px-3.5" @click="() => emit('save')" v-if="mapEditorStore.map">Save</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/settings.svg" alt="Map settings" /> <span class="h-5 ml-2.5">(S)</span></button>
<button class="btn-cyan px-3.5" @click="() => emit('clear')" v-if="mapEditorStore.map">Clear</button>
<button class="btn-cyan px-3.5" @click="() => mapEditorStore.toggleActive()">Exit</button>
</div> </div>
</div> </div>
<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('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="() => mapEditor.toggleActive()">Exit</button>
</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 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 { useMapEditorStore } from '@/stores/mapEditorStore' import Modal from '@/components/utilities/Modal.vue'
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 mapEditorStore = useMapEditorStore() const mapEditor = useMapEditorComposable()
const emit = defineEmits(['save', 'clear']) 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)
const listOpen = computed(() => mapEditor.tool.value === 'pencil' && (mapEditor.drawMode.value === 'tile' || mapEditor.drawMode.value === 'map_object'))
// drawMode // drawMode
function setDrawMode(value: string) { function setDrawMode(value: string) {
mapEditorStore.isTileListModalShown = value === 'tile' mapEditor.setDrawMode(value)
mapEditorStore.isMapObjectListModalShown = value === 'map_object' selectPencilOpen.value = false
selectEraserOpen.value = false
}
mapEditorStore.setDrawMode(value) function setPencilMode() {
mapEditor.setTool('pencil')
selectPencilOpen.value = false selectPencilOpen.value = false
} }
// drawMode // drawMode
function setEraserMode(value: string) { function setEraserMode() {
mapEditorStore.setEraserMode(value) mapEditor.setTool('eraser')
selectEraserOpen.value = false selectEraserOpen.value = false
} }
function handleClick(tool: string) { function toggleContinuousDrawing() {
if (tool === 'settings') { mapEditor.setInputMode(isContinuousDrawingEnabled.value ? 'hold' : 'tap')
mapEditorStore.toggleSettingsModal() }
} else {
mapEditorStore.setTool(tool)
}
function handleModeClick(mode: string, type: 'pencil' | 'eraser') {
setDrawMode(mode)
type === 'pencil' ? setPencilMode() : setEraserMode()
}
function handleClick(tool: string) {
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') {
const modes = ['tile', 'object', 'teleport', 'blocking tile'] const modes = ['tile', 'map_object', 'teleport', 'blocking tile']
const currentMode = tool === 'pencil' ? mapEditorStore.drawMode : mapEditorStore.eraserMode const currentIndex = modes.indexOf(mapEditor.drawMode.value)
const currentIndex = modes.indexOf(currentMode)
const nextIndex = (currentIndex + 1) % modes.length const nextIndex = (currentIndex + 1) % modes.length
const nextMode = modes[nextIndex] const nextMode = modes[nextIndex]
if (tool === 'pencil') { setDrawMode(nextMode)
setDrawMode(nextMode)
} else {
setEraserMode(nextMode)
}
} }
function initKeyShortcuts(event: KeyboardEvent) { function initKeyShortcuts(event: KeyboardEvent) {
// Check if map is set // Check if map is set
if (!mapEditorStore.map) 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)) {
const tool = keyActions[event.key] const tool = keyActions[event.key]
if ((tool === 'pencil' || tool === 'eraser') && mapEditorStore.tool === tool) { if ((tool === 'pencil' || tool === 'eraser') && mapEditor.tool.value === tool) {
cycleToolMode(tool) cycleToolMode(tool)
} else { } else {
handleClick(tool) handleClick(tool)

View File

@ -1,21 +0,0 @@
<template>
<div class="absolute top-0 right-4 hidden lg:block">
<p class="text-white text-lg">{{ gameStore.world.date.toLocaleString() }}</p>
</div>
</template>
<script setup lang="ts">
import { useGameStore } from '@/stores/gameStore'
import { onUnmounted } from 'vue'
const gameStore = useGameStore()
// Listen for new date from socket
gameStore.connection?.on('date', (data: Date) => {
gameStore.world.date = new Date(data)
})
onUnmounted(() => {
gameStore.connection?.off('date')
})
</script>

View File

@ -1,42 +0,0 @@
<template>
<div class="absolute z-50 w-full h-dvh top-0 left-0 bg-black/60" v-show="false">
<div class="center-element max-w-[875px] max-h-[600px] h-full w-[80%] bg-gray border-solid border-2 border-gray-500 rounded-md z-50 flex flex-col backdrop-blur-sm shadow-lg">
<div class="p-2.5 flex max-sm:flex-wrap justify-between items-center gap-5 border-solid border-0 border-b border-gray-500">
<h3 class="m-0 font-medium shrink-0">Game menu</h3>
<div class="hidden sm:flex gap-1.5 flex-wrap">
<button @click.stop="userPanelScreen = 'inventory'" :class="{ active: userPanelScreen === 'inventory' }" class="btn-cyan py-1.5 px-4 min-w-24">Inventory</button>
<button @click.stop="userPanelScreen = 'equipment'" :class="{ active: userPanelScreen === 'equipment' }" class="btn-cyan py-1.5 px-4 min-w-24">Equipment</button>
<button @click.stop="userPanelScreen = 'characterScreen'" :class="{ active: userPanelScreen === 'characterScreen' }" class="btn-cyan py-1.5 px-4 min-w-24">Character</button>
<button @click.stop="userPanelScreen = 'settings'" :class="{ active: userPanelScreen === 'settings' }" class="btn-cyan py-1.5 px-4 min-w-24">Settings</button>
</div>
<div class="flex gap-2.5">
<button class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
<img alt="close" draggable="false" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" />
</button>
</div>
<div class="flex sm:hidden gap-1.5 flex-wrap">
<button @click.stop="userPanelScreen = 'inventory'" :class="{ active: userPanelScreen === 'inventory' }" class="btn-cyan py-1.5 px-4 min-w-24">Inventory</button>
<button @click.stop="userPanelScreen = 'equipment'" :class="{ active: userPanelScreen === 'equipment' }" class="btn-cyan py-1.5 px-4 min-w-24">Equipment</button>
<button @click.stop="userPanelScreen = 'characterScreen'" :class="{ active: userPanelScreen === 'characterScreen' }" class="btn-cyan py-1.5 px-4 min-w-24">Character</button>
<button @click.stop="userPanelScreen = 'settings'" :class="{ active: userPanelScreen === 'settings' }" class="btn-cyan py-1.5 px-4 min-w-24">Settings</button>
</div>
</div>
<Inventory v-show="userPanelScreen === 'inventory'" />
<Equipment v-show="userPanelScreen === 'equipment'" />
<CharacterScreen v-show="userPanelScreen === 'characterScreen'" />
<Settings v-show="userPanelScreen === 'settings'" />
</div>
</div>
</template>
<script setup lang="ts">
import CharacterScreen from '@/components/gui/partials/CharacterScreen.vue'
import Equipment from '@/components/gui/partials/Equipment.vue'
import Inventory from '@/components/gui/partials/Inventory.vue'
import Settings from '@/components/gui/partials/Settings.vue'
import { useGameStore } from '@/stores/gameStore'
import { ref } from 'vue'
const gameStore = useGameStore()
let userPanelScreen = ref('inventory')
</script>

View File

@ -1,68 +0,0 @@
<template>
<div class="grow flex flex-col w-full h-full relative overflow-auto">
<div class="m-4 relative">
<h4 class="font-medium text-lg max-w-[375px]">Character</h4>
<div class="flex justify-center flex-wrap gap-20">
<div class="flex gap-3 mt-2 flex-wrap max-w-[375px]">
<div class="h-full flex flex-col justify-center items-center">
<img class="h-72 my-2 mx-auto" src="/assets/placeholders/inventory_player.png" />
</div>
</div>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-3 mx-5 mt-2">
<h3>{{ gameStore.character?.name }}</h3>
<div class="flex gap-4 flex-wrap max-w-[375px]">
<ul class="p-0 m-0 list-none">
<li class="leading-6 text-lg">Class:</li>
<li class="leading-6 text-lg">Race:</li>
<li class="leading-6 text-lg">Hit Points:</li>
<li class="leading-6 text-lg">Mana Points:</li>
<li class="leading-6 text-lg">Level:</li>
<li class="leading-6 text-lg">Stat Points:</li>
</ul>
<ul class="p-0 m-0 list-none">
<li class="leading-6 text-lg min-h-6">Knight</li>
<li class="leading-6 text-lg min-h-6">{{ gameStore.character?.characterType?.race }}</li>
<li class="leading-6 text-lg min-h-6">{{ gameStore.character?.hitpoints }} <span class="text-green">(+15)</span></li>
<li class="leading-6 text-lg min-h-6">{{ gameStore.character?.mana }}</li>
<li class="leading-6 text-lg min-h-6">{{ gameStore.character?.level }}</li>
<li class="leading-6 text-lg min-h-6">3</li>
</ul>
</div>
</div>
<div class="flex flex-col gap-3 mx-5 mt-2">
<h3>Alignment</h3>
<div class="h-8 w-64 rounded border border-solid border-white bg-gradient-to-r from-red to-blue relative">
<!-- TODO: Give slider left value based on alignment (0-100), new characters start with 50 -->
<div class="rounded border-2 border-solid border-white h-10 w-2 absolute top-1/2 -translate-y-1/2 -translate-x-1/2" :style="{ left: gameStore.character?.alignment + '%' }"></div>
</div>
</div>
</div>
</div>
<div class="absolute -left-5 -bottom-5 w-[calc(100%_+_40px)] my-px h-px bg-gray-500"></div>
</div>
<div class="m-4">
<h4 class="font-medium text-lg max-w-[375px]">Character stats</h4>
<div class="flex gap-3 mt-4 flex-wrap max-w-[375px]">
<ul class="p-0 m-0 list-none">
<li class="leading-6">Health:</li>
<li class="leading-6">Defense:</li>
<li class="leading-6">More stats:</li>
<li class="leading-6">Extra stats:</li>
</ul>
<ul class="p-0 m-0 list-none text-right">
<li class="leading-6">100 <span class="text-green">(+15)</span></li>
<li class="leading-6">1000 <span class="text-green">(+500)</span></li>
<li class="leading-6"></li>
<li class="leading-6"></li>
</ul>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useGameStore } from '@/stores/gameStore'
const gameStore = useGameStore()
</script>

View File

@ -1,89 +0,0 @@
<template>
<div class="grow flex flex-col w-full h-full relative overflow-auto">
<div class="m-4 relative">
<h4 class="font-medium text-lg max-w-[375px]">Equipment</h4>
<div class="flex justify-center items-center flex-wrap gap-20">
<div class="flex gap-3 mt-2 flex-wrap max-w-[375px]">
<div class="h-full flex flex-col justify-center items-center">
<h3>{{ gameStore.character?.name }}</h3>
<img class="h-60 my-2 mx-auto" src="/assets/placeholders/inventory_player.png" />
<span class="block text-sm">Level {{ gameStore.character?.level }}</span>
</div>
</div>
<div class="flex flex-col gap-3 mx-5 mt-2">
<div class="flex gap-3 justify-center">
<!-- Helmet -->
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square h-11 w-11 relative hover:bg-gray-200">
<img src="/assets/icons/inventory/helmet.svg" class="center-element w-11/12 opacity-20" />
</div>
<!-- Head charm -->
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square h-11 w-11 relative hover:bg-gray-200">
<img src="/assets/icons/inventory/head_charm.svg" class="center-element w-11/12 opacity-20" />
</div>
</div>
<div class="flex gap-3 justify-center">
<!-- Bracers -->
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md w-11 h-[104px] relative hover:bg-gray-200">
<img src="/assets/icons/inventory/bracers.svg" class="center-element w-11/12 opacity-20" />
</div>
<!-- Chestplate -->
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square w-[104px] h-[104px] relative hover:bg-gray-200">
<img src="/assets/icons/inventory/chestplate.svg" class="center-element w-10/12 opacity-20" />
</div>
<!-- Primary Weapon -->
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md w-11 h-[104px] self-stretch justify-self-stretch relative hover:bg-gray-200">
<img src="/assets/icons/inventory/primary_weapon.svg" class="center-element w-11/12 opacity-20" />
</div>
</div>
<div class="flex gap-3 justify-center">
<!-- Legs -->
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md w-11 h-[104px] self-stretch justify-self-stretch relative hover:bg-gray-200">
<img src="/assets/icons/inventory/legs.svg" class="center-element w-11/12 opacity-20" />
</div>
<div class="flex flex-col gap-3">
<!-- Belt/pouch -->
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square h-11 w-11 self-stretch justify-self-stretch relative hover:bg-gray-200">
<img src="/assets/icons/inventory/pouch.svg" class="center-element w-11/12 opacity-20" />
</div>
<!-- Boots -->
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square h-11 w-11 self-stretch justify-self-stretch relative hover:bg-gray-200">
<img src="/assets/icons/inventory/boots.svg" class="center-element w-11/12 opacity-20" />
</div>
</div>
</div>
</div>
</div>
<div class="absolute -left-5 -bottom-5 w-[calc(100%_+_40px)] my-px h-px bg-gray-500"></div>
</div>
<div class="m-4">
<h4 class="font-medium text-lg max-w-[375px]">Equipment Bonus</h4>
<div class="flex gap-3 mt-4 flex-wrap max-w-[375px]">
<ul class="p-0 m-0 list-none">
<li class="leading-6">Health:</li>
<li class="leading-6">Defense:</li>
<li class="leading-6">More stats:</li>
<li class="leading-6">Extra stats:</li>
</ul>
<ul class="p-0 m-0 list-none text-right">
<li class="leading-6">+15</li>
<li class="leading-6">500</li>
<li class="leading-6"></li>
<li class="leading-6"></li>
</ul>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useGameStore } from '@/stores/gameStore'
const gameStore = useGameStore()
</script>

View File

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

View File

@ -1,40 +0,0 @@
<template>
<div class="flex h-full w-full relative">
<div class="w-2/12 flex flex-col relative">
<!-- Settings Categories -->
<div class="relative p-2.5">
<h3>Settings</h3>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</div>
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'character' }" @click.stop="settingCategory = 'character'">
<span>Character</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'account' }" @click.stop="settingCategory = 'account'">
<span>Account</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'audio' }" @click.stop="settingCategory = 'audio'">
<span>Audio</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'video' }" @click.stop="settingCategory = 'video'">
<span>Video</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
</div>
<div class="absolute w-px bg-gray-500 h-full top-0 left-1/6"></div>
<!-- Assets list -->
<div class="overflow-auto h-full w-10/12 flex flex-col relative">
<CharacterSettings v-show="settingCategory == 'character'" />
</div>
</div>
</template>
<script setup lang="ts">
import CharacterSettings from '@/components/gui/partials/settings/CharacterSettings.vue'
import { ref } from 'vue'
let settingCategory = ref('character')
</script>

View File

@ -1,34 +0,0 @@
<template>
<div class="h-full overflow-auto">
<div class="relative p-2.5 flex flex-col gap-5 h-72">
<h3>Character details</h3>
<button @click="toggle" class="btn-cyan px-4 py-1.5 w-24">Edit</button>
<form class="flex gap-2.5 flex-wrap">
<div class="form-field-half max-w-[300px]">
<label for="name">Name</label>
<input class="input-field" :class="{ inactive: !editCharacter }" type="text" name="name" placeholder="Ethereal" :disabled="!editCharacter" />
</div>
<div class="form-field-half max-w-[300px] relative">
<label for="class">Class</label>
<select class="input-field" v-model="characterClass" :class="{ inactive: !editCharacter }" name="class" :disabled="!editCharacter">
<option value="Knight" :selected="characterClass == 'Knight'" :disabled="characterClass == 'Knight'">Knight</option>
<option value="Paladin" :selected="characterClass == 'Paladin'" :disabled="characterClass == 'Paladin'">Paladin</option>
</select>
<span v-if="!editCharacter" class="absolute bottom-[9px] left-[14px] text-sm text-gray-300/50">{{ characterClass }}</span>
</div>
<button v-if="editCharacter" @click="toggle" class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const editCharacter = ref(false)
const characterClass = ref('')
const toggle = () => {
editCharacter.value = !editCharacter.value
}
</script>

View File

@ -26,7 +26,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { login } from '@/services/authentication' 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 +39,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 === '') {

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,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { login, register } from '@/services/authentication' 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 +40,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 === '') {

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>
@ -34,7 +34,7 @@
<button class="ml-6 w-4 h-8 p-0"> <button class="ml-6 w-4 h-8 p-0">
<img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 m-auto" alt="Arrow left" /> <img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 m-auto" alt="Arrow left" />
</button> </button>
<img class="w-24 object-contain mb-3.5" alt="Player avatar" :src="config.server_endpoint + '/avatar/s/' + characters.find((c) => c.id === selectedCharacterId)?.characterType + '/' + (selectedHairId ?? 'default')" /> <img class="w-24 object-contain mb-3.5 max-h-[70%]" alt="Player avatar" :src="config.server_endpoint + '/avatar/s/' + characters.find((c) => c.id === selectedCharacterId)?.characterType + '/' + (selectedHairId ?? 'default')" />
<button class="mr-6 w-4 h-8 p-0"> <button class="mr-6 w-4 h-8 p-0">
<img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 -scale-x-100" alt="Arrow right" /> <img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 -scale-x-100" alt="Arrow right" />
</button> </button>
@ -87,7 +87,7 @@
</div> </div>
</div> </div>
<div v-else> <div v-else>
<img class="w-20 invert-80" src="/assets/icons/loading-icon1.svg" /> <img class="w-20 invert-80" src="/assets/icons/loading-icon1.svg" alt="Loading" />
</div> </div>
<div class="w-2/3 button-wrapper flex self-center justify-center lg:justify-end gap-4 max-w-[860px]" v-if="!isLoading"> <div class="w-2/3 button-wrapper flex self-center justify-center lg:justify-end gap-4 max-w-[860px]" v-if="!isLoading">
@ -122,27 +122,31 @@
<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 { 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[]>([])
const selectedCharacterId = ref<number | null>(null) const selectedCharacterId = ref<string | null>(null)
const isCreateNewCharacterModalOpen = ref<boolean>(false) const isCreateNewCharacterModalOpen = ref<boolean>(false)
const newCharacterName = ref<string>('') const newCharacterName = ref<string>('')
const characterHairs = ref<CharacterHair[]>([]) const characterHairs = ref<CharacterHair[]>([])
const selectedHairId = ref<number | null>(null) const selectedHairId = ref<string | null>(null)
// Fetch characters // Fetch characters
setTimeout(() => { setTimeout(() => {
gameStore.connection?.emit('character:list') console.log(SocketEvent.CHARACTER_LIST)
gameStore.connection?.emit(SocketEvent.CHARACTER_LIST)
}, 750) }, 750)
gameStore.connection?.on('character:list', (data: any) => { gameStore.connection?.on(SocketEvent.CHARACTER_LIST, (data: any) => {
characters.value = data characters.value = data
isLoading.value = false isLoading.value = false
}) })
@ -152,7 +156,7 @@ function loginWithCharacter() {
if (!selectedCharacterId.value) return if (!selectedCharacterId.value) return
gameStore.connection?.emit( gameStore.connection?.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) => { gameStore.connection?.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') gameStore.connection?.off(SocketEvent.CHARACTER_LIST)
gameStore.connection?.off('character:connect') gameStore.connection?.off(SocketEvent.CHARACTER_CONNECT)
gameStore.connection?.off('character:create:success') gameStore.connection?.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 />
@ -11,7 +11,6 @@
<ExpBar /> <ExpBar />
<CharacterProfile /> <CharacterProfile />
<Effects />
</Scene> </Scene>
</Game> </Game>
</div> </div>
@ -20,22 +19,21 @@
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import config from '@/application/config'
import 'phaser' import 'phaser'
import Effects from '@/components/Effects.vue' import CharacterProfile from '@/components/game/gui/CharacterProfile.vue'
import Chat from '@/components/game/gui/Chat.vue'
import Clock from '@/components/game/gui/Clock.vue'
import ExpBar from '@/components/game/gui/ExpBar.vue'
import Hotkeys from '@/components/game/gui/Hotkeys.vue'
import Hud from '@/components/game/gui/Hud.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 CharacterProfile from '@/components/gui/CharacterProfile.vue' import { useSoundComposable } from '@/composables/useSoundComposable'
import Chat from '@/components/gui/Chat.vue'
// import Minimap from '@/components/gui/Minimap.vue'
import Clock from '@/components/gui/Clock.vue'
import ExpBar from '@/components/gui/ExpBar.vue'
import Hotkeys from '@/components/gui/Hotkeys.vue'
import Hud from '@/components/gui/Hud.vue'
import Menu from '@/components/gui/Menu.vue'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import AwaitLoaderPlugin from 'phaser3-rex-plugins/plugins/awaitloader-plugin'
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,
@ -43,21 +41,13 @@ const gameConfig = {
height: window.innerHeight, height: window.innerHeight,
type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS
resolution: 5, resolution: 5,
plugins: { input: {
global: [ windowEvents: false
{
key: 'rexAwaitLoader',
plugin: AwaitLoaderPlugin,
start: true
}
]
} }
} }
const createGame = (game: Phaser.Game) => { const createGame = (game: Phaser.Game) => {
/** // Resize the game when the window is resized
* Resize the game when the window is resized
*/
addEventListener('resize', () => { addEventListener('resize', () => {
game.scale.resize(window.innerWidth, window.innerHeight) game.scale.resize(window.innerWidth, window.innerHeight)
}) })
@ -70,17 +60,17 @@ const createGame = (game: Phaser.Game) => {
}) })
gameStore.disconnectSocket() gameStore.disconnectSocket()
} }
playSound('/assets/sounds/connect.wav')
} }
function preloadScene(scene: Phaser.Scene) { function preloadScene(scene: Phaser.Scene) {
/** // Load the base assets into the Phaser scene
* Load the base assets into the Phaser scene
*/
scene.load.image('blank_tile', '/assets/map/blank_tile.png') scene.load.image('blank_tile', '/assets/map/blank_tile.png')
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

@ -1,25 +1,12 @@
<template> <template>
<div class="flex flex-col justify-center items-center h-dvh relative col"> <div class="flex flex-col justify-center items-center h-dvh relative col">
<svg width="40" height="40" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <img class="w-20 invert-80" src="/assets/icons/loading-icon1.svg" alt="Loading" />
<circle cx="4" cy="12" r="3" fill="white">
<animate id="spinner_qFRN" begin="0;spinner_OcgL.end+0.25s" attributeName="cy" calcMode="spline" dur="0.6s" values="12;6;12" keySplines=".33,.66,.66,1;.33,0,.66,.33" />
</circle>
<circle cx="12" cy="12" r="3" fill="white">
<animate begin="spinner_qFRN.begin+0.1s" attributeName="cy" calcMode="spline" dur="0.6s" values="12;6;12" keySplines=".33,.66,.66,1;.33,0,.66,.33" />
</circle>
<circle cx="20" cy="12" r="3" fill="white">
<animate id="spinner_OcgL" begin="spinner_qFRN.begin+0.2s" attributeName="cy" calcMode="spline" dur="0.6s" values="12;6;12" keySplines=".33,.66,.66,1;.33,0,.66,.33" />
</circle>
</svg>
</div> </div>
</template> </template>
<script setup lang="ts" async> <script setup lang="ts" async>
import config from '@/application/config' import { downloadCache } from '@/application/utilities'
import type { HttpResponse, MapObject } from '@/application/types'
import type { BaseStorage } from '@/storage/baseStorage'
import { CharacterHairStorage, CharacterTypeStorage, MapObjectStorage, MapStorage, SpriteStorage, TileStorage } from '@/storage/storages' import { CharacterHairStorage, CharacterTypeStorage, MapObjectStorage, MapStorage, SpriteStorage, TileStorage } from '@/storage/storages'
// import type { Map } from '@/application/types'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { ref } from 'vue' import { ref } from 'vue'
@ -28,32 +15,13 @@ const gameStore = useGameStore()
const totalItems = ref(0) const totalItems = ref(0)
const currentItem = ref(0) const currentItem = ref(0)
async function downloadAndStore<T extends { id: string }>(endpoint: string, storage: BaseStorage<T>) {
const request = await fetch(`${config.server_endpoint}/cache/${endpoint}`)
const response = (await request.json()) as HttpResponse<T[]>
if (!response.success) {
console.error(`Failed to download ${endpoint}:`, response.message)
return
}
const items = response.data ?? []
for (const item of items) {
await storage.add(item)
}
}
const tileStorage = new TileStorage()
const mapStorage = new MapStorage()
const mapObjectStorage = new MapObjectStorage()
Promise.all([ Promise.all([
downloadAndStore('tiles', tileStorage), downloadCache('tiles', new TileStorage()),
downloadAndStore('maps', mapStorage), downloadCache('maps', new MapStorage()),
downloadAndStore('map_objects', mapObjectStorage), downloadCache('map_objects', new MapObjectStorage()),
downloadAndStore('sprites', new SpriteStorage()), downloadCache('sprites', new SpriteStorage()),
downloadAndStore('character_types', new CharacterTypeStorage()), downloadCache('character_types', new CharacterTypeStorage()),
downloadAndStore('character_hair', new CharacterHairStorage()) downloadCache('character_hair', new CharacterHairStorage())
]).then(() => { ]).then(() => {
gameStore.game.isLoaded = true gameStore.game.isLoaded = true
}) })

View File

@ -8,8 +8,8 @@
<div class="h-dvh flex items-center lg:justify-center flex-col px-8 max-lg:pt-20"> <div class="h-dvh flex items-center lg:justify-center flex-col px-8 max-lg:pt-20">
<!-- <img src="/assets/tlogo.png" class="mb-10 w-52" alt="Noxious logo" />--> <!-- <img src="/assets/tlogo.png" class="mb-10 w-52" alt="Noxious logo" />-->
<div class="relative"> <div class="relative">
<img src="/assets/ui-elements/login-ui-box-outer.svg" class="absolute w-full h-full" alt="UI box outer" /> <img src="/assets/ui-elements/login-ui-box-outer.svg" class="absolute w-full h-full" alt="" />
<img src="/assets/ui-elements/login-ui-box-inner.svg" class="absolute left-2 top-2 w-[calc(100%_-_16px)] h-[calc(100%_-_16px)] max-lg:hidden" alt="UI box inner" /> <img src="/assets/ui-elements/login-ui-box-inner.svg" class="absolute left-2 top-2 w-[calc(100%_-_16px)] h-[calc(100%_-_16px)]" alt="" />
<!-- Login Form --> <!-- Login Form -->
<LoginForm v-if="currentForm === 'login' && !doesUrlHaveToken" @openResetPasswordModal="() => (isPasswordResetFormShown = true)" @switchToRegister="currentForm = 'register'" /> <LoginForm v-if="currentForm === 'login' && !doesUrlHaveToken" @openResetPasswordModal="() => (isPasswordResetFormShown = true)" @switchToRegister="currentForm = 'register'" />
@ -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

@ -1,8 +1,17 @@
<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">
<MapEditor :key="JSON.stringify(`${mapEditorStore.map?.id}_${mapEditorStore.map?.createdAt}_${mapEditorStore.map?.updatedAt ?? ''}`)" /> <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>
<Map v-if="mapEditor.currentMap.value" :key="mapEditor.currentMap.value?.id" />
<Toolbar ref="toolbar" @save="save" @clear="clear" @open-maps="mapModal?.open" @open-settings="mapSettingsModal?.open" @open-teleport="teleportModal?.open" />
<MapList ref="mapModal" @open-create-map="mapSettingsModal?.open" />
<TileList />
<MapObjectList />
<MapSettings ref="mapSettingsModal" />
<TeleportModal ref="teleportModal" />
</div>
</Scene> </Scene>
</Game> </Game>
</div> </div>
@ -10,17 +19,32 @@
<script setup lang="ts"> <script setup lang="ts">
import config from '@/application/config' import config from '@/application/config'
import { SocketEvent } from '@/application/enums'
import 'phaser' import 'phaser'
import type { TextureData } from '@/application/types' import type { Map as MapT } from '@/application/types'
import MapEditor from '@/components/gameMaster/mapEditor/MapEditor.vue' import Map from '@/components/gameMaster/mapEditor/Map.vue'
import { loadTexture } from '@/composables/gameComposable' import MapList from '@/components/gameMaster/mapEditor/partials/MapList.vue'
import MapObjectList from '@/components/gameMaster/mapEditor/partials/MapObjectList.vue'
import MapSettings from '@/components/gameMaster/mapEditor/partials/MapSettings.vue'
import TeleportModal from '@/components/gameMaster/mapEditor/partials/TeleportModal.vue'
import TileList from '@/components/gameMaster/mapEditor/partials/TileList.vue'
import Toolbar from '@/components/gameMaster/mapEditor/partials/Toolbar.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { loadAllTileTextures } from '@/services/mapService'
import { MapStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import AwaitLoaderPlugin from 'phaser3-rex-plugins/plugins/awaitloader-plugin'
import { Game, Scene } from 'phavuer' import { Game, Scene } from 'phavuer'
import { ref, useTemplateRef } from 'vue'
const mapStorage = new MapStorage()
const mapEditor = useMapEditorComposable()
const gameStore = useGameStore() const gameStore = useGameStore()
const mapEditorStore = useMapEditorStore()
const mapModal = useTemplateRef('mapModal')
const mapSettingsModal = useTemplateRef('mapSettingsModal')
const teleportModal = useTemplateRef('teleportModal')
const isLoaded = ref(false)
const gameConfig = { const gameConfig = {
name: config.name, name: config.name,
@ -28,59 +52,55 @@ const gameConfig = {
height: window.innerHeight, height: window.innerHeight,
type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS
resolution: 5, resolution: 5,
plugins: { input: {
global: [ windowEvents: false
{
key: 'rexAwaitLoader',
plugin: AwaitLoaderPlugin,
start: true
}
]
} }
} }
const createGame = (game: Phaser.Game) => { const createGame = (game: Phaser.Game) => {
/** // Resize the game when the window is resized
* Resize the game when the window is resized window.addEventListener('resize', () => {
*/
addEventListener('resize', () => {
game.scale.resize(window.innerWidth, window.innerHeight) game.scale.resize(window.innerWidth, window.innerHeight)
}) })
// We don't support canvas mode, only WebGL
if (game.renderer.type === Phaser.CANVAS) {
gameStore.addNotification({
title: 'Warning',
message: 'Your browser does not support WebGL. Please use a modern browser like Chrome, Firefox, or Edge.'
})
gameStore.disconnectSocket()
}
} }
const preloadScene = async (scene: Phaser.Scene) => { const preloadScene = async (scene: Phaser.Scene) => {
/** // Load the base assets into the Phaser scene
* Load the base assets into the Phaser scene
*/
scene.load.image('BLOCK', '/assets/map/bt_tile.png') scene.load.image('BLOCK', '/assets/map/bt_tile.png')
scene.load.image('TELEPORT', '/assets/map/tp_tile.png') scene.load.image('TELEPORT', '/assets/map/tp_tile.png')
scene.load.image('blank_tile', '/assets/map/blank_tile.png') scene.load.image('blank_tile', '/assets/map/blank_tile.png')
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
* Because Phaser can't load tiles after the scene with map in it is created, await loadAllTileTextures(scene)
* we need to load and cache all the tiles first.
* Then load them into the scene.
*/
scene.load.rexAwait(async function (successCallback: any) {
const tiles: { data: TextureData[] } = await fetch(config.server_endpoint + '/assets/list_tiles').then((response) => response.json())
for await (const tile of tiles?.data ?? []) { // Wait for all assets to be loaded before continuing
await loadTexture(scene, tile) await new Promise<void>((resolve) => {
} scene.load.on(Phaser.Loader.Events.COMPLETE, () => {
resolve()
successCallback() })
isLoaded.value = true
}) })
} }
const createScene = async (scene: Phaser.Scene) => {} function save() {
const currentMap = mapEditor.currentMap.value
if (!currentMap) return
const data = {
...currentMap,
mapId: currentMap.id
}
gameStore.connection?.emit(SocketEvent.GM_MAP_UPDATE, data, (response: MapT) => {
mapStorage.update(response.id, response)
})
}
function clear() {
if (!mapEditor.currentMap.value) return
// Clear placed objects, event tiles and tiles
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

@ -3,8 +3,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useCameraControls } from '@/composables/useCameraControls' import { useControlsComposable } from '@/composables/useControlsComposable'
import { usePointerHandlers } from '@/composables/usePointerHandlers'
import { Image, useScene } from 'phavuer' import { Image, useScene } from 'phavuer'
import { onBeforeUnmount, ref } from 'vue' import { onBeforeUnmount, ref } from 'vue'
@ -14,45 +13,16 @@ type WayPoint = { visible: boolean; x: number; y: number }
// Props // Props
const props = defineProps<{ layer: Phaser.Tilemaps.TilemapLayer }>() const props = defineProps<{ layer: Phaser.Tilemaps.TilemapLayer }>()
// Constants
const ZOOM_SETTINGS = {
WHEEL_FACTOR: 0.005,
KEY_FACTOR: 0.3,
MIN: 1,
MAX: 3
} as const
// Setup // Setup
const scene = useScene() const scene = useScene()
const waypoint = ref<WayPoint>({ visible: false, x: 0, y: 0 }) const waypoint = ref<WayPoint>({ visible: false, x: 0, y: 0 })
const { camera } = useCameraControls(scene) const { setupControls, cleanupControls } = useControlsComposable(scene, props.layer, waypoint)
const { setupPointerHandlers, cleanupPointerHandlers } = usePointerHandlers(scene, props.layer, waypoint, camera)
// Handlers
function handleScrollZoom(pointer: Phaser.Input.Pointer) {
if (!(pointer.event instanceof WheelEvent && pointer.event.shiftKey)) return
const zoomLevel = Phaser.Math.Clamp(camera.zoom - pointer.event.deltaY * ZOOM_SETTINGS.WHEEL_FACTOR, ZOOM_SETTINGS.MIN, ZOOM_SETTINGS.MAX)
camera.setZoom(zoomLevel)
}
function handleKeyComboZoom(event: { keyCodes: number[] }) {
const deltaY = event.keyCodes[1] === 38 ? 1 : -1 // 38 is Up, 40 is Down
const zoomLevel = Phaser.Math.Clamp(camera.zoom + deltaY * ZOOM_SETTINGS.KEY_FACTOR, ZOOM_SETTINGS.MIN, ZOOM_SETTINGS.MAX)
camera.setZoom(zoomLevel)
}
// Event setup // Event setup
setupPointerHandlers() setupControls()
scene.input.keyboard?.createCombo([16, 38], { resetOnMatch: true }) // Shift + Up
scene.input.keyboard?.createCombo([16, 40], { resetOnMatch: true }) // Shift + Down
scene.input.keyboard?.on('keycombomatch', handleKeyComboZoom)
scene.input.on(Phaser.Input.Events.POINTER_WHEEL, handleScrollZoom)
// Cleanup // Cleanup
onBeforeUnmount(() => { onBeforeUnmount(() => {
cleanupPointerHandlers() cleanupControls()
scene.input.keyboard?.off('keycombomatch')
scene.input.off(Phaser.Input.Events.POINTER_WHEEL, handleScrollZoom)
}) })
</script> </script>

View File

@ -1,7 +1,7 @@
<template></template> <template></template>
<script setup lang="ts"> <script setup lang="ts">
import { CharacterHairStorage, CharacterTypeStorage, MapObjectStorage, MapStorage, SpriteStorage, TileStorage } from '@/storage/storages' import { CharacterHairStorage, CharacterTypeStorage, MapObjectStorage, MapStorage, SoundStorage, SpriteStorage, TileStorage } from '@/storage/storages'
import { TextureStorage } from '@/storage/textureStorage' import { TextureStorage } from '@/storage/textureStorage'
import { onMounted, onUnmounted } from 'vue' import { onMounted, onUnmounted } from 'vue'
@ -12,6 +12,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,6 +33,7 @@ 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
} }

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="emit('modal:close')" 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>
@ -46,7 +46,7 @@
import { computed, onMounted, onUnmounted, ref, watch } from 'vue' import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
interface ModalProps { interface ModalProps {
isModalOpen: boolean isModalOpen?: boolean
closable?: boolean closable?: boolean
isResizable?: boolean isResizable?: boolean
isFullScreen?: boolean isFullScreen?: boolean
@ -79,10 +79,20 @@ const props = withDefaults(defineProps<ModalProps>(), {
}) })
const emit = defineEmits<{ const emit = defineEmits<{
'modal:open': []
'modal:close': [] 'modal:close': []
'character:create': []
}>() }>()
defineExpose({
open: () => (isModalOpenRef.value = true),
close: () => (isModalOpenRef.value = false),
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)
const width = ref(props.modalWidth) const width = ref(props.modalWidth)
const height = ref(props.modalHeight) const height = ref(props.modalHeight)
@ -150,6 +160,11 @@ function drag(event: MouseEvent) {
y.value = dragState.initialY + (event.clientY - dragState.startY) y.value = dragState.initialY + (event.clientY - dragState.startY)
} }
function closeModal() {
isModalOpenRef.value = false
emit('modal:close')
}
function toggleFullScreen() { function toggleFullScreen() {
if (isFullScreen.value) { if (isFullScreen.value) {
Object.assign({ x, y, width, height }, preFullScreenState) Object.assign({ x, y, width, height }, preFullScreenState)

View File

@ -1,5 +1,5 @@
<template> <template>
<Modal v-for="notification in gameStore.notifications" :key="notification.id" :isModalOpen="true" @modal:close="closeNotification(notification.id)"> <Modal v-for="notification in gameStore.notifications" :key="notification.id" :isModalOpen="true" @modal:close="closeNotification(notification!.id as string)">
<template #modalHeader v-if="notification.title"> <template #modalHeader v-if="notification.title">
<h3 class="m-0 font-medium shrink-0 text-white">{{ notification.title }}</h3> <h3 class="m-0 font-medium shrink-0 text-white">{{ notification.title }}</h3>
</template> </template>
@ -10,6 +10,7 @@
</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 { 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 +27,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 +53,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

@ -0,0 +1,69 @@
import config from '@/application/config'
import { getTile, tileToWorldXY } from '@/services/mapService'
import { useGameStore } from '@/stores/gameStore'
import { ref, type Ref } from 'vue'
export function useBaseControlsComposable(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
const gameStore = useGameStore()
const pointerStartPosition = ref({ x: 0, y: 0 })
const dragThreshold = 5 // pixels
function updateWaypoint(worldX: number, worldY: number) {
const pointerTile = getTile(layer, worldX, worldY)
if (!pointerTile) {
waypoint.value.visible = false
return
}
const worldPoint = tileToWorldXY(layer, pointerTile.x, pointerTile.y)
if (!worldPoint.worldPositionX || !worldPoint.worldPositionX) return
waypoint.value = {
visible: true,
x: worldPoint.worldPositionX,
y: worldPoint.worldPositionY + config.tile_size.height + 15
}
}
function handleDragMap(pointer: Phaser.Input.Pointer) {
if (!gameStore.game.isPlayerDraggingCamera) return
const deltaX = pointer.x - pointerStartPosition.value.x
const deltaY = pointer.y - pointerStartPosition.value.y
if (Math.abs(deltaX) <= dragThreshold && Math.abs(deltaY) <= dragThreshold) return
const scrollX = camera.scrollX - deltaX / camera.zoom
const scrollY = camera.scrollY - deltaY / camera.zoom
camera.setScroll(scrollX, scrollY)
pointerStartPosition.value = { x: pointer.x, y: pointer.y }
}
function startDragging(pointer: Phaser.Input.Pointer) {
pointerStartPosition.value = { x: pointer.x, y: pointer.y }
gameStore.setPlayerDraggingCamera(true)
}
function stopDragging() {
gameStore.setPlayerDraggingCamera(false)
}
function handleZoom(pointer: Phaser.Input.Pointer) {
if (pointer.event instanceof WheelEvent && pointer.event.shiftKey) {
const deltaY = pointer.event.deltaY
const zoomLevel = camera.zoom - deltaY * 0.005
if (zoomLevel > 0 && zoomLevel < 3) {
camera.setZoom(zoomLevel)
}
}
}
return {
updateWaypoint,
handleDragMap,
startDragging,
stopDragging,
handleZoom,
pointerStartPosition
}
}

View File

@ -0,0 +1,108 @@
import { SocketEvent } from '@/application/enums'
import { getTile } from '@/services/mapService'
import { useGameStore } from '@/stores/gameStore'
import type { Ref } from 'vue'
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) {
const gameStore = useGameStore()
const baseHandlers = useBaseControlsComposable(scene, layer, waypoint, camera)
function handlePointerDown(pointer: Phaser.Input.Pointer) {
baseHandlers.startDragging(pointer)
}
function handlePointerMove(pointer: Phaser.Input.Pointer) {
baseHandlers.updateWaypoint(pointer.worldX, pointer.worldY)
baseHandlers.handleDragMap(pointer)
}
function handlePointerUp(pointer: Phaser.Input.Pointer) {
baseHandlers.stopDragging()
const pointerTile = getTile(layer, pointer.worldX, pointer.worldY)
if (!pointerTile) return
gameStore.connection?.emit(SocketEvent.MAP_CHARACTER_MOVE, {
positionX: pointerTile.x,
positionY: pointerTile.y
})
}
const pressedKeys = new Set<string>()
let moveInterval: number | null = null
function handleKeyDown(event: KeyboardEvent) {
if (!gameStore.character) return
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) {
// Prevent key repeat events
if (event.repeat) return
pressedKeys.add(event.key)
// Start movement loop if not already running
if (!moveInterval) {
moveInterval = window.setInterval(moveCharacter, 80) // Increased interval to match server throttle `MOVEMENT_THROTTLE`
moveCharacter() // Move immediately on first press
}
}
// Attack on CTRL
if (event.key === 'Control') {
gameStore.connection?.emit(SocketEvent.MAP_CHARACTER_ATTACK)
}
}
function handleKeyUp(event: KeyboardEvent) {
pressedKeys.delete(event.key)
// If no movement keys are pressed, clear the interval
if (pressedKeys.size === 0 && moveInterval) {
clearInterval(moveInterval)
moveInterval = null
}
}
function moveCharacter() {
if (!gameStore.character) return
const { positionX, positionY } = gameStore.character
let newX = positionX
let newY = positionY
// Calculate new position based on pressed keys
if (pressedKeys.has('ArrowLeft')) newX--
if (pressedKeys.has('ArrowRight')) newX++
if (pressedKeys.has('ArrowUp')) newY--
if (pressedKeys.has('ArrowDown')) newY++
// Only emit if position changed
if (newX !== positionX || newY !== positionY) {
gameStore.connection?.emit(SocketEvent.MAP_CHARACTER_MOVE, {
positionX: newX,
positionY: newY
})
}
}
const setupControls = () => {
scene.input.on(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
scene.input.on(Phaser.Input.Events.POINTER_WHEEL, baseHandlers.handleZoom)
scene.input.keyboard!.on('keydown', handleKeyDown)
scene.input.keyboard!.on('keyup', handleKeyUp)
}
const cleanupControls = () => {
scene.input.off(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
scene.input.off(Phaser.Input.Events.POINTER_UP, handlePointerUp)
scene.input.off(Phaser.Input.Events.POINTER_WHEEL, baseHandlers.handleZoom)
scene.input.keyboard!.off('keydown', handleKeyDown)
scene.input.keyboard!.off('keyup', handleKeyUp)
}
return { setupControls, cleanupControls }
}

View File

@ -0,0 +1,42 @@
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { computed, type Ref } from 'vue'
import { useBaseControlsComposable } from './useBaseControlsComposable'
export function useMapEditorControlsComposable(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
const mapEditor = useMapEditorComposable()
const baseHandlers = useBaseControlsComposable(scene, layer, waypoint, camera)
const isMoveTool = computed(() => mapEditor.tool.value === 'move')
function handlePointerDown(pointer: Phaser.Input.Pointer) {
if (isMoveTool.value || pointer.event.shiftKey) {
baseHandlers.startDragging(pointer)
}
}
function handlePointerMove(pointer: Phaser.Input.Pointer) {
if (isMoveTool.value || pointer.event.shiftKey) {
baseHandlers.handleDragMap(pointer)
}
baseHandlers.updateWaypoint(pointer.worldX, pointer.worldY)
}
function handlePointerUp(pointer: Phaser.Input.Pointer) {
baseHandlers.stopDragging()
}
const setupControls = () => {
scene.input.on(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
scene.input.on(Phaser.Input.Events.POINTER_WHEEL, baseHandlers.handleZoom)
}
const cleanupControls = () => {
scene.input.off(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
scene.input.off(Phaser.Input.Events.POINTER_UP, handlePointerUp)
scene.input.off(Phaser.Input.Events.POINTER_WHEEL, baseHandlers.handleZoom)
}
return { setupControls, cleanupControls }
}

View File

@ -1,34 +0,0 @@
export function createSceneLoader(scene: Phaser.Scene) {
const width = scene.cameras.main.width
const height = scene.cameras.main.height
const progressBox = scene.add.graphics()
const progressBar = scene.add.graphics()
progressBox.fillStyle(0x222222, 0.8)
progressBox.fillRect(width / 2 - 180, height / 2, 320, 50)
const loadingText = scene.make.text({
x: width / 2,
y: height / 2 - 50,
text: 'Loading...',
style: {
font: '20px monospace',
// @ts-ignore
fill: '#ffffff'
}
})
loadingText.setOrigin(0.5, 0.5)
scene.load.on(Phaser.Loader.Events.PROGRESS, function (value: any) {
progressBar.clear()
progressBar.fillStyle(0x368f8b, 1)
progressBar.fillRect(width / 2 - 180 + 10, height / 2 + 10, 300 * value, 30)
})
scene.load.on(Phaser.Loader.Events.COMPLETE, function () {
progressBar.destroy()
progressBox.destroy()
loadingText.destroy()
return true
})
}

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