1
0
forked from noxious/client

Compare commits

...

223 Commits

Author SHA1 Message Date
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
8befce7ffb Update packages 2025-01-29 23:28:04 +01: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
9de7af961e Improved map tile initialising 2025-01-10 23:22:32 +01:00
4067ec2585 . 2025-01-10 21:03:02 +01:00
fb18841c91 npm run format 2025-01-09 16:02:17 +01:00
7b1dcf7ce3 Added comments 2025-01-09 16:00:36 +01:00
7546116878 Better variable namings 2025-01-09 15:58:02 +01:00
03fef60621 Load textures using cache data instead of data sent from server 2025-01-08 21:13:04 +01:00
574777da80 stuffs 2025-01-07 22:20:46 +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
c2db9b5469 POC working new caching method - moved controllers folder, renamed assets to textures, fixed HTTP bug, formatted code 2025-01-07 03:59:08 +01:00
6e30a8530a POC working new caching method - moved controllers folder, renamed assets to textures, fixed HTTP bug, formatted code 2025-01-07 03:59:02 +01:00
41f82897a8 Better naming 2025-01-06 00:11:19 +01:00
37b97b0aac Fixes 2025-01-05 22:06:05 +01:00
c1d9cc3a11 Disabled vue dev tools, replaced ref with shallowRef, better naming 2025-01-05 21:00:16 +01:00
b54b825422 Map event tile improvements 2025-01-05 06:22:28 +01:00
0142850983 Map editor WIP 2025-01-05 04:01:50 +01:00
2d09715dc4 Map editor improvements 2025-01-05 01:43:39 +01:00
ef807982a5 Map editor improvements 2025-01-05 00:07:55 +01:00
ae0841889b Fixed emits 2025-01-04 23:17:54 +01:00
bdd2f93175 Added gap between list elements
(Cyan boxes touched when 1 item was selected and other was hovered over)
2025-01-04 22:41:20 +01:00
10f6dc3802 TS improvements 2025-01-04 19:17:10 +01:00
700bd57e67 Almost finalised refactoring 2025-01-03 14:35:08 +01:00
145143cdc5 4k on chat bubble text 2025-01-02 20:37:37 +01:00
201853a3ec Activated 4k on username text rendering 2025-01-02 20:33:54 +01:00
40c87f0ee3 Renamed zone > map 2025-01-02 17:31:31 +01:00
736ddddc54 Fix chips input component 2025-01-02 01:38:42 +01:00
63758e67b3 Removed redundant field 2025-01-01 23:01:58 +01:00
b51aa29bd8 Updated types 2025-01-01 20:59:38 +01:00
f9bfbdf735 Renamed property for better consistency 2025-01-01 20:46:02 +01:00
2abce7a7e7 Sorted imports 2025-01-01 19:05:24 +01:00
6ec9f8a7bc Loading char. texture works again 2025-01-01 18:16:31 +01:00
8191a039c9 Walking works again but needs to be improved 2025-01-01 17:50:07 +01:00
540425ca44 Minor change 2025-01-01 04:48:57 +01:00
1c2e642fe3 Typo 2025-01-01 02:59:26 +01:00
8355c83dc8 Renamed class 2024-12-31 16:15:28 +01:00
5fcb336835 Moved file 2024-12-30 17:44:56 +01:00
90bdf43b64 Small change 2024-12-30 02:47:49 +01:00
e9dfcf7870 Minor changes 2024-12-29 02:36:04 +01:00
d0c08c25fd #286 - Added global class for fully absolute-centering elements 2024-12-29 02:21:21 +01:00
7bb7af9476 #295 + #296 - Changed login boxes and char select styling 2024-12-29 02:10:18 +01:00
e4186a1bf5 WIP zone loading 2024-12-29 00:40:02 +01:00
8c664d7774 Started working on improved connect method 2024-12-28 23:48:52 +01:00
744df2e2dc Re-enabled vue dev tools, moved type into types.ts, added temp. logging 2024-12-28 17:27:00 +01:00
b4f9b11143 Removed console log 2024-12-27 19:04:33 +01:00
18b07d2f46 Several fixes, improvements, refactor for authentication 2024-12-27 19:00:53 +01:00
9d0f810ab3 Double field fix 2024-12-27 00:54:31 +01:00
cf3f17dfef Disabled vue dev tools for testing purposes 2024-12-27 00:54:23 +01:00
6be1134c8c Minor improvement 2024-12-27 00:49:57 +01:00
6dad7bc9dd http improvements, fixed link 2024-12-27 00:48:36 +01:00
231f19a30f Added characterChest component for chest equipment, moved some files 2024-12-27 00:27:54 +01:00
9c105d6df6 Returned data update 2024-12-26 23:55:47 +01:00
179ceb0ca0 Cleaned up assets, added default border values to main.scss 2024-12-26 20:03:42 +01:00
680661f07c #258 - Put update effects in the timeout corner until zoneEffects is ready
Reverted latest changes due to zoneEffects needing to fully overwrite
2024-12-25 00:40:56 +01:00
c54d2a2da8 Merge branch 'main' of ssh://gitea.directonline.io:29417/sylvan-quest/client 2024-12-24 00:54:27 +01:00
85f0fca2ae Added copy sprite button, changed asset manager layout, updated packages 2024-12-24 00:54:20 +01:00
420e63b724 #258 - Made it so zoneEffects only overrides defined effects instead of all 2024-12-23 23:23:16 +01:00
5d9b4fd19a #187 - Enter to focus chat when not focused 2024-12-22 20:56:06 +01:00
b3d68ef562 Merge branch 'main' of ssh://gitea.directonline.io:29417/sylvan-quest/client 2024-12-22 20:08:49 +01:00
baae737d6b CRUD components for items 2024-12-22 20:08:45 +01:00
03f8b327c5 #258 - Fixed zone effects when set in settings 2024-12-22 20:06:51 +01:00
b9a1ce5ab5 Adjusted sorting 2024-12-22 02:36:14 +01:00
1b650bd733 #16: Show updates made to character in real time 2024-12-22 00:13:15 +01:00
b867250580 Updated name 2024-12-21 22:10:37 +01:00
2c7a1e27be Fixes for origin being string, styling bug hair select and wrong label tags 2024-12-21 17:48:20 +01:00
0e455f8ffc Use originX and Y for hair 2024-12-21 03:00:09 +01:00
8005bc1318 Small fix 2024-12-21 02:29:48 +01:00
11e978121f Renamed frame speed > frame rate 2024-12-21 02:27:47 +01:00
727ca99b73 #262 : Use frameRate is value is set in sprite settings 2024-12-21 02:20:03 +01:00
97080d7380 Better anim. timing 2024-12-21 02:09:18 +01:00
1a3a53a229 Timing for animations 2024-12-20 21:29:33 +01:00
a926de8466 Hair anim. adjustment 2024-12-20 20:36:13 +01:00
5eabb39ec8 Improved hair animation 2024-12-20 01:28:48 +01:00
03313cb092 #259 - Changed bg options for modals, restyled GM Panel 2024-12-15 15:24:10 +01:00
d58cfa668d More finetuning of hair positioning 2024-12-15 14:40:47 +01:00
e3e40dd083 updated packages 2024-12-13 00:47:44 +01:00
facdd2d1b4 Minor styling changes 2024-12-13 00:47:36 +01:00
7d6bd39f29 GmPanel is full screen by default now 2024-12-12 01:12:33 +01:00
608932300f Improved proof of concept hair customisation & sprite anchor points 2024-12-12 00:42:56 +01:00
b5c1c92b04 Worked on character customisation 2024-12-10 02:22:03 +01:00
c68b129da8 #260 - Fix Characterprofile start position 2024-12-08 19:58:43 +01:00
963c593a1f #260 - Add min height when no character is selected 2024-12-08 19:45:11 +01:00
a299e22f88 #260 - added responsive styling character select 2024-12-08 19:29:07 +01:00
2007bfd7c5 Altered menu a bit 2024-12-08 00:53:07 +01:00
4cae045d0d Effect improvement 2024-12-08 00:35:00 +01:00
1fa8b8f06e You can now zoom in/out with key combination (shift + arrow uo/down)
my 5 EUR Action gaming mouse doesnt let me scroll on MacOS 💩
2024-12-07 23:24:11 +01:00
4095184b27 updated packages 2024-12-07 22:50:36 +01:00
857d56a878 Overriding zone effects now works 2024-12-07 22:50:27 +01:00
8087f754b0 Merge branch 'feature/#245-character-hair' of ssh://gitea.directonline.io:29417/sylvan-quest/client into feature/#245-character-hair 2024-12-05 09:56:17 +01:00
1479d96162 npm update, proof of concept code for character hair 2024-12-05 09:55:20 +01:00
606e220a9f Slightly adjusted styling radio hair color buttons 2024-11-27 20:39:09 +01:00
6988565484 Removed redundant code 2024-11-25 00:42:07 +01:00
fbc4a3dcdb Better typescript 2024-11-25 00:41:12 +01:00
924d5bdd13 Bug fixes and improvements to character select screen 2024-11-25 00:40:21 +01:00
25a2fd24f3 Prep hair color radio btns, adjust styling hairstyles to make it tab friendly 2024-11-24 20:46:29 +01:00
64f5ac45dd Removed console.log, renamed hars to characterHairs 2024-11-24 20:28:50 +01:00
937ce939d1 Preselect hairstyle that's set on character
Removed unused watch
2024-11-24 20:19:45 +01:00
7d89364104 Fix styling conditions for radio select
WIP
2024-11-24 17:15:57 +01:00
f7b8c235d8 Renamed hair > characterHair, split character logic (chat bubble, healthbar etc) into separate components for better DX 2024-11-24 15:13:11 +01:00
89d83efca4 Mess 2024-11-23 22:55:43 +01:00
ab97e27f27 Moved game components into a new folder, working proof of concept hair customisation 2024-11-23 16:47:41 +01:00
ee3e1b55cb Huehue wups 2024-11-23 15:43:57 +01:00
5e109e2a39 Removed ID from radio 2024-11-23 15:39:47 +01:00
a8e50c993a Commented out color swatch, added justify-center 2024-11-23 15:38:39 +01:00
ba8af589a7 Merge remote-tracking branch 'origin/feature/#245-character-hair' into feature/#250
# Conflicts:
#	src/components/screens/Characters.vue
2024-11-23 15:34:42 +01:00
301340327a Added components to manage hair types 2024-11-23 15:29:49 +01:00
1e4c58c79e npm update 2024-11-23 15:29:38 +01:00
f87cd063ee Center elements in characters screen 2024-11-23 15:29:28 +01:00
9593298389 #250 - Changed focus styling, adjusted where needed 2024-11-23 00:27:11 +01:00
ad4651844d Updated TS types 2024-11-21 03:00:09 +01:00
3748c459f8 Merge remote-tracking branch 'origin/main' into feature/#248 2024-11-20 21:19:22 +01:00
50ea3ecdab npm update 2024-11-20 21:19:05 +01:00
8910390f7b #248 - Unfocus chat input when clicking outside 2024-11-19 22:24:34 +01:00
d820490b2b Added update logic for character types, added isEnabledForCharCreation field 2024-11-17 17:52:24 +01:00
2c96caee4f Text white when item is active in asset manager 2024-11-17 17:00:07 +01:00
84939a7d32 Disabled texture in GM utility modals , fixed btn padding for zone delete button 2024-11-17 00:46:05 +01:00
1e3fc2b0f8 Added disableBgTexture option to modals 2024-11-17 00:39:28 +01:00
7c8b5f3e82 npm update 2024-11-16 01:21:43 +01:00
570d315bf5 Small improvements 2024-11-14 23:46:13 +01:00
7871b34c60 Don't open GM panel if focus is on any input or textarea 2024-11-14 22:28:10 +01:00
85d64f23eb #238: Remove hash from URL with JS instead of full redirect 2024-11-14 22:20:46 +01:00
bdb6dd0d54 #161: Store chats into database 2024-11-14 20:42:52 +01:00
faf887163a Added resize event to update to particle window size when resizing 2024-11-13 21:25:54 +01:00
dd5baa530d Removed console.log(), fixed small bug 2024-11-13 19:38:51 +01:00
d9947e29cf Prevent loading anims more than once 2024-11-13 15:42:21 +01:00
1888521762 Fixed modal fullscreen icons, made types compatible with server changes made for #174, npm update, npm run format, minor improvements 2024-11-13 13:22:20 +01:00
48fef2313b Renamed and moved assets for clarity, removed unused svg's 2024-11-08 22:03:10 +01:00
0a99d2c430 Finished base layout of character select/create 2024-11-08 21:22:50 +01:00
ed6f592606 WIP character select 2024-11-06 23:05:37 +01:00
46ebfaec01 npm update 2024-11-05 23:18:10 +01:00
1384f50406 npm run format 2024-11-05 23:16:47 +01:00
d71f4e7b59 #192: Update light and other effects based on server date / weather state 2024-11-05 23:07:05 +01:00
58929290ab #184: Listen for weather updates from client 2024-11-05 22:46:21 +01:00
63146106c0 Moved clock pos. 2024-11-05 22:02:46 +01:00
7c5602f204 #197: Added background image loader 2024-11-05 22:01:28 +01:00
e711e124ce Map editor tiles improvement 2024-11-05 21:31:28 +01:00
e1b39c42ec Several map editor improvements 2024-11-05 21:28:12 +01:00
d81c889426 Removed GM tools, added event listener for shift + G to open GM panel 2024-11-05 20:53:39 +01:00
afb0edacf6 #184: Added clock component 2024-11-05 20:42:52 +01:00
6d7d568746 Moved login partial components 2024-11-05 20:36:00 +01:00
8df5b6eb76 #239: Add loading indicator to password reset submit button for better UX 2024-11-05 20:21:52 +01:00
270d12821a Renamed component, inform user when password reset mail has been sent, added comment for #238 2024-11-05 01:02:27 +01:00
9c244e980c Cleaned component 2024-11-05 00:35:26 +01:00
25ba54c8ac Moved ResetPassword component to correct dir 2024-11-05 00:33:28 +01:00
9c4bef864b Updated default value 2024-11-05 00:01:36 +01:00
186 changed files with 5673 additions and 7029 deletions

View File

@ -1,5 +1,5 @@
VITE_NAME=Sylvan Quest
VITE_NAME=Noxious
VITE_DEVELOPMENT=true
VITE_SERVER_ENDPOINT=http://localhost:4000
VITE_TILE_SIZE_X=64
VITE_TILE_SIZE_Y=32
VITE_TILE_SIZE_WIDTH=64
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

@ -4,5 +4,8 @@
"tabWidth": 2,
"singleQuote": true,
"printWidth": 300,
"trailingComma": "none"
"trailingComma": "none",
"plugins": ["@ianvs/prettier-plugin-sort-imports"],
"importOrderParserPlugins": ["typescript", "jsx", "decorators-legacy", "classProperties"],
"importOrderCaseSensitive": false
}

View File

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

View File

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

4104
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,6 @@
"test:unit": "vitest",
"build-only": "vite build",
"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/"
},
"dependencies": {
@ -27,19 +26,14 @@
"zod": "^3.22.2"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.10.3",
"@ianvs/prettier-plugin-sort-imports": "^4.4.0",
"@tsconfig/node20": "^20.1.4",
"@types/jsdom": "^21.1.7",
"@types/node": "^20.14.11",
"@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/tsconfig": "^0.5.1",
"autoprefixer": "^10.4.19",
"easystarjs": "^0.4.4",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.27.0",
"jsdom": "^24.1.1",
"npm-run-all2": "^6.2.3",
"phaser3-rex-plugins": "^1.80.8",
@ -51,7 +45,6 @@
"typescript": "~5.6.2",
"vite": "^5.4.9",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-vue-devtools": "^7.5.2",
"vitest": "^2.0.3",
"vue-tsc": "^1.6.5"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 16L15 12M15 12L11 8M15 12H3M4.51555 17C6.13007 19.412 8.87958 21 12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3C8.87958 3 6.13007 4.58803 4.51555 7" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 453 B

View File

Before

Width:  |  Height:  |  Size: 597 KiB

After

Width:  |  Height:  |  Size: 597 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 600 KiB

After

Width:  |  Height:  |  Size: 600 KiB

View File

Before

Width:  |  Height:  |  Size: 600 KiB

After

Width:  |  Height:  |  Size: 600 KiB

View File

Before

Width:  |  Height:  |  Size: 599 KiB

After

Width:  |  Height:  |  Size: 599 KiB

View File

@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_598_541)">
<path d="M10.0327 5.69111L12.3905 3.33333H9.33333V2H14.6667V7.33333H13.3333V4.27614L10.9755 6.63392C11.6183 7.47513 12 8.52633 12 9.66667C12 12.4281 9.7614 14.6667 7 14.6667C4.23857 14.6667 2 12.4281 2 9.66667C2 6.90527 4.23857 4.66667 7 4.66667C8.14033 4.66667 9.19153 5.04843 10.0327 5.69111ZM7 13.3333C9.02507 13.3333 10.6667 11.6917 10.6667 9.66667C10.6667 7.6416 9.02507 6 7 6C4.97495 6 3.33333 7.6416 3.33333 9.66667C3.33333 11.6917 4.97495 13.3333 7 13.3333Z" fill="#999999"/>
</g>
<defs>
<clipPath id="clip0_598_541">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 729 B

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 325 B

After

Width:  |  Height:  |  Size: 325 B

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 847 B

After

Width:  |  Height:  |  Size: 847 B

View File

Before

Width:  |  Height:  |  Size: 745 B

After

Width:  |  Height:  |  Size: 745 B

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 915 B

After

Width:  |  Height:  |  Size: 918 B

View File

Before

Width:  |  Height:  |  Size: 768 B

After

Width:  |  Height:  |  Size: 768 B

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 375 B

After

Width:  |  Height:  |  Size: 375 B

View File

Before

Width:  |  Height:  |  Size: 652 B

After

Width:  |  Height:  |  Size: 652 B

View File

Before

Width:  |  Height:  |  Size: 346 B

After

Width:  |  Height:  |  Size: 346 B

View File

@ -1 +0,0 @@
<svg width="48" xmlns="http://www.w3.org/2000/svg" height="48" id="screenshot-e9346f42-72c8-800c-8004-507b356b7f18" viewBox="0 0 48 48" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-e9346f42-72c8-800c-8004-507b356b7f18" width="1em" height="1em" rx="0" ry="0" style="fill: rgb(0, 0, 0);"><g id="shape-e9346f42-72c8-800c-8004-507b356b7f1b"><defs style="fill: rgb(0, 0, 0);"/></g><g id="shape-e9346f42-72c8-800c-8004-507b356b7f1c"><defs><mask width="1.2" height="1.2" x="-0.1" id="render-3-ipSEnterKey0" data-old-y="-0.1" data-old-width="1.2" data-old-x="-0.1" y="-0.1" data-old-height="1.2"><g class="svg-mask-wrapper" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"><path fill="#fff" stroke="#fff" d="M44 44V4H24v16H4v24z"/><path stroke="#000" d="m21 28l-4 4l4 4"/><path stroke="#000" d="M34 23v9H17"/></g></g></mask></defs><g class="fills" id="fills-e9346f42-72c8-800c-8004-507b356b7f1c"><path d="M0.000,0.000L48.000,0.000L48.000,48.000L0.000,48.000ZZ" mask="url(#render-3-ipSEnterKey0)" style="fill: #696969; fill-opacity: 1;"/></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,3 @@
<svg width="13" height="16" viewBox="0 0 13 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.00044 7.42339L12.0005 1.07252C12.3193 0.888385 12.7271 0.997653 12.9111 1.31652C12.9697 1.41785 13.0005 1.53285 13.0005 1.64985L13.0005 14.3516C13.0005 14.7198 12.702 15.0182 12.3338 15.0182C12.2167 15.0182 12.1018 14.9874 12.0005 14.9289L1.00044 8.57805C0.681573 8.39399 0.572326 7.98625 0.756419 7.66739C0.814932 7.56605 0.899092 7.48185 1.00044 7.42339Z" fill="#4D4D4D"/>
</svg>

After

Width:  |  Height:  |  Size: 490 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="M8.5859 10.0001L0.792969 2.20718L2.20718 0.792969L10.0001 8.58582L17.793 0.792969L19.2072 2.20718L11.4143 10.0001L19.2072 17.7929L17.793 19.2072L10.0001 11.4143L2.20718 19.2072L0.792969 17.7929L8.5859 10.0001Z" fill="#999999"/>
</svg>

After

Width:  |  Height:  |  Size: 340 B

View File

Before

Width:  |  Height:  |  Size: 109 B

After

Width:  |  Height:  |  Size: 109 B

View File

Before

Width:  |  Height:  |  Size: 696 B

After

Width:  |  Height:  |  Size: 696 B

View File

Before

Width:  |  Height:  |  Size: 708 B

After

Width:  |  Height:  |  Size: 708 B

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 B

View File

@ -1 +0,0 @@
<svg width="170" xmlns="http://www.w3.org/2000/svg" height="275" id="screenshot-c7008730-586b-8052-8004-79a2f5807f5c" viewBox="0 0 170 275" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-c7008730-586b-8052-8004-79a2f5807f5c"><defs><linearGradient id="fill-color-gradient-render-1-0" x1="0.4238862507773219" y1="0.26897966364321624" x2="0.7513711107948178" y2="0.9146156628521559" gradientTransform=""><stop offset="0" stop-color="#ffffff" stop-opacity="0.4"/><stop offset="1" stop-color="#ffffff" stop-opacity="0"/></linearGradient><pattern patternUnits="userSpaceOnUse" x="0" y="0" width="170" height="275" patternTransform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)" id="fill-0-render-1"><g><rect width="170" height="275" style="fill: url(&quot;#fill-color-gradient-render-1-0&quot;);"/></g></pattern></defs><g class="fills" id="fills-c7008730-586b-8052-8004-79a2f5807f5c"><path d="M170.000,20.000L170.000,255.000C170.000,266.038,161.038,275.000,150.000,275.000L20.000,275.000C8.962,275.000,0.000,266.038,0.000,255.000L0.000,20.000C0.000,8.962,8.962,0.000,20.000,0.000L150.000,0.000C150.000,0.000,150.000,0.000,150.000,0.000C150.000,11.038,158.962,20.000,170.000,20.000Z" fill="url(#fill-0-render-1)"/></g><g id="strokes-c7008730-586b-8052-8004-79a2f5807f5c" class="strokes"><g class="inner-stroke-shape"><defs><clipPath id="inner-stroke-render-1-c7008730-586b-8052-8004-79a2f5807f5c-0"><use href="#stroke-shape-render-1-c7008730-586b-8052-8004-79a2f5807f5c-0"/></clipPath><path d="M170.000,20.000L170.000,255.000C170.000,266.038,161.038,275.000,150.000,275.000L20.000,275.000C8.962,275.000,0.000,266.038,0.000,255.000L0.000,20.000C0.000,8.962,8.962,0.000,20.000,0.000L150.000,0.000C150.000,0.000,150.000,0.000,150.000,0.000C150.000,11.038,158.962,20.000,170.000,20.000Z" id="stroke-shape-render-1-c7008730-586b-8052-8004-79a2f5807f5c-0" style="fill: none; stroke-width: 4; stroke: rgb(255, 255, 255); stroke-opacity: 1;"/></defs><use href="#stroke-shape-render-1-c7008730-586b-8052-8004-79a2f5807f5c-0" clip-path="url('#inner-stroke-render-1-c7008730-586b-8052-8004-79a2f5807f5c-0')"/></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1 +0,0 @@
<svg width="290" xmlns="http://www.w3.org/2000/svg" height="87" id="screenshot-e9942e24-155b-8096-8004-7eaff9882cd6" viewBox="0 0 290 87" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-e9942e24-155b-8096-8004-7eaff9882cd6"><g class="fills" id="fills-e9942e24-155b-8096-8004-7eaff9882cd6"><path d="M3.485,8.722C7.089,3.457,13.144,0.000,20.000,0.000L270.000,0.000C281.038,0.000,290.000,8.962,290.000,20.000L290.000,67.000C290.000,78.038,281.038,87.000,270.000,87.000L20.000,87.000C13.166,87.000,7.128,83.565,3.520,78.329C21.157,76.589,35.000,61.648,35.000,43.500C35.000,25.390,21.216,10.475,3.485,8.722ZM0.000,67.000L0.000,20.000"/></g><g id="strokes-e9942e24-155b-8096-8004-7eaff9882cd6" class="strokes"><g class="inner-stroke-shape"><defs><clipPath id="inner-stroke-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0"><use href="#stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0"/></clipPath><path d="M3.485,8.722C7.089,3.457,13.144,0.000,20.000,0.000L270.000,0.000C281.038,0.000,290.000,8.962,290.000,20.000L290.000,67.000C290.000,78.038,281.038,87.000,270.000,87.000L20.000,87.000C13.166,87.000,7.128,83.565,3.520,78.329C21.157,76.589,35.000,61.648,35.000,43.500C35.000,25.390,21.216,10.475,3.485,8.722ZM0.000,67.000L0.000,20.000" id="stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0" style="fill: none; stroke-width: 6; stroke: rgb(77 77 77); stroke-opacity: 1;"/></defs><use href="#stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0" clip-path="url('#inner-stroke-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0')"/></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1 +0,0 @@
<svg width="290" xmlns="http://www.w3.org/2000/svg" height="87" id="screenshot-e9942e24-155b-8096-8004-7eaff9882cd6" viewBox="0 0 290 87" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-e9942e24-155b-8096-8004-7eaff9882cd6"><g class="fills" id="fills-e9942e24-155b-8096-8004-7eaff9882cd6"><path d="M3.485,8.722C7.089,3.457,13.144,0.000,20.000,0.000L270.000,0.000C281.038,0.000,290.000,8.962,290.000,20.000L290.000,67.000C290.000,78.038,281.038,87.000,270.000,87.000L20.000,87.000C13.166,87.000,7.128,83.565,3.520,78.329C21.157,76.589,35.000,61.648,35.000,43.500C35.000,25.390,21.216,10.475,3.485,8.722ZM0.000,67.000L0.000,20.000" style="fill: rgb(127, 127, 127); fill-opacity: 0.7;"/></g><g id="strokes-e9942e24-155b-8096-8004-7eaff9882cd6" class="strokes"><g class="inner-stroke-shape"><defs><clipPath id="inner-stroke-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0"><use href="#stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0"/></clipPath><path d="M3.485,8.722C7.089,3.457,13.144,0.000,20.000,0.000L270.000,0.000C281.038,0.000,290.000,8.962,290.000,20.000L290.000,67.000C290.000,78.038,281.038,87.000,270.000,87.000L20.000,87.000C13.166,87.000,7.128,83.565,3.520,78.329C21.157,76.589,35.000,61.648,35.000,43.500C35.000,25.390,21.216,10.475,3.485,8.722ZM0.000,67.000L0.000,20.000" id="stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0" style="fill: none; stroke-width: 6; stroke: rgb(255, 255, 255); stroke-opacity: 1;"/></defs><use href="#stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0" clip-path="url('#inner-stroke-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0')"/></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1 +0,0 @@
<svg width="1508.086" xmlns="http://www.w3.org/2000/svg" height="1511.251" id="screenshot-0d120e2a-8725-8061-8004-79728483f7ea" viewBox="-201.784 -208.012 1508.086 1511.251" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-0d120e2a-8725-8061-8004-79728483f7ea" width="800px" height="800px" rx="0" ry="0" style="opacity: 0.3; fill: rgb(0, 0, 0);"><g id="shape-0d120e2a-8725-8061-8004-79728484b3fe"><g class="fills" id="fills-0d120e2a-8725-8061-8004-79728484b3fe"><path d="M1190.359,745.690L1043.367,575.945L1133.504,630.603C1099.180,585.722,1047.978,532.622,975.722,469.519L780.783,401.898L896.468,404.538C851.234,371.382,797.068,339.090,738.130,311.073L601.350,337.338L689.349,289.039C627.425,263.088,562.143,241.922,497.713,229.160C430.172,215.674,363.453,211.534,303.512,221.641L314.753,151.382C271.664,177.012,239.130,209.992,214.226,251.017L204.390,177.710C166.181,212.950,148.095,250.172,143.131,301.343C69.092,307.974,-2.300,327.925,-73.861,347.005L-63.628,384.898C361.675,238.903,753.109,407.667,987.467,615.054L960.305,643.506C749.259,458.743,490.332,358.712,193.406,380.541C209.110,415.226,228.858,447.126,251.288,474.785L371.998,430.671L277.417,504.449C294.635,521.771,312.591,536.670,331.111,548.765L396.081,470.998L358.366,564.253C377.625,573.924,397.183,580.217,416.674,582.837C534.164,599.232,652.310,618.566,782.785,703.535L773.618,601.955L831.163,737.792C852.261,754.489,876.370,771.191,897.168,782.701L861.169,684.190L960.409,811.519C976.589,817.512,992.991,822.477,1008.953,826.486C1066.083,840.287,1120.015,842.594,1157.256,819.002C1175.975,807.393,1189.205,786.963,1190.791,762.853C1191.080,756.933,1190.907,750.762,1190.359,745.690ZZ" style="fill: rgb(13 109 105);"/></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

BIN
public/assets/tlogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 470 KiB

After

Width:  |  Height:  |  Size: 470 KiB

View File

Before

Width:  |  Height:  |  Size: 471 KiB

After

Width:  |  Height:  |  Size: 471 KiB

View File

Before

Width:  |  Height:  |  Size: 598 KiB

After

Width:  |  Height:  |  Size: 598 KiB

View File

@ -0,0 +1,25 @@
<svg width="190" height="202" viewBox="0 0 190 202" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 8H8V0C7.40741 4.14815 4.14815 7.40741 0 8Z" fill="#1A1A1A"/>
<path d="M7.55 0V0C7.55 4.16975 4.16975 7.55 0 7.55V7.55" stroke="#4D4D4D"/>
<mask id="path-3-inside-1_632_705" fill="white">
<path d="M0 8H8V194H0V8Z"/>
</mask>
<path d="M0 8H8V194H0V8Z" fill="#1A1A1A"/>
<path d="M1 194V8H-1V194H1Z" fill="#4D4D4D" mask="url(#path-3-inside-1_632_705)"/>
<path d="M0 194H8V202C7.40741 197.852 4.14815 194.593 0 194Z" fill="#1A1A1A"/>
<path d="M7.55 202V202C7.55 197.83 4.16975 194.45 0 194.45V194.45" stroke="#4D4D4D"/>
<mask id="path-7-inside-2_632_705" fill="white">
<path d="M8 0H182V202H8V0Z"/>
</mask>
<path d="M8 0H182V202H8V0Z" fill="#1A1A1A"/>
<path d="M8 1H182V-1H8V1ZM182 201H8V203H182V201Z" fill="#4D4D4D" mask="url(#path-7-inside-2_632_705)"/>
<path d="M190 8H182V0C182.593 4.14815 185.852 7.40741 190 8Z" fill="#1A1A1A"/>
<path d="M182.45 0V0C182.45 4.16975 185.83 7.55 190 7.55V7.55" stroke="#4D4D4D"/>
<mask id="path-11-inside-3_632_705" fill="white">
<path d="M190 8H182V194H190V8Z"/>
</mask>
<path d="M190 8H182V194H190V8Z" fill="#1A1A1A"/>
<path d="M189 194V8H191V194H189Z" fill="#4D4D4D" mask="url(#path-11-inside-3_632_705)"/>
<path d="M190 194H182V202C182.593 197.852 185.852 194.593 190 194Z" fill="#1A1A1A"/>
<path d="M182.45 202V202C182.45 197.83 185.83 194.45 190 194.45V194.45" stroke="#4D4D4D"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 470 KiB

After

Width:  |  Height:  |  Size: 470 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 302 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 400 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 453 KiB

After

Width:  |  Height:  |  Size: 454 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 301 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -1,42 +1,48 @@
<template>
<Debug />
<Notifications />
<GmTools v-if="gameStore.character?.role === 'gm'" />
<GmPanel v-if="gameStore.character?.role === 'gm'" />
<BackgroundImageLoader />
<GmPanel v-if="gameStore.character?.role === 'gm'" @open-map-editor="mapEditor.toggleActive" />
<component :is="currentScreen" />
</template>
<script setup lang="ts">
import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import Notifications from '@/components/utilities/Notifications.vue'
import GmTools from '@/components/gameMaster/GmTools.vue'
import GmPanel from '@/components/gameMaster/GmPanel.vue'
import Login from '@/components/screens/Login.vue'
import Characters from '@/components/screens/Characters.vue'
import Game from '@/components/screens/Game.vue'
import ZoneEditor from '@/components/screens/ZoneEditor.vue'
import { computed, watch } from 'vue'
import Loading from '@/components/screens/Loading.vue'
import Login from '@/components/screens/Login.vue'
import MapEditor from '@/components/screens/MapEditor.vue'
import BackgroundImageLoader from '@/components/utilities/BackgroundImageLoader.vue'
import Debug from '@/components/utilities/Debug.vue'
import Notifications from '@/components/utilities/Notifications.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { useGameStore } from '@/stores/gameStore'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { computed, ref, useTemplateRef, watch } from 'vue'
const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore()
const mapEditor = useMapEditorComposable()
const currentScreen = computed(() => {
if (!gameStore.game.isLoaded) return Loading
if (!gameStore.connection) return Login
if (!gameStore.token) return Login
if (!gameStore.character) return Characters
if (zoneEditorStore.active) return ZoneEditor
if (mapEditor.active.value) return MapEditor
return Game
})
// Watch zoneEditorStore.active and empty gameStore.game.loadedAssets
// Watch mapEditor.active and empty gameStore.game.loadedAssets
watch(
() => zoneEditorStore.active,
() => mapEditor.active.value,
() => {
gameStore.game.loadedAssets = []
gameStore.game.loadedTextures = []
}
)
// #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) => {
if (!(event.target instanceof HTMLButtonElement)) {
return
@ -44,4 +50,18 @@ addEventListener('click', (event) => {
const audio = new Audio('/assets/music/click-btn.mp3')
audio.play()
})
// Watch for "G" key press and toggle the gm panel
addEventListener('keydown', (event) => {
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
if (event.repeat || event.isComposing || event.defaultPrevented || document.activeElement?.tagName.toUpperCase() === 'INPUT' || document.activeElement?.tagName.toUpperCase() === 'TEXTAREA') {
return
}
if (event.key === 'G') {
gameStore.toggleGmPanel()
}
})
</script>

View File

@ -3,7 +3,7 @@ export default {
development: import.meta.env.VITE_DEVELOPMENT === 'true',
server_endpoint: import.meta.env.VITE_SERVER_ENDPOINT,
tile_size: {
x: Number(import.meta.env.VITE_TILE_SIZE_X),
y: Number(import.meta.env.VITE_TILE_SIZE_Y)
width: Number(import.meta.env.VITE_TILE_SIZE_WIDTH),
height: Number(import.meta.env.VITE_TILE_SIZE_HEIGHT)
}
}

View File

@ -1,119 +1,126 @@
export type UUID = `${string}-${string}-${string}-${string}-${string}`
export type Notification = {
id?: string
title?: string
message?: string
}
export type AssetDataT = {
export type HttpResponse<T> = {
success: boolean
message?: string
data?: T
}
export type TextureData = {
key: string
data: string
group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
data: string // URL or Base64 encoded blob
group: 'tiles' | 'map_objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
updatedAt: Date
isAnimated?: boolean
frameCount?: number
originX?: number
originY?: number
frameRate?: number
frameWidth?: number
frameHeight?: number
frameCount?: number
}
export type Tile = {
id: string
id: UUID
name: string
tags: any | null
createdAt: Date
updatedAt: Date
}
export type Object = {
id: string
export type MapObject = {
id: UUID
name: string
tags: any | null
originX: number
originY: number
isAnimated: boolean
frameSpeed: number
frameRate: number
frameWidth: number
frameHeight: number
createdAt: Date
updatedAt: Date
ZoneObject: ZoneObject[]
}
export type Item = {
id: string
id: UUID
name: string
description: string | null
itemType: ItemType
stackable: boolean
rarity: ItemRarity
sprite?: Sprite
createdAt: Date
updatedAt: Date
characters: CharacterItem[]
}
export type Zone = {
id: number
export type ItemType = 'WEAPON' | 'HELMET' | 'CHEST' | 'LEGS' | 'BOOTS' | 'GLOVES' | 'RING' | 'NECKLACE'
export type ItemRarity = 'COMMON' | 'UNCOMMON' | 'RARE' | 'EPIC' | 'LEGENDARY'
export type Map = {
id: UUID
name: string
width: number
height: number
tiles: any | null
tiles: string[][]
pvp: boolean
zoneEffects: ZoneEffect[]
zoneEventTiles: ZoneEventTile[]
zoneObjects: ZoneObject[]
mapEffects: MapEffect[]
mapEventTiles: MapEventTile[]
placedMapObjects: PlacedMapObject[]
characters: Character[]
chats: Chat[]
createdAt: Date
updatedAt: Date
}
export type ZoneEffect = {
id: string
zoneId: number
zone: Zone
export type MapEffect = {
id: UUID
map: Map
effect: string
strength: number
}
export type ZoneObject = {
id: string
zoneId: number
zone: Zone
objectId: string
object: Object
export type PlacedMapObject = {
id: UUID
map: Map
mapObject: MapObject
depth: number
isRotated: boolean
positionX: number
positionY: number
}
export enum ZoneEventTileType {
export enum MapEventTileType {
BLOCK = 'BLOCK',
TELEPORT = 'TELEPORT',
NPC = 'NPC',
ITEM = 'ITEM'
}
export type ZoneEventTile = {
id: string
zoneId: number
zone: Zone
type: ZoneEventTileType
export type MapEventTile = {
id: UUID
mapId: UUID
type: MapEventTileType
positionX: number
positionY: number
teleport?: ZoneEventTileTeleport
teleport?: MapEventTileTeleport
}
export type ZoneEventTileTeleport = {
id: string
zoneEventTileId: string
zoneEventTile: ZoneEventTile
toZoneId: number
toZone: Zone
export type MapEventTileTeleport = {
id: UUID
mapEventTile: MapEventTile
toMap: Map
toPositionX: number
toPositionY: number
toRotation: number
}
export type User = {
id: number
id: UUID
username: string
password: string
characters: Character[]
@ -133,20 +140,27 @@ export enum CharacterRace {
}
export type CharacterType = {
id: number
id: UUID
name: string
gender: CharacterGender
race: CharacterRace
characters: Character[]
spriteId?: string
isSelectable: boolean
sprite?: Sprite
createdAt: Date
updatedAt: Date
}
export type CharacterHair = {
id: UUID
name: string
sprite?: Sprite
gender: CharacterGender
isSelectable: boolean
}
export type Character = {
id: number
userId: number
id: UUID
userId: UUID
user: User
name: string
hitpoints: number
@ -158,29 +172,43 @@ export type Character = {
positionX: number
positionY: number
rotation: number
zoneId: number
zone: Zone
characterTypeId: number | null
characterType: CharacterType | null
characterType: UUID | null
characterHair: UUID | null
map: UUID
chats: Chat[]
items: CharacterItem[]
equipment: CharacterEquipment[]
}
export type ExtendedCharacter = Character & {
isMoving?: boolean
export type MapCharacter = {
character: Character
isMoving: boolean
}
export type CharacterItem = {
id: number
characterId: number
id: UUID
character: Character
itemId: string
item: Item
quantity: number
}
export type CharacterEquipment = {
id: UUID
slot: CharacterEquipmentSlotType
characterItem: CharacterItem
}
export enum CharacterEquipmentSlotType {
HEAD = 'HEAD',
BODY = 'BODY',
ARMS = 'ARMS',
LEGS = 'LEGS',
NECK = 'NECK',
RING = 'RING'
}
export type Sprite = {
id: string
id: UUID
name: string
createdAt: Date
updatedAt: Date
@ -188,39 +216,45 @@ export type Sprite = {
characterTypes: CharacterType[]
}
export interface SpriteImage {
url: string
offset: {
x: number
y: number
}
}
export type SpriteAction = {
id: string
spriteId: string
sprite: Sprite
sprite: string
action: string
sprites: string[]
sprites: SpriteImage[]
originX: number
originY: number
isAnimated: boolean
isLooping: boolean
frameWidth: number
frameHeight: number
frameSpeed: number
frameRate: number
}
export type Chat = {
id: number
characterId: number
id: string
character: Character
zoneId: number
zone: Zone
map: Map
message: string
createdAt: Date
}
export type ChatMessage = {
character: Character
message: string
}
export type WorldSettings = {
date: Date
isRainEnabled: boolean
isFogEnabled: boolean
weatherState: WeatherState
}
export type WeatherState = {
rainPercentage: number
fogDensity: number
}
export type mapLoadData = {
mapId: UUID
characters: MapCharacter[]
}

View File

@ -0,0 +1,52 @@
import config from '@/application/config'
import type { HttpResponse } from '@/application/types'
import type { BaseStorage } from '@/storage/baseStorage'
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))
}
export function unduplicateArray(array: any[]) {
return [...new Set(array.flat())]
}
export function getDomain() {
// Check if not localhost
if (window.location.hostname !== 'localhost') {
return window.location.hostname
}
// Check if not IP address
if (window.location.hostname.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) {
return window.location.hostname
}
if (window.location.hostname.split('.').length < 3) {
return window.location.hostname
}
return window.location.hostname.split('.').slice(-2).join('.')
}
export async function downloadCache<T extends { id: string; updatedAt: Date }>(endpoint: string, storage: BaseStorage<T>) {
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) {
let overwrite = false
const existingItem = await storage.get(item.id)
if (!existingItem || item.updatedAt > existingItem.updatedAt) {
overwrite = true
}
await storage.add(item, overwrite)
}
}

Binary file not shown.

View File

@ -23,6 +23,14 @@ body {
// Disable pinch zoom
touch-action: pan-x pan-y;
// Add custom focus outline
*:focus-visible {
@apply outline-gray-300;
@apply outline;
@apply outline-offset-2;
@apply rounded-sm;
}
}
h1,
@ -45,7 +53,7 @@ label {
button,
a {
@apply font-medium drop-shadow-20;
@apply font-medium;
}
button,
@ -58,13 +66,20 @@ input {
appearance: textfield;
}
&[type='number']::-webkit-inner-spin-button,
&[type='number']::-webkit-outer-spin-button {
&[type='number']::-webkit-outer-spin-button,
&[type='radio'] {
-webkit-appearance: none;
}
}
.input-field {
@apply px-4 py-2.5 text-base leading-5 focus-visible:outline-none bg-gray border border-solid border-gray-500 rounded text-gray-300;
@apply px-4 py-2.5 text-base leading-5 bg-gray border border-solid border-gray-500 rounded text-gray-300;
&:focus-visible {
@apply outline-none border-cyan rounded bg-gray-900;
}
&::placeholder {
@apply focus-visible:text-gray-300/50;
}
&.inactive {
@apply bg-gray-600/50 hover:cursor-not-allowed;
&::placeholder {
@ -99,11 +114,11 @@ button {
}
&.btn-red {
@apply bg-red text-gray-50 text-base leading-5 rounded py-2.5;
@apply bg-red-300 text-gray-50 text-base leading-5 rounded py-2.5;
&.active,
&:hover {
@apply bg-red-300;
@apply bg-red-400;
}
}
@ -111,8 +126,9 @@ button {
@apply text-gray-50 border-2 border-solid border-gray-500 text-base leading-5 rounded py-2.5;
&.active,
&.selected,
&:hover {
@apply bg-gray-700 border-gray-700;
@apply bg-gray border-gray;
}
}
@ -123,6 +139,28 @@ button {
&.eye-open {
@apply bg-[url('/assets/icons/eye-closed.svg')] w-5 h-4 right-2.5;
}
&.selected {
@apply bg-gray-500 border-gray-400;
}
}
.character.active {
@apply bg-gray bg-none;
}
.hair-deselect:has(:checked) {
img {
@apply brightness-200;
}
}
.default-border {
@apply border border-solid border-gray-500;
}
.center-element {
@apply absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2;
}
.text-pixel {
@ -133,6 +171,15 @@ button {
@apply hidden;
}
.scrollbar {
&::-webkit-scrollbar {
@apply block w-0.5 bg-gray-300 rounded-sm;
}
&::-webkit-scrollbar-thumb {
@apply bg-gray-175;
}
}
canvas {
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-crisp-edges;

View File

@ -1,110 +0,0 @@
<template>
<Scene name="effects" @preload="preloadScene" @create="createScene" @update="updateScene"> </Scene>
</template>
<script setup lang="ts">
import { Scene } from 'phavuer'
import { useZoneStore } from '@/stores/zoneStore'
import { onBeforeUnmount, ref, watch } from 'vue'
const zoneStore = useZoneStore()
const sceneRef = ref<Phaser.Scene | null>(null)
// Effect-related refs
const lightEffect = ref<Phaser.GameObjects.Graphics | null>(null)
const rainEmitter = ref<Phaser.GameObjects.Particles.ParticleEmitter | null>(null)
const fogSprite = ref<Phaser.GameObjects.Sprite | null>(null)
const preloadScene = async (scene: Phaser.Scene) => {
scene.load.image('raindrop', 'assets/raindrop.png')
scene.load.image('fog', 'assets/fog.png')
}
const createScene = async (scene: Phaser.Scene) => {
sceneRef.value = scene
createLightEffect(scene)
createRainEffect(scene)
createFogEffect(scene)
}
const updateScene = () => {
updateEffects()
}
const createLightEffect = (scene: Phaser.Scene) => {
lightEffect.value = scene.add.graphics()
lightEffect.value.setDepth(1000)
}
const createRainEffect = (scene: Phaser.Scene) => {
rainEmitter.value = scene.add.particles(0, 0, 'raindrop', {
x: { min: 0, max: window.innerWidth },
y: -50,
quantity: 5,
lifespan: 2000,
speedY: { min: 300, max: 500 },
scale: { start: 0.005, end: 0.005 },
alpha: { start: 0.5, end: 0 },
blendMode: 'ADD'
})
rainEmitter.value.setDepth(900)
rainEmitter.value.stop()
}
const createFogEffect = (scene: Phaser.Scene) => {
fogSprite.value = scene.add.sprite(window.innerWidth / 2, window.innerHeight / 2, 'fog')
fogSprite.value.setScale(2)
fogSprite.value.setAlpha(0)
fogSprite.value.setDepth(950)
}
const updateEffects = () => {
const effects = zoneStore.zone?.zoneEffects || []
effects.forEach((effect) => {
switch (effect.effect) {
case 'light':
updateLightEffect(effect.strength)
break
case 'rain':
updateRainEffect(effect.strength)
break
case 'fog':
updateFogEffect(effect.strength)
break
}
})
}
const updateLightEffect = (strength: number) => {
if (!lightEffect.value) return
const darkness = 1 - strength / 100
lightEffect.value.clear()
lightEffect.value.fillStyle(0x000000, darkness)
lightEffect.value.fillRect(0, 0, window.innerWidth, window.innerHeight)
}
const updateRainEffect = (strength: number) => {
if (!rainEmitter.value) return
if (strength > 0) {
rainEmitter.value.start()
rainEmitter.value.setQuantity(Math.floor((strength / 100) * 10))
} else {
rainEmitter.value.stop()
}
}
const updateFogEffect = (strength: number) => {
if (!fogSprite.value) return
fogSprite.value.setAlpha(strength / 100)
}
watch(() => zoneStore.zone?.zoneEffects, updateEffects, { deep: true })
onBeforeUnmount(() => {
if (sceneRef.value) sceneRef.value.scene.remove('effects')
})
// @TODO : Fix resize issue
</script>

View File

@ -1,10 +1,10 @@
<template>
<div class="flex flex-wrap items-center input-field gap-1">
<div v-for="(chip, i) in internalValue" :key="i" class="flex gap-2.5 items-center bg-cyan rounded py-1 px-2">
<div class="flex flex-wrap items-center input-field gap-1" @click="focusInput">
<div v-for="(chip, i) in internalValue" :key="i" class="flex gap-2.5 items-center bg-cyan rounded py-1 px-2" role="listitem">
<span class="text-xs text-white">{{ chip }}</span>
<button type="button" class="text-xs cursor-pointer text-white font-light font-default not-italic hover:text-gray-50" @click="deleteChip(i)" aria-label="Remove chip">×</button>
<button type="button" class="text-xs cursor-pointer text-white font-light font-default not-italic hover:text-gray-50" @click.stop="deleteChip(i)" aria-label="Remove tag">×</button>
</div>
<input class="outline-none border-none p-1 text-gray-300" placeholder="Tag name" v-model="currentInput" @keypress.enter.prevent="addChip" @keydown.backspace="handleBackspace" />
<input ref="inputRef" class="outline-none border-none p-1 text-gray-300 min-w-[60px] flex-grow" :placeholder="placeholder" v-model.trim="currentInput" @keydown="handleKeydown" @paste="handlePaste" :maxlength="maxChipLength" aria-label="Add new tag" />
</div>
</template>
@ -14,20 +14,29 @@ import type { Ref } from 'vue'
interface Props {
modelValue?: string[]
maxChips?: number
maxChipLength?: number
placeholder?: string
allowDuplicates?: boolean
}
const props = withDefaults(defineProps<Props>(), {
modelValue: () => []
modelValue: () => [],
maxChips: 10,
maxChipLength: 20,
placeholder: 'Add tag',
allowDuplicates: false
})
const emit = defineEmits<{
(e: 'update:modelValue', value: string[]): void
(e: 'error', message: string): void
}>()
const currentInput: Ref<string> = ref('')
const internalValue = ref<string[]>([])
const inputRef = ref<HTMLInputElement | null>(null)
// Initialize internalValue with props.modelValue
watch(
() => props.modelValue,
(newValue) => {
@ -36,9 +45,27 @@ watch(
{ immediate: true }
)
const validateChip = (chip: string): boolean => {
if (!chip) {
return false
}
if (!props.allowDuplicates && internalValue.value.includes(chip)) {
emit('error', 'Duplicate tags are not allowed')
return false
}
if (internalValue.value.length >= props.maxChips) {
emit('error', `Maximum ${props.maxChips} tags allowed`)
return false
}
return true
}
const addChip = () => {
const trimmedInput = currentInput.value.trim()
if (trimmedInput && !internalValue.value.includes(trimmedInput)) {
if (validateChip(trimmedInput)) {
internalValue.value.push(trimmedInput)
emit('update:modelValue', internalValue.value)
currentInput.value = ''
@ -50,10 +77,36 @@ const deleteChip = (index: number) => {
emit('update:modelValue', internalValue.value)
}
const handleBackspace = (event: KeyboardEvent) => {
if (event.key === 'Backspace' && currentInput.value === '' && internalValue.value.length > 0) {
internalValue.value.pop()
emit('update:modelValue', internalValue.value)
const handleKeydown = (event: KeyboardEvent) => {
switch (event.key) {
case 'Enter':
event.preventDefault()
addChip()
break
case 'Backspace':
if (currentInput.value === '' && internalValue.value.length > 0) {
deleteChip(internalValue.value.length - 1)
}
break
}
}
const handlePaste = (event: ClipboardEvent) => {
event.preventDefault()
const pastedText = event.clipboardData?.getData('text')
if (pastedText) {
const chips = pastedText
.split(/[,\n]/)
.map((chip) => chip.trim())
.filter(Boolean)
chips.forEach((chip) => {
currentInput.value = chip
addChip()
})
}
}
const focusInput = () => {
inputRef.value?.focus()
}
</script>

View File

@ -0,0 +1,187 @@
<template>
<ChatBubble :mapCharacter="props.mapCharacter" :currentX="currentPositionX" :currentY="currentPositionY" />
<Healthbar :mapCharacter="props.mapCharacter" :currentX="currentPositionX" :currentY="currentPositionY" />
<CharacterHair :mapCharacter="props.mapCharacter" :currentX="currentPositionX" :currentY="currentPositionY" />
<Sprite ref="charSprite" :depth="isometricDepth" :x="currentPositionX" :y="currentPositionY" :origin-y="1" :flipX="isFlippedX" />
</template>
<script lang="ts" setup>
import config from '@/application/config'
import { type MapCharacter } from '@/application/types'
import CharacterHair from '@/components/game/character/partials/CharacterHair.vue'
import ChatBubble from '@/components/game/character/partials/ChatBubble.vue'
import Healthbar from '@/components/game/character/partials/Healthbar.vue'
import { loadSpriteTextures } from '@/composables/gameComposable'
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/mapComposable'
import { CharacterTypeStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore'
import { useMapStore } from '@/stores/mapStore'
import { refObj, Sprite, useScene } from 'phavuer'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
enum Direction {
POSITIVE,
NEGATIVE,
UNCHANGED
}
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
mapCharacter: MapCharacter
}>()
const charSprite = refObj<Phaser.GameObjects.Sprite>()
const charSpriteId = ref('')
const gameStore = useGameStore()
const mapStore = useMapStore()
const scene = useScene()
const currentPositionX = ref(0)
const currentPositionY = ref(0)
const isometricDepth = ref(1)
const isInitialPosition = ref(true)
const tween = ref<Phaser.Tweens.Tween | null>(null)
const updateIsometricDepth = (positionX: number, positionY: number) => {
isometricDepth.value = calculateIsometricDepth(positionX, positionY, 28, 94, true)
}
const updatePosition = (positionX: number, positionY: number, direction: Direction) => {
const newPositionX = tileToWorldX(props.tilemap, positionX, positionY)
const newPositionY = tileToWorldY(props.tilemap, positionX, positionY)
if (isInitialPosition.value) {
currentPositionX.value = newPositionX
currentPositionY.value = newPositionY
isInitialPosition.value = false
return
}
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 currentDirection = computed(() => {
return [0, 6].includes(props.mapCharacter.character.rotation ?? 0) ? 'left_up' : 'right_down'
})
const currentAction = computed(() => {
return props.mapCharacter.isMoving ? 'walk' : 'idle'
})
const charTexture = computed(() => {
const spriteId = charSpriteId.value ?? 'idle_right_down'
return `${spriteId}-${currentAction.value}_${currentDirection.value}`
})
const updateSprite = () => {
if (!charSprite.value) return
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)
}
}
const handlePositionUpdate = (newValues: any, oldValues: any) => {
if (!newValues) return
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)
}
if (newValues.isMoving !== oldValues?.isMoving || newValues.rotation !== oldValues?.rotation) {
updateSprite()
}
}
watch(
() => ({
positionX: props.mapCharacter.character.positionX,
positionY: props.mapCharacter.character.positionY,
isMoving: props.mapCharacter.isMoving,
rotation: props.mapCharacter.character.rotation
}),
handlePositionUpdate
)
onMounted(async () => {
let character = props.mapCharacter.character
const characterTypeStorage = new CharacterTypeStorage()
const spriteId = await characterTypeStorage.getSpriteId(character.characterType!)
if (!spriteId) return
charSpriteId.value = spriteId
await loadSpriteTextures(scene, spriteId)
if (charSprite.value) {
charSprite.value.setTexture(charTexture.value)
charSprite.value.setFlipX(isFlippedX.value)
charSprite.value.setName(props.mapCharacter.character.name)
}
if (character.id === gameStore.character!.id) {
mapStore.setCharacterLoaded(true)
// #146 : Set camera position to character, need to be improved still
scene.cameras.main.startFollow(charSprite.value as Phaser.GameObjects.Sprite)
}
updatePosition(character.positionX, character.positionY, character.rotation)
})
onUnmounted(() => {
tween.value?.stop()
})
</script>

View File

@ -0,0 +1,51 @@
<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

@ -0,0 +1,58 @@
<template>
<Image v-bind="imageProps" v-if="gameStore.isTextureLoaded(texture)" />
</template>
<script lang="ts" setup>
import type { MapCharacter, Sprite as SpriteT } from '@/application/types'
import { loadSpriteTextures } from '@/composables/gameComposable'
import { CharacterHairStorage, CharacterTypeStorage, SpriteStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore'
import { Image, useScene } from 'phavuer'
import { computed, onMounted, ref } from 'vue'
const props = defineProps<{
mapCharacter: MapCharacter
currentX: number
currentY: number
}>()
const gameStore = useGameStore()
const scene = useScene()
const hairSpriteId = ref('')
const sprite = ref<SpriteT | null>(null)
const texture = computed(() => {
const { rotation } = props.mapCharacter.character
const direction = [0, 6].includes(rotation) ? 'back' : 'front'
return `${hairSpriteId.value}-${direction}`
})
const isFlippedX = computed(() => [6, 4].includes(props.mapCharacter.character.rotation ?? 0))
const imageProps = computed(() => {
const direction = [0, 6].includes(props.mapCharacter.character.rotation ?? 0) ? 'back' : 'front'
const spriteAction = sprite.value?.spriteActions?.find((spriteAction) => spriteAction.action === direction)
return {
depth: 9999,
originX: Number(spriteAction?.originX) ?? 0,
originY: Number(spriteAction?.originY) ?? 0,
flipX: isFlippedX.value,
texture: texture.value,
y: props.currentY,
x: props.currentX
}
})
onMounted(async () => {
const characterHairStorage = new CharacterHairStorage()
const spriteId = await characterHairStorage.getSpriteId(props.mapCharacter.character.characterHair!)
if (!spriteId) return
hairSpriteId.value = spriteId
const spriteStorage = new SpriteStorage()
sprite.value = await spriteStorage.get(spriteId)
await loadSpriteTextures(scene, spriteId)
})
</script>

View File

@ -0,0 +1,47 @@
<template>
<Container ref="charChatContainer" :depth="999" :x="currentX" :y="currentY">
<RoundRectangle @create="createChatBubble" :origin-x="0.5" :origin-y="7.5" :fillColor="0xffffff" :width="194" :height="21" :radius="20" />
<Text @create="createChatText" :style="{ fontSize: 13, fontFamily: 'Arial', color: '#000' }" />
</Container>
</template>
<script setup lang="ts">
import type { MapCharacter } from '@/application/types'
import { Container, refObj, RoundRectangle, Text, useGame } from 'phavuer'
import { onMounted } from 'vue'
const props = defineProps<{
mapCharacter: MapCharacter
currentX: number
currentY: number
}>()
const game = useGame()
const charChatContainer = refObj<Phaser.GameObjects.Container>()
const createChatBubble = (container: Phaser.GameObjects.Container) => {
container.setName(`${props.mapCharacter.character.name}_chatBubble`)
}
const createChatText = (text: Phaser.GameObjects.Text) => {
text.setName(`${props.mapCharacter.character.name}_chatText`)
text.setFontSize(13)
text.setFontFamily('Arial')
text.setOrigin(0.5, 10.9)
text.setResolution(2)
// Fix text alignment on Windows and Android
if (game.device.os.windows || game.device.os.android) {
text.setOrigin(0.5, 9.75)
if (game.device.browser.firefox) {
text.setOrigin(0.5, 10.9)
}
}
}
onMounted(() => {
charChatContainer.value!.setName(`${props.mapCharacter.character!.name}_chatContainer`)
charChatContainer.value!.setVisible(false)
})
</script>

View File

@ -0,0 +1,36 @@
<template>
<Container :depth="999" :x="currentX" :y="currentY">
<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="36.4" :fillColor="0x00b3b3" :width="70" :height="3" :radius="5" />
</Container>
</template>
<script setup lang="ts">
import type { MapCharacter } from '@/application/types'
import { Container, RoundRectangle, Text, useGame } from 'phavuer'
const props = defineProps<{
mapCharacter: MapCharacter
currentX: number
currentY: number
}>()
const game = useGame()
const createNicknameText = (text: Phaser.GameObjects.Text) => {
text.setFontSize(13)
text.setFontFamily('Arial')
text.setOrigin(0.5, 9)
text.setResolution(2)
// Fix text alignment on Windows and Android
if (game.device.os.windows || game.device.os.android) {
text.setOrigin(0.5, 8)
if (game.device.browser.firefox) {
text.setOrigin(0.5, 9)
}
}
}
</script>

View File

@ -0,0 +1,173 @@
<template>
<div class="absolute" v-if="gameStore.uiSettings.isCharacterProfileOpen" :style="modalStyle">
<div class="relative">
<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)]" alt="" />
<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>
<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 draggable="false" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" alt="Close button icon" />
</button>
</div>
<div class="py-4 px-6 flex flex-col gap-7 relative z-10">
<div class="flex flex-col gap-2.5">
<div class="flex justify-between">
<div>
<p class="text-sm m-0 font-bold text-white tracking-wide">{{ gameStore.character?.name }}</p>
<span class="text-xs">{{ gameStore.character?.experience }} / 18.600XP</span>
</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]">
<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>
</div>
<div class="flex justify-between">
<div class="flex flex-col gap-0.5">
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
<span class="absolute w-full top-1/2 -translate-y-1/2 text-[6px] text-center">CROWN</span>
</div>
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
<span class="absolute w-full top-1/2 -translate-y-1/2 text-[6px] text-center">R-HAND</span>
</div>
<div class="flex gap-0.5 items-end">
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
<span class="absolute w-full top-1/2 -translate-y-1/2 text-[6px] text-center">L-HAND</span>
</div>
<div class="w-6 h-6 default-border rounded-sm bg-gray relative hover:bg-gray-600">
<span class="absolute w-full top-1/2 -translate-y-1/2 text-[6px] text-center">RING</span>
</div>
</div>
</div>
<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="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" alt="Helmet icon" />
</div>
<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" alt="Chestplate icon" />
</div>
<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">
<img class="w-4 h-4 center-element" src="/assets/icons/profile/boots.svg" alt="Boots icon" />
</div>
<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" alt="Legs icon" />
</div>
</div>
</div>
</div>
<div class="flex justify-between">
<div class="flex flex-col">
<div class="w-[105px] h-px mb-[3px] flex justify-between">
<div class="w-px h-full bg-gray-300"></div>
<div class="w-[101px] h-full bg-gray-300"></div>
<div class="w-px h-full bg-gray-300"></div>
</div>
<div class="flex gap-11">
<p class="m-0 text-xs text-white tracking-wide">Health</p>
<span class="m-0 text-xs text-white tracking-wide">+ 15</span>
</div>
<div class="w-[105px] h-px my-[3px] flex justify-between">
<div class="w-px h-full bg-gray-300"></div>
<div class="w-[101px] h-full bg-gray-300"></div>
<div class="w-px h-full bg-gray-300"></div>
</div>
<div class="flex gap-11">
<p class="m-0 text-xs text-white tracking-wide">Health</p>
<span class="m-0 text-xs text-white tracking-wide">+ 15</span>
</div>
<div class="w-[105px] h-px mt-[3px] flex justify-between">
<div class="w-px h-full bg-gray-300"></div>
<div class="w-[101px] h-full bg-gray-300"></div>
<div class="w-px h-full bg-gray-300"></div>
</div>
</div>
<div class="flex flex-col">
<div class="w-[105px] h-px mb-[3px] flex justify-between">
<div class="w-px h-full bg-gray-300"></div>
<div class="w-[101px] h-full bg-gray-300"></div>
<div class="w-px h-full bg-gray-300"></div>
</div>
<div class="flex gap-11">
<p class="m-0 text-xs text-white tracking-wide">Health</p>
<span class="m-0 text-xs text-white tracking-wide">+ 15</span>
</div>
<div class="w-[105px] h-px my-[3px] flex justify-between">
<div class="w-px h-full bg-gray-300"></div>
<div class="w-[101px] h-full bg-gray-300"></div>
<div class="w-px h-full bg-gray-300"></div>
</div>
<div class="flex gap-11">
<p class="m-0 text-xs text-white tracking-wide">Health</p>
<span class="m-0 text-xs text-white tracking-wide">+ 15</span>
</div>
<div class="w-[105px] h-px mt-[3px] flex justify-between">
<div class="w-px h-full bg-gray-300"></div>
<div class="w-[101px] h-full bg-gray-300"></div>
<div class="w-px h-full bg-gray-300"></div>
</div>
</div>
</div>
</div>
<div class="grid grid-rows-4 grid-cols-6 gap-0.5">
<div v-for="n in 24" class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600"></div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useGameStore } from '@/stores/gameStore'
import { computed, onMounted, onUnmounted, ref } from 'vue'
const gameStore = useGameStore()
const width = ref(286)
const height = ref(483)
const x = ref(window.innerWidth / 2 - 143)
const y = ref(window.innerHeight / 2 - 241)
const isDragging = ref(false)
const modalStyle = computed(() => ({
top: `${y.value}px`,
left: `${x.value}px`,
width: `${width.value}px`,
height: `${height.value}px`
}))
function startDrag(event: MouseEvent) {
isDragging.value = true
const startX = event.clientX - x.value
const startY = event.clientY - y.value
function drag(event: MouseEvent) {
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 keyPress(event: KeyboardEvent) {
if (event.altKey && event.key === 'c') {
gameStore.toggleCharacterProfile()
}
}
onMounted(() => {
addEventListener('keydown', keyPress)
})
onUnmounted(() => {
removeEventListener('keydown', keyPress)
})
</script>

View File

@ -7,36 +7,55 @@
</div>
</div>
<div class="w-96 mx-auto relative">
<img src="/assets/icons/chat-icon.svg" class="absolute top-1/2 -translate-y-1/2 left-2.5 h-4 w-4 opacity-50" />
<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
class="w-[332px] h-8 rounded-sm text-xs font-default pl-8 pr-4 py-0 bg-gray-600 border-2 border-solid border-gray-500 text-gray-300 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover focus:outline-none focus:ring-0 focus:border-cyan-800"
placeholder="Type something..."
v-model="message"
@keypress="handleKeyPress"
@submit="handleSubmit"
ref="chatInput"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, nextTick } from 'vue'
import type { Chat } from '@/application/types'
import { useGameStore } from '@/stores/gameStore'
import type { Character, ChatMessage } from '@/types'
import { useZoneStore } from '@/stores/zoneStore'
import { useMapStore } from '@/stores/mapStore'
import { onClickOutside, useFocus } from '@vueuse/core'
import { useScene } from 'phavuer'
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
const scene = useScene()
const gameStore = useGameStore()
const zoneStore = useZoneStore()
const mapStore = useMapStore()
const message = ref('')
const chats = ref([] as ChatMessage[])
const chats = ref([] as Chat[])
const chatWindow = ref<HTMLElement | null>(null)
const chatInput = ref<HTMLElement | null>(null)
const { focused } = useFocus(chatInput)
function focusChat(event: KeyboardEvent) {
if (event.key === 'Enter' && !focused.value) {
focused.value = true
}
}
onClickOutside(chatInput, (event) => unfocusChat(event, chatInput.value as HTMLElement))
function unfocusChat(event: Event, targetElement: HTMLElement) {
if (!(event.target instanceof Node) || !targetElement.contains(event.target)) {
targetElement.blur()
}
}
const sendMessage = () => {
if (!message.value.trim()) return
gameStore.connection?.emit('chat:send_message', { message: message.value }, (response: boolean) => {})
gameStore.connection?.emit('chat:message', { message: message.value }, (response: boolean) => {})
message.value = ''
}
@ -60,11 +79,11 @@ const scrollToBottom = () => {
})
}
gameStore.connection?.on('chat:message', (data: ChatMessage) => {
gameStore.connection?.on('chat:message', (data: Chat) => {
chats.value.push(data)
scrollToBottom()
if (!zoneStore.characterLoaded) return
if (!mapStore.characterLoaded) return
const charChatContainer = scene.children.getByName(data.character.name + '_chatContainer') as Phaser.GameObjects.Container
if (!charChatContainer) return
@ -117,7 +136,12 @@ gameStore.connection?.on('chat:message', (data: ChatMessage) => {
})
scrollToBottom()
onMounted(() => {
addEventListener('keydown', focusChat)
})
onBeforeUnmount(() => {
gameStore.connection?.off('chat:message')
removeEventListener('keydown', focusChat)
})
</script>

View File

@ -0,0 +1,21 @@
<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

@ -2,44 +2,44 @@
<div class="absolute top-4 left-[300px] w-[422px]">
<div class="flex gap-2.5">
<div class="relative">
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-frame-empty.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
<span class="z-10 text-pixel absolute top-1 left-2">F1</span>
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f1-icon.png')] bg-no-repeat"></div>
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/ingameUI/f1-icon.png')] bg-no-repeat"></div>
</div>
<div class="relative">
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-frame-empty.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
<span class="z-10 text-pixel absolute top-1 left-2">F2</span>
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f2-icon.png')] bg-no-repeat"></div>
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/ingameUI/f2-icon.png')] bg-no-repeat"></div>
</div>
<div class="relative">
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-frame-empty.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
<span class="z-10 text-pixel absolute top-1 left-2">F3</span>
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f3-icon.png')] bg-no-repeat"></div>
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/ingameUI/f3-icon.png')] bg-no-repeat"></div>
</div>
<div class="relative">
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-frame-empty.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
<span class="z-10 text-pixel absolute top-1 left-2">F4</span>
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f4-icon.png')] bg-no-repeat"></div>
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/ingameUI/f4-icon.png')] bg-no-repeat"></div>
</div>
<div class="relative">
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-frame-empty.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
<span class="z-10 text-pixel absolute top-1 left-2">F5</span>
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f5-icon.png')] bg-no-repeat"></div>
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/ingameUI/f5-icon.png')] bg-no-repeat"></div>
</div>
<div class="relative">
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-frame-empty.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
<span class="z-10 text-pixel absolute top-1 left-2">F6</span>
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f6-icon.png')] bg-no-repeat"></div>
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/ingameUI/f6-icon.png')] bg-no-repeat"></div>
</div>
<div class="relative">
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-frame-empty.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
<span class="z-10 text-pixel absolute top-1 left-2">F7</span>
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f7-icon.png')] bg-no-repeat"></div>
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/ingameUI/f7-icon.png')] bg-no-repeat"></div>
</div>
<div class="relative">
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-frame-empty.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
<span class="z-10 text-pixel absolute top-1 left-2">F8</span>
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f8-icon.png')] bg-no-repeat"></div>
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/ingameUI/f8-icon.png')] bg-no-repeat"></div>
</div>
</div>
</div>

View File

@ -1,5 +1,5 @@
<template>
<div class="absolute left-[66px] top-4 bg-[url('/assets/ui-elements/ui-rect-border-4-corners.svg')] bg-no-repeat px-4 py-2 w-[181px] h-[26px] flex flex-col justify-between">
<div class="absolute left-[66px] top-4 bg-[url('/assets/ui-elements/hud-ui-box.svg')] bg-no-repeat px-4 py-2 w-[181px] h-[26px] flex flex-col justify-between">
<div class="w-full flex items-center gap-2">
<label class="text-xs leading-3 text-pixel" for="hp">HP</label>
<progress class="h-2 rounded-sm w-full max-w-44 appearance-none accent-green" id="hp" :value="gameStore.character?.hitpoints" max="100">{{ gameStore.character?.hitpoints }}%</progress>

View File

@ -5,8 +5,8 @@
<p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">Open menu</p>
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
</div>
<a class="group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners.svg')] bg-no-repeat block w-[42px] h-[42px] relative">
<img class="group-hover:drop-shadow-default w-6 h-5 mx-[9px] my-[11px] object-contain" draggable="false" src="/assets/icons/menu-icon.svg" />
<a 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" alt="Menu button icon" />
</a>
</li>
<li class="menu-item group relative" @click="gameStore.toggleCharacterProfile">
@ -14,8 +14,8 @@
<p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">User Profile</p>
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
</div>
<a class="group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners.svg')] bg-no-repeat block w-[42px] h-[42px] relative">
<img class="group-hover:drop-shadow-default w-8 h-8 m-[5px] object-contain" draggable="false" src="/assets/avatar/default/head.png" />
<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" 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>
</a>
</li>
@ -24,8 +24,8 @@
<p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">Open Chat</p>
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
</div>
<a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border border-b-4 border-solid rounded border-gray-500 block w-[34px] h-[31px]">
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/chat-icon.svg" />
<a 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" alt="Open chat button icon" />
</a>
</li>
<li class="menu-item group relative">
@ -33,8 +33,8 @@
<p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">World map</p>
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
</div>
<a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border border-b-4 border-solid rounded border-gray-500 block w-[34px] h-[31px]">
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/map-icon.svg" />
<a 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" alt="World map button icon" />
</a>
</li>
<li class="menu-item group relative">
@ -42,8 +42,8 @@
<p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">Users</p>
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
</div>
<a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border border-b-4 border-solid rounded border-gray-500 block w-[34px] h-[31px]">
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/socials-icon.svg" />
<a 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" alt="Users button icon" />
</a>
</li>
</ul>

View File

@ -0,0 +1,23 @@
<template>
<div class="absolute top-4 right-4 hidden lg:block">
<div class="w-40 h-40 rounded-full default-border bg-[url('/assets/ui-texture.png')] bg-no-repeat">
<div class="w-40 h-40 rounded-full shadow-inner"></div>
</div>
<div class="absolute -bottom-3 left-1/2 -translate-x-1/2 flex gap-1">
<button class="w-6 h-6 relative p-0">
<img class="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" alt=""/>
</button>
<button class="w-6 h-6 relative p-0">
<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" alt=""/>
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { useGameStore } from '@/stores/gameStore'
const gameStore = useGameStore()
</script>

View File

@ -0,0 +1,14 @@
<template>
<Character v-for="item in mapStore.characters" :key="item.character.id" :tilemap="tilemap" :mapCharacter="item" />
</template>
<script setup lang="ts">
import Character from '@/components/game/character/Character.vue'
import { useMapStore } from '@/stores/mapStore'
const mapStore = useMapStore()
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
}>()
</script>

View File

@ -0,0 +1,46 @@
<template>
<MapTiles :key="mapStore.mapId" @tileMap:create="tileMap = $event" />
<PlacedMapObjects v-if="tileMap" :key="mapStore.mapId" :tilemap="tileMap" />
<Characters v-if="tileMap && mapStore.characters" :tilemap="tileMap" />
</template>
<script setup lang="ts">
import type { MapCharacter, mapLoadData, UUID } from '@/application/types'
import Characters from '@/components/game/map/Characters.vue'
import MapTiles from '@/components/game/map/MapTiles.vue'
import PlacedMapObjects from '@/components/game/map/PlacedMapObjects.vue'
import { useGameStore } from '@/stores/gameStore'
import { useMapStore } from '@/stores/mapStore'
import { onUnmounted, shallowRef } from 'vue'
const gameStore = useGameStore()
const mapStore = useMapStore()
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
// Event listeners
gameStore.connection?.on('map:character:teleport', async (data: mapLoadData) => {
mapStore.setMapId(data.mapId)
mapStore.setCharacters(data.characters)
})
gameStore.connection?.on('map:character:join', async (data: MapCharacter) => {
mapStore.addCharacter(data)
})
gameStore.connection?.on('map:character:leave', (characterId: UUID) => {
mapStore.removeCharacter(characterId)
})
gameStore.connection?.on('map:character:move', (data: { characterId: UUID; positionX: number; positionY: number; rotation: number; isMoving: boolean }) => {
mapStore.updateCharacterPosition(data)
})
onUnmounted(() => {
mapStore.reset()
gameStore.connection?.off('map:character:teleport')
gameStore.connection?.off('map:character:join')
gameStore.connection?.off('map:character:leave')
gameStore.connection?.off('map:character:move')
})
</script>

View File

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

View File

@ -0,0 +1,28 @@
<template>
<PlacedMapObject v-for="placedMapObject in items" :tilemap="tilemap" :placedMapObject />
</template>
<script setup lang="ts">
import type { PlacedMapObject as PlacedMapObjectT } from '@/application/types'
import PlacedMapObject from '@/components/game/map/partials/PlacedMapObject.vue'
import { MapStorage } from '@/storage/storages'
import { useMapStore } from '@/stores/mapStore'
import { onMounted, ref } from 'vue'
defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
}>()
const mapStore = useMapStore()
const mapStorage = new MapStorage()
const items = ref<PlacedMapObjectT[]>([])
onMounted(async () => {
if (!mapStore.mapId) return
const map = await mapStorage.get(mapStore.mapId)
if (!map) return
items.value = map.placedMapObjects
})
</script>

View File

@ -0,0 +1,43 @@
<template>
<Image v-if="gameStore.isTextureLoaded(props.placedMapObject.mapObject.id)" v-bind="imageProps" />
</template>
<script setup lang="ts">
import type { PlacedMapObject, TextureData } from '@/application/types'
import { loadTexture } from '@/composables/gameComposable'
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/mapComposable'
import { useGameStore } from '@/stores/gameStore'
import { Image, useScene } from 'phavuer'
import { computed, onMounted } from 'vue'
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
placedMapObject: PlacedMapObject
}>()
const gameStore = useGameStore()
const scene = useScene()
const imageProps = computed(() => ({
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)
})
onMounted(async () => {})
</script>

View File

@ -1,11 +1,12 @@
<template>
<Modal :isModalOpen="gameStore.uiSettings.isGmPanelOpen" @modal:close="() => gameStore.toggleGmPanel()" :modal-width="1000" :modal-height="650" :can-full-screen="true">
<Modal :isModalOpen="gameStore.uiSettings.isGmPanelOpen" @modal:close="() => gameStore.toggleGmPanel()" :modal-width="1000" :modal-height="650" :is-full-screen="true" :bg-style="'dark'">
<template #modalHeader>
<div class="flex gap-1.5 flex-wrap">
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">General</button>
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Users</button>
<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 class="btn-cyan py-1.5 px-4 min-w-24" type="button" @click="$emit('open-map-editor')">Map editor</button>
</div>
</template>
<template #modalBody>
@ -17,12 +18,15 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import Modal from '@/components/utilities/Modal.vue'
import AssetManager from '@/components/gameMaster/assetManager/AssetManager.vue'
import Modal from '@/components/utilities/Modal.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { useGameStore } from '@/stores/gameStore'
import { ref } from 'vue'
defineEmits(['open-map-editor'])
const gameStore = useGameStore()
const mapEditor = useMapEditorComposable()
let toggle = ref('asset-manager')
</script>

View File

@ -1,44 +0,0 @@
<template>
<Modal :isModalOpen="true" :closable="false" :is-resizable="false" :modal-width="modalWidth" :modal-height="modalHeight" :modal-position-x="posXY.x" :modal-position-y="posXY.y">
<template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">GM tools</h3>
</template>
<template #modalBody>
<div class="content flex flex-col gap-2.5 m-4 h-20">
<button class="btn-cyan py-1.5 px-4 w-full" type="button" @click="gameStore.toggleGmPanel()">Toggle GM panel</button>
<button class="btn-cyan py-1.5 px-4 w-full" type="button" @click="() => zoneEditorStore.toggleActive()">Zone manager</button>
</div>
</template>
</Modal>
</template>
<script setup lang="ts">
import Modal from '@/components/utilities/Modal.vue'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { useGameStore } from '@/stores/gameStore'
import { onMounted, ref } from 'vue'
const zoneEditorStore = useZoneEditorStore()
const gameStore = useGameStore()
const modalWidth = ref(200)
const modalHeight = ref(170)
let posXY = ref({ x: 0, y: 0 })
onMounted(() => {
window.addEventListener('resize', () => {
posXY.value = customPositionGmPanel(modalWidth.value)
})
})
const customPositionGmPanel = (modalWidth: number) => {
const padding = 25
const width = window.innerWidth
const x = width - (modalWidth + 4) - 25
const y = padding
return { x, y }
}
posXY.value = customPositionGmPanel(modalWidth.value)
</script>

View File

@ -1,85 +1,79 @@
<template>
<div class="flex h-full w-full relative">
<div class="w-2/12 flex flex-col relative overflow-auto">
<div class="flex gap-4 h-[calc(100%_-_32px)] w-[calc(100%_-_32px)] relative m-4">
<div class="w-2/12 flex flex-col relative overflow-auto rounded-md default-border bg-gray p-2.5">
<!-- Asset Categories -->
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'tiles' }" @click="() => (selectedCategory = 'tiles')">
<span :class="{ 'text-white': selectedCategory === 'tiles' }">Tiles</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'tiles' }" @click="() => (selectedCategory = 'tiles')">
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'tiles' }">Tiles</span>
</a>
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'objects' }" @click="() => (selectedCategory = 'objects')">
<span :class="{ 'text-white': selectedCategory === 'objects' }">Objects</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'map_objects' }" @click="() => (selectedCategory = 'map_objects')">
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'map_objects' }">Map objects</span>
</a>
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'sprites' }" @click="() => (selectedCategory = 'sprites')">
<span :class="{ 'text-white': selectedCategory === 'sprites' }">Sprites</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'sprites' }" @click="() => (selectedCategory = 'sprites')">
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'sprites' }">Sprites</span>
</a>
<a class="relative p-2.5 hover:cursor-pointer">
<span>Items</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'items' }" @click="() => (selectedCategory = 'items')">
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'items' }">Items</span>
</a>
<a class="relative p-2.5 hover:cursor-pointer">
<span>NPC's</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group">
<span class="group-hover:text-white">NPC's</span>
</a>
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'shops' }" @click="() => (selectedCategory = 'shops')">
<span :class="{ 'text-white': selectedCategory === 'shops' }">Shops</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan/80': selectedCategory === 'shops' }" @click="() => (selectedCategory = 'shops')">
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'shops' }">Shops</span>
</a>
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'characterTypes' }" @click="() => (selectedCategory = 'characterTypes')">
<span :class="{ 'text-white': selectedCategory === 'characterTypes' }">Character types</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan/80': selectedCategory === 'characterTypes' }" @click="() => (selectedCategory = 'characterTypes')">
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'characterTypes' }">Character types</span>
</a>
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'characterHair' }" @click="() => (selectedCategory = 'characterHair')">
<span :class="{ 'text-white': selectedCategory === 'characterHair' }">Character hair</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan/80': selectedCategory === 'characterHair' }" @click="() => (selectedCategory = 'characterHair')">
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'characterHair' }">Character hair</span>
</a>
<a class="relative p-2.5 hover:cursor-pointer">
<span>Mounts</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group">
<span class="group-hover:text-white">Mounts</span>
</a>
<a class="relative p-2.5 hover:cursor-pointer">
<span>Pets</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group">
<span class="group-hover:text-white">Pets</span>
</a>
<a class="relative p-2.5 hover:cursor-pointer">
<span>Emoticons</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group">
<span class="group-hover:text-white">Emoticons</span>
</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-4/12 flex flex-col relative">
<TileList v-if="selectedCategory === 'tiles'" />
<ObjectList v-if="selectedCategory === 'objects'" />
<MapObjectList v-if="selectedCategory === 'map_objects'" />
<SpriteList v-if="selectedCategory === 'sprites'" />
<ItemList v-if="selectedCategory === 'items'" />
<CharacterTypeList v-if="selectedCategory === 'characterTypes'" />
<CharacterHairList v-if="selectedCategory === 'characterHair'" />
</div>
<div class="absolute w-px bg-gray-500 h-full top-0 left-1/2"></div>
<!-- Asset details -->
<div class="flex w-1/2 after:hidden flex-col relative overflow-auto">
<div class="flex w-7/12 after:hidden flex-col relative overflow-auto">
<TileDetails v-if="selectedCategory === 'tiles' && assetManagerStore.selectedTile" />
<ObjectDetails v-if="selectedCategory === 'objects' && assetManagerStore.selectedObject" />
<MapObjectDetails v-if="selectedCategory === 'map_objects' && assetManagerStore.selectedMapObject" />
<SpriteDetails v-if="selectedCategory === 'sprites' && assetManagerStore.selectedSprite" />
<ItemDetails v-if="selectedCategory === 'items' && assetManagerStore.selectedItem" />
<CharacterTypeDetails v-if="selectedCategory === 'characterTypes' && assetManagerStore.selectedCharacterType" />
<CharacterHairDetails v-if="selectedCategory === 'characterHair' && assetManagerStore.selectedCharacterHair" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import TileList from '@/components/gameMaster/assetManager/partials/tile/TileList.vue'
import TileDetails from '@/components/gameMaster/assetManager/partials/tile/TileDetails.vue'
import ObjectList from '@/components/gameMaster/assetManager/partials/object/ObjectList.vue'
import ObjectDetails from '@/components/gameMaster/assetManager/partials/object/ObjectDetails.vue'
import SpriteList from '@/components/gameMaster/assetManager/partials/sprite/SpriteList.vue'
import SpriteDetails from '@/components/gameMaster/assetManager/partials/sprite/SpriteDetails.vue'
import CharacterTypeList from '@/components/gameMaster/assetManager/partials/characterType/CharacterTypeList.vue'
import CharacterHairDetails from '@/components/gameMaster/assetManager/partials/characterHair/CharacterHairDetails.vue'
import CharacterHairList from '@/components/gameMaster/assetManager/partials/characterHair/CharacterHairList.vue'
import CharacterTypeDetails from '@/components/gameMaster/assetManager/partials/characterType/CharacterTypeDetails.vue'
import CharacterTypeList from '@/components/gameMaster/assetManager/partials/characterType/CharacterTypeList.vue'
import ItemDetails from '@/components/gameMaster/assetManager/partials/item/itemDetails.vue'
import ItemList from '@/components/gameMaster/assetManager/partials/item/itemList.vue'
import MapObjectDetails from '@/components/gameMaster/assetManager/partials/mapObject/MapObjectDetails.vue'
import MapObjectList from '@/components/gameMaster/assetManager/partials/mapObject/MapObjectList.vue'
import SpriteDetails from '@/components/gameMaster/assetManager/partials/sprite/SpriteDetails.vue'
import SpriteList from '@/components/gameMaster/assetManager/partials/sprite/SpriteList.vue'
import TileDetails from '@/components/gameMaster/assetManager/partials/tile/TileDetails.vue'
import TileList from '@/components/gameMaster/assetManager/partials/tile/TileList.vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { ref } from 'vue'
const assetManagerStore = useAssetManagerStore()
const selectedCategory = ref('tiles')

View File

@ -0,0 +1,124 @@
<template>
<div class="h-full overflow-auto">
<div class="p-2.5 block rounded-md default-border bg-gray">
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveCharacterHair">
<div class="form-field-full">
<label for="name">Name</label>
<input v-model="characterName" class="input-field" type="text" name="name" placeholder="Character Type Name" />
</div>
<div class="form-field-full">
<label for="gender">Gender</label>
<select v-model="characterGender" class="input-field" name="gender">
<option v-for="gender in genderOptions" :key="gender" :value="gender">{{ gender }}</option>
</select>
</div>
<div class="form-field-full">
<label for="isSelectable">Is selectable</label>
<select v-model="characterIsSelectable" class="input-field" name="isSelectable">
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>
</div>
<div class="form-field-full">
<label for="spriteId">Sprite</label>
<select v-model="characterSpriteId" class="input-field" name="spriteId">
<option disabled selected value="">Select sprite</option>
<option v-for="sprite in assetManagerStore.spriteList" :key="sprite.id" :value="sprite.id">{{ sprite.name }}</option>
</select>
</div>
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeCharacterHair">Remove</button>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import type { CharacterGender, CharacterHair, Sprite } from '@/application/types'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore()
const selectedCharacterHair = computed(() => assetManagerStore.selectedCharacterHair)
const characterName = ref('')
const characterGender = ref<CharacterGender>('MALE' as CharacterGender.MALE)
const characterIsSelectable = ref<boolean>(false)
const characterSpriteId = ref<string | null | undefined>(null)
const genderOptions: CharacterGender[] = ['MALE' as CharacterGender.MALE, 'FEMALE' as CharacterGender.FEMALE]
if (!selectedCharacterHair.value) {
console.error('No character hair selected')
}
if (selectedCharacterHair.value) {
characterName.value = selectedCharacterHair.value.name
characterGender.value = selectedCharacterHair.value.gender
characterIsSelectable.value = selectedCharacterHair.value.isSelectable
characterSpriteId.value = selectedCharacterHair.value.sprite?.id
}
function removeCharacterHair() {
if (!selectedCharacterHair.value) return
gameStore.connection?.emit('gm:characterHair:remove', { id: selectedCharacterHair.value.id }, (response: boolean) => {
if (!response) {
console.error('Failed to remove character hair')
return
}
refreshCharacterHairList()
})
}
function refreshCharacterHairList(unsetSelectedCharacterHair = true) {
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => {
assetManagerStore.setCharacterHairList(response)
if (unsetSelectedCharacterHair) {
assetManagerStore.setSelectedCharacterHair(null)
}
})
}
function saveCharacterHair() {
const characterHairData = {
id: selectedCharacterHair.value!.id,
name: characterName.value,
gender: characterGender.value,
isSelectable: characterIsSelectable.value,
spriteId: characterSpriteId.value
}
gameStore.connection?.emit('gm:characterHair:update', characterHairData, (response: boolean) => {
if (!response) {
console.error('Failed to save character type')
return
}
refreshCharacterHairList(false)
})
}
watch(selectedCharacterHair, (characterHair: CharacterHair | null) => {
if (!characterHair) return
characterName.value = characterHair.name
characterGender.value = characterHair.gender
characterIsSelectable.value = characterHair.isSelectable
characterSpriteId.value = characterHair.sprite?.id
})
onMounted(() => {
if (!selectedCharacterHair.value) return
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
assetManagerStore.setSpriteList(response)
})
})
onBeforeUnmount(() => {
assetManagerStore.setSelectedCharacterHair(null)
})
</script>

View File

@ -0,0 +1,99 @@
<template>
<div class="relative mb-5 flex items-center gap-x-2.5">
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
<label for="create-character" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2.5 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
<button class="p-0 h-5" id="create-character" @click="createNewCharacterHair">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="white">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
</button>
</label>
</div>
<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">
<a
v-for="{ data: characterHair } in list"
:key="characterHair.id"
class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group"
:class="{ 'bg-cyan': assetManagerStore.selectedCharacterHair?.id === characterHair.id }"
@click="assetManagerStore.setSelectedCharacterHair(characterHair as CharacterHair)"
>
<div class="flex items-center gap-2.5">
<span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedCharacterHair?.id === characterHair.id }">{{ characterHair.name }}</span>
</div>
</a>
</div>
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" />
</button>
</div>
</div>
</template>
<script setup lang="ts">
import type { CharacterHair } from '@/application/types'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore'
import { useVirtualList } from '@vueuse/core'
import { computed, onMounted, ref } from 'vue'
const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore()
const searchQuery = ref('')
const hasScrolled = ref(false)
const elementToScroll = ref()
const handleSearch = () => {
// Trigger a re-render of the virtual list
virtualList.value?.scrollTo(0)
}
const createNewCharacterHair = () => {
gameStore.connection?.emit('gm:characterHair:create', {}, (response: boolean) => {
if (!response) {
console.error('Failed to create new character type')
return
}
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => {
assetManagerStore.setCharacterHairList(response)
})
})
}
const filteredCharacterHairs = computed(() => {
if (!searchQuery.value) {
return assetManagerStore.characterHairList
}
return assetManagerStore.characterHairList.filter((character) => character.name.toLowerCase().includes(searchQuery.value.toLowerCase()))
})
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(filteredCharacterHairs, {
itemHeight: 48
})
const virtualList = ref({ scrollTo })
const onScroll = () => {
let scrollTop = elementToScroll.value.style.marginTop.replace('px', '')
if (scrollTop > 80) {
hasScrolled.value = true
} else if (scrollTop <= 80) {
hasScrolled.value = false
}
}
function toTop() {
virtualList.value?.scrollTo(0)
}
onMounted(() => {
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => {
assetManagerStore.setCharacterHairList(response)
})
})
</script>

View File

@ -1,6 +1,6 @@
<template>
<div class="h-full overflow-auto">
<div class="m-2.5 p-2.5 block">
<div class="p-2.5 block rounded-md default-border bg-gray">
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveCharacterType">
<div class="form-field-full">
<label for="name">Name</label>
@ -19,8 +19,18 @@
</select>
</div>
<div class="form-field-full">
<label for="spriteId">Sprite ID</label>
<input v-model="characterSpriteId" class="input-field" type="text" name="spriteId" placeholder="Sprite ID" />
<label for="isSelectable">Is selectable</label>
<select v-model="characterIsSelectable" class="input-field" name="isSelectable">
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>
</div>
<div class="form-field-full">
<label for="spriteId">Sprite</label>
<select v-model="characterSpriteId" class="input-field" name="spriteId">
<option disabled selected value="">Select sprite</option>
<option v-for="sprite in assetManagerStore.spriteList" :key="sprite.id" :value="sprite.id">{{ sprite.name }}</option>
</select>
</div>
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeCharacterType">Remove</button>
@ -30,10 +40,10 @@
</template>
<script setup lang="ts">
import type { CharacterType, CharacterGender, CharacterRace } from '@/types'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import type { CharacterGender, CharacterRace, CharacterType, Sprite } from '@/application/types'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore()
@ -43,7 +53,8 @@ const selectedCharacterType = computed(() => assetManagerStore.selectedCharacter
const characterName = ref('')
const characterGender = ref<CharacterGender>('MALE' as CharacterGender.MALE)
const characterRace = ref<CharacterRace>('HUMAN' as CharacterRace.HUMAN)
const characterSpriteId = ref('')
const characterIsSelectable = ref<boolean>(false)
const characterSpriteId = ref<string | null | undefined>(null)
const genderOptions: CharacterGender[] = ['MALE' as CharacterGender.MALE, 'FEMALE' as CharacterGender.FEMALE]
const raceOptions: CharacterRace[] = ['HUMAN' as CharacterRace.HUMAN, 'ELF' as CharacterRace.ELF, 'DWARF' as CharacterRace.DWARF, 'ORC' as CharacterRace.ORC, 'GOBLIN' as CharacterRace.GOBLIN]
@ -56,7 +67,8 @@ if (selectedCharacterType.value) {
characterName.value = selectedCharacterType.value.name
characterGender.value = selectedCharacterType.value.gender
characterRace.value = selectedCharacterType.value.race
characterSpriteId.value = selectedCharacterType.value.spriteId
characterIsSelectable.value = selectedCharacterType.value.isSelectable
characterSpriteId.value = selectedCharacterType.value.sprite?.id
}
function removeCharacterType() {
@ -83,10 +95,11 @@ function refreshCharacterTypeList(unsetSelectedCharacterType = true) {
function saveCharacterType() {
const characterTypeData = {
id: selectedCharacterType.value?.id,
id: selectedCharacterType.value!.id,
name: characterName.value,
gender: characterGender.value,
race: characterRace.value,
isSelectable: characterIsSelectable.value,
spriteId: characterSpriteId.value
}
@ -104,11 +117,16 @@ watch(selectedCharacterType, (characterType: CharacterType | null) => {
characterName.value = characterType.name
characterGender.value = characterType.gender
characterRace.value = characterType.race
characterSpriteId.value = characterType.spriteId
characterIsSelectable.value = characterType.isSelectable
characterSpriteId.value = characterType.sprite?.id
})
onMounted(() => {
if (!selectedCharacterType.value) return
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
assetManagerStore.setSpriteList(response)
})
})
onBeforeUnmount(() => {

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