Compare commits

..

187 Commits

Author SHA1 Message Date
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
148 changed files with 5130 additions and 6498 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>

3920
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

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

BIN
public/assets/tlogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 301 KiB

After

Width:  |  Height:  |  Size: 302 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 400 KiB

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

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 />
<BackgroundImageLoader />
<GmPanel v-if="gameStore.character?.role === 'gm'" />
<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 BackgroundImageLoader from '@/components/utilities/BackgroundImageLoader.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
@ -50,7 +56,9 @@ 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.repeat || event.isComposing || event.defaultPrevented || document.activeElement?.tagName.toUpperCase() === 'INPUT' || document.activeElement?.tagName.toUpperCase() === 'TEXTAREA') {
return
}
if (event.key === 'G') {
gameStore.toggleGmPanel()

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,21 +140,27 @@ export enum CharacterRace {
}
export type CharacterType = {
id: number
id: UUID
name: string
gender: CharacterGender
race: CharacterRace
isEnabledForCharCreation: boolean
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
@ -159,34 +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 ZoneCharacter = {
export type MapCharacter = {
character: Character
isMoving?: boolean
}
export type ExtendedCharacter = Character & {
isMoving?: boolean
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
@ -194,41 +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 WorldSettings = {
date: Date
isRainEnabled: boolean
isFogEnabled: boolean
fogDensity: number
weatherState: WeatherState
}
export type WeatherState = {
isRainEnabled: boolean
rainPercentage: number
isFogEnabled: boolean
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;
}
}
@ -113,7 +128,7 @@ button {
&.active,
&.selected,
&:hover {
@apply bg-gray-700 border-gray-700;
@apply bg-gray border-gray;
}
}
@ -130,12 +145,24 @@ button {
}
}
.character {
&.active {
@apply pr-px border-r-0;
.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 {
@apply text-white font-ui drop-shadow-pixel-black;
}

View File

@ -1,218 +0,0 @@
<template>
<Scene name="effects" @preload="preloadScene" @create="createScene" @update="updateScene" />
</template>
<script setup lang="ts">
import { Scene } from 'phavuer'
import { useZoneStore } from '@/stores/zoneStore'
import { useGameStore } from '@/stores/gameStore'
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
import type { WeatherState } from '@/types'
// Constants
const SUNRISE_HOUR = 6
const SUNSET_HOUR = 20
const DAY_STRENGTH = 100
const NIGHT_STRENGTH = 30
// Stores
const gameStore = useGameStore()
const zoneStore = useZoneStore()
// Scene ref
const sceneRef = ref<Phaser.Scene | null>(null)
// Effect 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)
// State refs
const weatherState = ref<WeatherState>({
isRainEnabled: false,
rainPercentage: 0,
isFogEnabled: false,
fogDensity: 0
})
// Scene lifecycle methods
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
setupEffects(scene)
setupSocketListeners()
}
const updateScene = () => {
updateEffects()
}
// Effect setup
const setupEffects = (scene: Phaser.Scene) => {
createLightEffect(scene)
createRainEffect(scene)
createFogEffect(scene)
}
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)
}
// Lighting calculations
const calculateLightStrength = (time: Date): number => {
const hour = time.getHours()
const minute = time.getMinutes()
let strength = DAY_STRENGTH
// Night time (10 PM - 6 AM)
if (hour >= SUNSET_HOUR || hour < SUNRISE_HOUR) {
strength = NIGHT_STRENGTH
}
// Full daylight (7 AM - 7 PM)
else if (hour > SUNRISE_HOUR && hour < SUNSET_HOUR - 2) {
strength = DAY_STRENGTH
}
// Sunrise transition (6 AM - 7 AM)
else if (hour === SUNRISE_HOUR) {
strength = NIGHT_STRENGTH + ((DAY_STRENGTH - NIGHT_STRENGTH) * minute) / 60
}
// Sunset transition (8 PM - 10 PM)
else if (hour >= SUNSET_HOUR - 2 && hour < SUNSET_HOUR) {
const totalMinutes = (hour - (SUNSET_HOUR - 2)) * 60 + minute
const transitionProgress = totalMinutes / 120 // 2 hours = 120 minutes
strength = DAY_STRENGTH - (DAY_STRENGTH - NIGHT_STRENGTH) * transitionProgress
}
return strength
}
// Effect updates
const updateEffects = () => {
const effects = zoneStore.zone?.zoneEffects || []
if (effects.length > 0) {
updateZoneEffects(effects)
} else {
// Make sure we're getting the current time
const lightStrength = calculateLightStrength(gameStore.world.date)
updateLightEffect(lightStrength)
updateWeatherEffects()
}
}
const updateZoneEffects = (effects: any[]) => {
// Always update light based on time when zone effects are present
updateLightEffect(calculateLightStrength(gameStore.world.date))
effects.forEach((effect) => {
switch (effect.effect) {
case 'rain':
updateRainEffect(effect.strength)
break
case 'fog':
updateFogEffect(effect.strength)
break
}
})
}
const updateWeatherEffects = () => {
updateRainEffect(weatherState.value.isRainEnabled ? weatherState.value.rainPercentage : 0)
updateFogEffect(weatherState.value.isFogEnabled ? weatherState.value.fogDensity * 100 : 0)
}
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)
}
// Socket handlers
const setupSocketListeners = () => {
// Initial weather state
gameStore.connection?.emit('weather', (response: WeatherState) => {
if (zoneStore.zone?.zoneEffects) return
weatherState.value = response
updateEffects()
})
// Weather updates
gameStore.connection?.on('weather', (data: WeatherState) => {
weatherState.value = data
updateEffects()
})
// Time updates
gameStore.connection?.on('date', () => {
if (zoneStore.zone?.zoneEffects) return
updateEffects()
})
}
const updateEffectWindowSize = () => {
if (rainEmitter.value) rainEmitter.value.updateConfig({ x: { min: 0, max: window.innerWidth } })
if (fogSprite.value) {
fogSprite.value.setX(window.innerWidth / 2)
fogSprite.value.setY(window.innerHeight / 2)
}
}
// Watchers
watch(() => zoneStore.zone?.zoneEffects, updateEffects, { deep: true })
onMounted(() => {
window.addEventListener('resize', updateEffectWindowSize)
})
// Cleanup
onBeforeUnmount(() => {
window.removeEventListener('resize', updateEffectWindowSize)
if (sceneRef.value) sceneRef.value.scene.remove('effects')
gameStore.connection?.off('weather')
})
</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" />
<img src="/assets/ui-elements/profile-ui-box-inner.svg" class="absolute left-2 bottom-2 w-[calc(100%_-_16px)] h-[calc(100%_-_40px)]" />
<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 alt="close" draggable="false" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" />
</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" />
</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" />
<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" />
</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" />
</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" />
</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" />
</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

@ -21,27 +21,35 @@
</template>
<script setup lang="ts">
import { onBeforeUnmount, ref, nextTick } from 'vue'
import { onClickOutside } from '@vueuse/core'
import type { Chat } from '@/application/types'
import { useGameStore } from '@/stores/gameStore'
import type { Chat } 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 Chat[])
const chatWindow = ref<HTMLElement | null>(null)
const chatInput = ref<HTMLElement | null>(null)
onClickOutside(chatInput, event => unfocusChat(event, chatInput.value as HTMLElement))
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();
targetElement.blur()
}
}
@ -75,7 +83,7 @@ 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
@ -128,7 +136,12 @@ gameStore.connection?.on('chat:message', (data: Chat) => {
})
scrollToBottom()
onMounted(() => {
addEventListener('keydown', focusChat)
})
onBeforeUnmount(() => {
gameStore.connection?.off('chat:message')
removeEventListener('keydown', focusChat)
})
</script>

View File

@ -15,7 +15,7 @@
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
</div>
<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/avatar/default/head.png" />
<img class="group-hover:drop-shadow-default w-8 h-8 m-[5px] object-contain" draggable="false" src="/assets/placeholders/head.png" />
<p class="absolute bottom-0 -right-1.5 m-0 max-w-4 font-ui z-10 text-white text-[12px] leading-[6px] drop-shadow-pixel"><span class="font-ui text-white text-[8px] ml-0.5">LVL</span> {{ characterLevel }}</p>
</a>
</li>
@ -24,7 +24,7 @@
<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]">
<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" />
</a>
</li>
@ -33,7 +33,7 @@
<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]">
<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" />
</a>
</li>
@ -42,7 +42,7 @@
<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]">
<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" />
</a>
</li>

View File

@ -1,15 +1,15 @@
<template>
<div class="absolute top-4 right-4 hidden lg:block">
<div class="w-40 h-40 rounded-full border border-solid border-gray-500 bg-[url('/assets/ui-texture.png')] bg-no-repeat">
<div class="w-40 h-40 rounded-full 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="absolute w-3 h-3 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/plus-icon.svg" />
<img class="w-3 h-3 center-element" src="/assets/icons/plus-icon.svg" />
<img class="w-full h-full" src="/assets/ui-elements/button-ui-box-textured.svg" />
</button>
<button class="w-6 h-6 relative p-0">
<img class="absolute w-3 h-3 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/minus-icon.svg" />
<img class="w-3 h-3 center-element" src="/assets/icons/minus-icon.svg" />
<img class="w-full h-full" src="/assets/ui-elements/button-ui-box-textured.svg" />
</button>
</div>

View File

@ -1,6 +1,6 @@
<template>
<div class="absolute z-50 w-full h-dvh top-0 left-0 bg-black/60" v-show="false">
<div class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 max-w-[875px] max-h-[600px] h-full w-[80%] bg-gray-700 border-solid border-2 border-gray-500 rounded-md z-50 flex flex-col backdrop-blur-sm shadow-lg">
<div class="center-element max-w-[875px] max-h-[600px] h-full w-[80%] bg-gray border-solid border-2 border-gray-500 rounded-md z-50 flex flex-col backdrop-blur-sm shadow-lg">
<div class="p-2.5 flex max-sm:flex-wrap justify-between items-center gap-5 border-solid border-0 border-b border-gray-500">
<h3 class="m-0 font-medium shrink-0">Game menu</h3>
<div class="hidden sm:flex gap-1.5 flex-wrap">
@ -30,12 +30,12 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import CharacterScreen from '@/components/game/gui/partials/CharacterScreen.vue'
import Equipment from '@/components/game/gui/partials/Equipment.vue'
import Inventory from '@/components/game/gui/partials/Inventory.vue'
import Settings from '@/components/game/gui/partials/Settings.vue'
import { useGameStore } from '@/stores/gameStore'
import Inventory from '@/components/gui/partials/Inventory.vue'
import Equipment from '@/components/gui/partials/Equipment.vue'
import CharacterScreen from '@/components/gui/partials/CharacterScreen.vue'
import Settings from '@/components/gui/partials/Settings.vue'
import { ref } from 'vue'
const gameStore = useGameStore()
let userPanelScreen = ref('inventory')

View File

@ -14,47 +14,47 @@
<div class="flex gap-3 justify-center">
<!-- Helmet -->
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square h-11 w-11 relative hover:bg-gray-200">
<img src="/assets/icons/inventory/helmet.svg" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" />
<img src="/assets/icons/inventory/helmet.svg" class="center-element w-11/12 opacity-20" />
</div>
<!-- Head charm -->
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square h-11 w-11 relative hover:bg-gray-200">
<img src="/assets/icons/inventory/head_charm.svg" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" />
<img src="/assets/icons/inventory/head_charm.svg" class="center-element w-11/12 opacity-20" />
</div>
</div>
<div class="flex gap-3 justify-center">
<!-- Bracers -->
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md w-11 h-[104px] relative hover:bg-gray-200">
<img src="/assets/icons/inventory/bracers.svg" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" />
<img src="/assets/icons/inventory/bracers.svg" class="center-element w-11/12 opacity-20" />
</div>
<!-- Chestplate -->
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square w-[104px] h-[104px] relative hover:bg-gray-200">
<img src="/assets/icons/inventory/chestplate.svg" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-10/12 opacity-20" />
<img src="/assets/icons/inventory/chestplate.svg" class="center-element w-10/12 opacity-20" />
</div>
<!-- Primary Weapon -->
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md w-11 h-[104px] self-stretch justify-self-stretch relative hover:bg-gray-200">
<img src="/assets/icons/inventory/primary_weapon.svg" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" />
<img src="/assets/icons/inventory/primary_weapon.svg" class="center-element w-11/12 opacity-20" />
</div>
</div>
<div class="flex gap-3 justify-center">
<!-- Legs -->
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md w-11 h-[104px] self-stretch justify-self-stretch relative hover:bg-gray-200">
<img src="/assets/icons/inventory/legs.svg" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" />
<img src="/assets/icons/inventory/legs.svg" class="center-element w-11/12 opacity-20" />
</div>
<div class="flex flex-col gap-3">
<!-- Belt/pouch -->
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square h-11 w-11 self-stretch justify-self-stretch relative hover:bg-gray-200">
<img src="/assets/icons/inventory/pouch.svg" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" />
<img src="/assets/icons/inventory/pouch.svg" class="center-element w-11/12 opacity-20" />
</div>
<!-- Boots -->
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square h-11 w-11 self-stretch justify-self-stretch relative hover:bg-gray-200">
<img src="/assets/icons/inventory/boots.svg" class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-11/12 opacity-20" />
<img src="/assets/icons/inventory/boots.svg" class="center-element w-11/12 opacity-20" />
</div>
</div>
</div>

View File

@ -33,8 +33,8 @@
</template>
<script setup lang="ts">
import CharacterSettings from '@/components/game/gui/partials/settings/CharacterSettings.vue'
import { ref } from 'vue'
import CharacterSettings from '@/components/gui/partials/settings/CharacterSettings.vue'
let settingCategory = ref('character')
</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,12 +1,12 @@
<template>
<Modal :isModalOpen="gameStore.uiSettings.isGmPanelOpen" @modal:close="() => gameStore.toggleGmPanel()" :modal-width="1000" :modal-height="650" :can-full-screen="true" :disable-bg-texture="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="() => zoneEditorStore.toggleActive()">Zone 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>
@ -18,14 +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 { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { ref } from 'vue'
defineEmits(['open-map-editor'])
const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore()
const mapEditor = useMapEditorComposable()
let toggle = ref('asset-manager')
</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,8 @@
</select>
</div>
<div class="form-field-full">
<label for="isEnabledForCharCreation">Is enabled for character creation</label>
<select v-model="characterIsEnabledForCharCreation" class="input-field" name="isEnabledForCharCreation">
<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>
@ -40,10 +40,10 @@
</template>
<script setup lang="ts">
import type { CharacterType, CharacterGender, CharacterRace, Sprite } 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()
@ -53,7 +53,7 @@ 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 characterIsEnabledForCharCreation = ref<boolean>(false)
const characterIsSelectable = ref<boolean>(false)
const characterSpriteId = ref<string | null | undefined>(null)
const genderOptions: CharacterGender[] = ['MALE' as CharacterGender.MALE, 'FEMALE' as CharacterGender.FEMALE]
@ -67,8 +67,8 @@ if (selectedCharacterType.value) {
characterName.value = selectedCharacterType.value.name
characterGender.value = selectedCharacterType.value.gender
characterRace.value = selectedCharacterType.value.race
characterIsEnabledForCharCreation.value = selectedCharacterType.value.isEnabledForCharCreation
characterSpriteId.value = selectedCharacterType.value.spriteId
characterIsSelectable.value = selectedCharacterType.value.isSelectable
characterSpriteId.value = selectedCharacterType.value.sprite?.id
}
function removeCharacterType() {
@ -99,7 +99,7 @@ function saveCharacterType() {
name: characterName.value,
gender: characterGender.value,
race: characterRace.value,
isEnabledForCharCreation: characterIsEnabledForCharCreation.value,
isSelectable: characterIsSelectable.value,
spriteId: characterSpriteId.value
}
@ -117,8 +117,8 @@ watch(selectedCharacterType, (characterType: CharacterType | null) => {
characterName.value = characterType.name
characterGender.value = characterType.gender
characterRace.value = characterType.race
characterIsEnabledForCharCreation.value = characterType.isEnabledForCharCreation
characterSpriteId.value = characterType.spriteId
characterIsSelectable.value = characterType.isSelectable
characterSpriteId.value = characterType.sprite?.id
})
onMounted(() => {

View File

@ -1,36 +1,42 @@
<template>
<div class="relative p-2.5 flex items-center gap-x-2.5">
<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 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
<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="createNewCharacterType">
<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 class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</div>
<div v-bind="containerProps" class="overflow-y-auto relative" @scroll="onScroll">
<div v-bind="wrapperProps" ref="elementToScroll">
<a v-for="{ data: characterType } in list" :key="characterType.id" class="relative p-2.5 cursor-pointer block" :class="{ 'bg-cyan/80': assetManagerStore.selectedCharacterType?.id === characterType.id }" @click="assetManagerStore.setSelectedCharacterType(characterType as CharacterType)">
<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: characterType } in list"
:key="characterType.id"
class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group"
:class="{ 'bg-cyan': assetManagerStore.selectedCharacterType?.id === characterType.id }"
@click="assetManagerStore.setSelectedCharacterType(characterType as CharacterType)"
>
<div class="flex items-center gap-2.5">
<span :class="{ 'text-white': assetManagerStore.selectedCharacterType?.id === characterType.id }">{{ characterType.name }}</span>
<span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedCharacterType?.id === characterType.id }">{{ characterType.name }}</span>
</div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
</div>
<button class="left-[calc(50%_-_60px)] fixed bottom-2.5 min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
<img class="absolute invert w-8 h-8 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
</button>
<div 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 { useGameStore } from '@/stores/gameStore'
import { onMounted, ref, computed } from 'vue'
import type { CharacterType } from '@/application/types'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import type { CharacterType } from '@/types'
import { useGameStore } from '@/stores/gameStore'
import { useVirtualList } from '@vueuse/core'
import { computed, onMounted, ref } from 'vue'
const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore()

View File

@ -0,0 +1,143 @@
<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="saveItem">
<div class="form-field-full">
<label for="name">Name</label>
<input v-model="itemName" class="input-field" type="text" name="name" placeholder="Item Name" />
</div>
<div class="form-field-full">
<label for="description">Description</label>
<input v-model="itemDescription" class="input-field" type="text" name="description" placeholder="Item Description" />
</div>
<div class="form-field-full">
<label for="itemType">Type</label>
<select v-model="itemType" class="input-field" name="itemType">
<option v-for="type in itemTypeOptions" :key="type" :value="type">{{ type }}</option>
</select>
</div>
<div class="form-field-full">
<label for="rarity">Rarity</label>
<select v-model="itemRarity" class="input-field" name="rarity">
<option v-for="rarity in rarityOptions" :key="rarity" :value="rarity">{{ rarity }}</option>
</select>
</div>
<div class="form-field-full">
<label for="stackable">Stackable</label>
<select v-model="itemStackable" class="input-field" name="stackable">
<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="itemSpriteId" 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="removeItem">Remove</button>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import type { Item, ItemRarity, ItemType, 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 selectedItem = computed(() => assetManagerStore.selectedItem)
const itemName = ref('')
const itemDescription = ref('')
const itemType = ref<ItemType>('WEAPON' as ItemType)
const itemRarity = ref<ItemRarity>('COMMON' as ItemRarity)
const itemStackable = ref<boolean>(false)
const itemSpriteId = ref<string | null | undefined>(null)
const itemTypeOptions: ItemType[] = ['WEAPON', 'HELMET', 'CHEST', 'LEGS', 'BOOTS', 'GLOVES', 'RING', 'NECKLACE']
const rarityOptions: ItemRarity[] = ['COMMON', 'UNCOMMON', 'RARE', 'EPIC', 'LEGENDARY']
if (!selectedItem.value) {
console.error('No item selected')
}
if (selectedItem.value) {
itemName.value = selectedItem.value.name
itemDescription.value = selectedItem.value.description || ''
itemType.value = selectedItem.value.itemType
itemRarity.value = selectedItem.value.rarity
itemStackable.value = selectedItem.value.stackable
itemSpriteId.value = selectedItem.value.spriteId
}
function removeItem() {
if (!selectedItem.value) return
gameStore.connection?.emit('gm:item:remove', { id: selectedItem.value.id }, (response: boolean) => {
if (!response) {
console.error('Failed to remove item')
return
}
refreshItemList()
})
}
function refreshItemList(unsetSelectedItem = true) {
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => {
assetManagerStore.setItemList(response)
if (unsetSelectedItem) {
assetManagerStore.setSelectedItem(null)
}
})
}
function saveItem() {
const itemData = {
id: selectedItem.value!.id,
name: itemName.value,
description: itemDescription.value,
itemType: itemType.value,
rarity: itemRarity.value,
stackable: itemStackable.value,
spriteId: itemSpriteId.value
}
gameStore.connection?.emit('gm:item:update', itemData, (response: boolean) => {
if (!response) {
console.error('Failed to save item')
return
}
refreshItemList(false)
})
}
watch(selectedItem, (item: Item | null) => {
if (!item) return
itemName.value = item.name
itemDescription.value = item.description || ''
itemType.value = item.itemType
itemRarity.value = item.rarity
itemStackable.value = item.stackable
itemSpriteId.value = item.spriteId
})
onMounted(() => {
if (!selectedItem.value) return
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
assetManagerStore.setSpriteList(response)
})
})
onBeforeUnmount(() => {
assetManagerStore.setSelectedItem(null)
})
</script>

View File

@ -0,0 +1,95 @@
<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-item" 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-item" @click="createNewItem">
<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: item } in list" :key="item.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedItem?.id === item.id }" @click="assetManagerStore.setSelectedItem(item as Item)">
<div class="flex items-center gap-2.5">
<span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedItem?.id === item.id }">
{{ item.name }}
<small class="text-gray-400">({{ item.itemType }})</small>
</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 { Item } 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 = () => {
virtualList.value?.scrollTo(0)
}
const createNewItem = () => {
gameStore.connection?.emit('gm:item:create', {}, (response: boolean) => {
if (!response) {
console.error('Failed to create new item')
return
}
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => {
assetManagerStore.setItemList(response)
})
})
}
const filteredItems = computed(() => {
if (!searchQuery.value) {
return assetManagerStore.itemList
}
return assetManagerStore.itemList.filter((item) => item.name.toLowerCase().includes(searchQuery.value.toLowerCase()) || item.itemType.toLowerCase().includes(searchQuery.value.toLowerCase()))
})
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(filteredItems, {
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:item:list', {}, (response: Item[]) => {
assetManagerStore.setItemList(response)
})
})
</script>

View File

@ -0,0 +1,146 @@
<template>
<div class="h-full overflow-auto">
<div class="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray">
<img class="max-h-56" :src="`${config.server_endpoint}/textures/map_objects/${selectedMapObject?.id}.png`" :alt="'Object ' + selectedMapObject?.id" />
</div>
<div class="mt-5 block">
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject">
<div class="form-field-full">
<label for="name">Name</label>
<input v-model="mapObjectName" class="input-field" type="text" name="name" placeholder="Wall #1" />
</div>
<div class="form-field-half">
<label for="origin-x">Origin X</label>
<input v-model="mapObjectOriginX" class="input-field" type="number" step="any" name="origin-x" placeholder="Origin X" />
</div>
<div class="form-field-half">
<label for="origin-y">Origin Y</label>
<input v-model="mapObjectOriginY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
</div>
<div class="form-field-full">
<label for="tags">Tags</label>
<ChipsInput v-model="mapObjectTags" @update:modelValue="mapObjectTags = $event" />
</div>
<div class="form-field-full">
<label for="frame-speed">Frame rate</label>
<input v-model="mapObjectFrameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
</div>
<div class="form-field-half">
<label for="frame-width">Frame width</label>
<input v-model="mapObjectFrameWidth" class="input-field" type="number" step="any" name="frame-width" placeholder="Frame width" />
</div>
<div class="form-field-half">
<label for="frame-height">Frame height</label>
<input v-model="mapObjectFrameHeight" class="input-field" type="number" step="any" name="frame-height" placeholder="Frame height" />
</div>
<div class="flex gap-4">
<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="removeObject">Delete</button>
</div>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import config from '@/application/config'
import type { MapObject } from '@/application/types'
import ChipsInput from '@/components/forms/ChipsInput.vue'
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 selectedMapObject = computed(() => assetManagerStore.selectedMapObject)
const mapObjectName = ref('')
const mapObjectTags = ref<string[]>([])
const mapObjectOriginX = ref(0)
const mapObjectOriginY = ref(0)
const mapObjectFrameRate = ref(0)
const mapObjectFrameWidth = ref(0)
const mapObjectFrameHeight = ref(0)
if (!selectedMapObject.value) {
console.error('No map mapObject selected')
}
if (selectedMapObject.value) {
mapObjectName.value = selectedMapObject.value.name
mapObjectTags.value = selectedMapObject.value.tags
mapObjectOriginX.value = selectedMapObject.value.originX
mapObjectOriginY.value = selectedMapObject.value.originY
mapObjectFrameRate.value = selectedMapObject.value.frameRate
mapObjectFrameWidth.value = selectedMapObject.value.frameWidth
mapObjectFrameHeight.value = selectedMapObject.value.frameHeight
}
function removeObject() {
gameStore.connection?.emit('gm:mapObject:remove', { mapObject: selectedMapObject.value?.id }, (response: boolean) => {
if (!response) {
console.error('Failed to remove mapObject')
return
}
refreshObjectList()
})
}
function refreshObjectList(unsetSelectedMapObject = true) {
gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => {
assetManagerStore.setMapObjectList(response)
if (unsetSelectedMapObject) {
assetManagerStore.setSelectedMapObject(null)
}
})
}
function saveObject() {
if (!selectedMapObject.value) {
console.error('No mapObject selected')
return
}
gameStore.connection?.emit(
'gm:mapObject:update',
{
id: selectedMapObject.value.id,
name: mapObjectName.value,
tags: mapObjectTags.value,
originX: mapObjectOriginX.value,
originY: mapObjectOriginY.value,
frameRate: mapObjectFrameRate.value,
frameWidth: mapObjectFrameWidth.value,
frameHeight: mapObjectFrameHeight.value
},
(response: boolean) => {
if (!response) {
console.error('Failed to save mapObject')
return
}
refreshObjectList(false)
}
)
}
watch(selectedMapObject, (mapObject: MapObject | null) => {
if (!mapObject) return
mapObjectName.value = mapObject.name
mapObjectTags.value = mapObject.tags
mapObjectOriginX.value = mapObject.originX
mapObjectOriginY.value = mapObject.originY
mapObjectFrameRate.value = mapObject.frameRate
mapObjectFrameWidth.value = mapObject.frameWidth
mapObjectFrameHeight.value = mapObject.frameHeight
})
onMounted(() => {
if (!selectedMapObject.value) return
})
onBeforeUnmount(() => {
assetManagerStore.setSelectedMapObject(null)
})
</script>

View File

@ -1,39 +1,39 @@
<template>
<div class="relative p-2.5 flex items-center gap-x-2.5">
<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="upload-asset" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
<label for="upload-asset" 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">
<input class="hidden" id="upload-asset" ref="objectUploadField" type="file" accept="image/png" multiple @change="handleFileUpload" />
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
</label>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</div>
<div v-bind="containerProps" class="overflow-y-auto relative" @scroll="onScroll">
<div v-bind="wrapperProps" ref="elementToScroll">
<a v-for="{ data: object } in list" :key="object.id" class="relative p-2.5 cursor-pointer block" :class="{ 'bg-cyan/80': assetManagerStore.selectedObject?.id === object.id }" @click="assetManagerStore.setSelectedObject(object as Object)">
<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: mapObject } in list" :key="mapObject.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedMapObject?.id === mapObject.id }" @click="assetManagerStore.setSelectedMapObject(mapObject as MapObject)">
<div class="flex items-center gap-2.5">
<div class="h-7 w-16 max-w-16 flex justify-center">
<img class="h-7" :src="`${config.server_endpoint}/assets/objects/${object.id}.png`" alt="Object" />
<img class="h-7" :src="`${config.server_endpoint}/textures/map_objects/${mapObject.id}.png`" alt="Object" />
</div>
<span :class="{ 'text-white': assetManagerStore.selectedObject?.id === object.id }">{{ object.name }}</span>
<span :class="{ 'text-white': assetManagerStore.selectedMapObject?.id === mapObject.id }">{{ mapObject.name }}</span>
</div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
</div>
<button class="left-[calc(50%_-_60px)] fixed bottom-2.5 min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
<img class="absolute invert w-8 h-8 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
</button>
<div 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 config from '@/config'
import { useGameStore } from '@/stores/gameStore'
import { onMounted, ref, computed } from 'vue'
import config from '@/application/config'
import type { MapObject } from '@/application/types'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import type { Object } from '@/types'
import { useGameStore } from '@/stores/gameStore'
import { useVirtualList } from '@vueuse/core'
import { computed, onMounted, ref } from 'vue'
const gameStore = useGameStore()
const objectUploadField = ref(null)
@ -47,14 +47,14 @@ const elementToScroll = ref()
const handleFileUpload = (e: Event) => {
const files = (e.target as HTMLInputElement).files
if (!files) return
gameStore.connection?.emit('gm:object:upload', files, (response: boolean) => {
gameStore.connection?.emit('gm:mapObject:upload', files, (response: boolean) => {
if (!response) {
if (config.development) console.error('Failed to upload object')
return
}
gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => {
assetManagerStore.setObjectList(response)
gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => {
assetManagerStore.setMapObjectList(response)
})
})
}
@ -66,9 +66,9 @@ const handleSearch = () => {
const filteredObjects = computed(() => {
if (!searchQuery.value) {
return assetManagerStore.objectList
return assetManagerStore.mapObjectList
}
return assetManagerStore.objectList.filter((object) => object.name.toLowerCase().includes(searchQuery.value.toLowerCase()))
return assetManagerStore.mapObjectList.filter((object) => object.name.toLowerCase().includes(searchQuery.value.toLowerCase()))
})
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(filteredObjects, {
@ -92,8 +92,8 @@ function toTop() {
}
onMounted(() => {
gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => {
assetManagerStore.setObjectList(response)
gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => {
assetManagerStore.setMapObjectList(response)
})
})
</script>

View File

@ -1,163 +0,0 @@
<template>
<div class="h-full overflow-auto">
<div class="relative p-2.5 flex flex-col items-center justify-between h-72">
<div class="filler"></div>
<img class="max-h-56" :src="`${config.server_endpoint}/assets/objects/${selectedObject?.id}.png`" :alt="'Object ' + selectedObject?.id" />
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeObject">Remove</button>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</div>
<div class="m-2.5 p-2.5 block">
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject">
<div class="form-field-full">
<label for="name">Name</label>
<input v-model="objectName" class="input-field" type="text" name="name" placeholder="Wall #1" />
</div>
<div class="form-field-half">
<label for="origin-x">Origin X</label>
<input v-model="objectOriginX" class="input-field" type="number" step="any" name="origin-x" placeholder="Origin X" />
</div>
<div class="form-field-half">
<label for="origin-y">Origin Y</label>
<input v-model="objectOriginY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
</div>
<div class="form-field-full">
<label for="origin-x">Tags</label>
<ChipsInput v-model="objectTags" @update:modelValue="objectTags = $event" />
</div>
<div class="form-field-full">
<label for="origin-x">Is animated</label>
<select v-model="objectIsAnimated" class="input-field" name="is-animated">
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>
</div>
<div class="form-field-full">
<label for="frame-speed">Frame speed</label>
<input v-model="objectFrameSpeed" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame speed" />
</div>
<div class="form-field-half">
<label for="frame-width">Frame width</label>
<input v-model="objectFrameWidth" class="input-field" type="number" step="any" name="frame-width" placeholder="Frame width" />
</div>
<div class="form-field-half">
<label for="frame-height">Frame height</label>
<input v-model="objectFrameHeight" class="input-field" type="number" step="any" name="frame-height" placeholder="Frame height" />
</div>
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import type { Object } from '@/types'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { useGameStore } from '@/stores/gameStore'
import config from '@/config'
import ChipsInput from '@/components/forms/ChipsInput.vue'
const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore()
const zoneEditorStore = useZoneEditorStore()
const selectedObject = computed(() => assetManagerStore.selectedObject)
const objectName = ref('')
const objectTags = ref<string[]>([])
const objectOriginX = ref(0)
const objectOriginY = ref(0)
const objectIsAnimated = ref(false)
const objectFrameSpeed = ref(0)
const objectFrameWidth = ref(0)
const objectFrameHeight = ref(0)
if (!selectedObject.value) {
console.error('No object selected')
}
if (selectedObject.value) {
objectName.value = selectedObject.value.name
objectTags.value = selectedObject.value.tags
objectOriginX.value = selectedObject.value.originX
objectOriginY.value = selectedObject.value.originY
objectIsAnimated.value = selectedObject.value.isAnimated
objectFrameSpeed.value = selectedObject.value.frameSpeed
objectFrameWidth.value = selectedObject.value.frameWidth
objectFrameHeight.value = selectedObject.value.frameHeight
}
function removeObject() {
gameStore.connection?.emit('gm:object:remove', { object: selectedObject.value?.id }, (response: boolean) => {
if (!response) {
console.error('Failed to remove object')
return
}
refreshObjectList()
})
}
function refreshObjectList(unsetSelectedObject = true) {
gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => {
assetManagerStore.setObjectList(response)
if (unsetSelectedObject) {
assetManagerStore.setSelectedObject(null)
}
if (zoneEditorStore.active) {
zoneEditorStore.setObjectList(response)
}
})
}
function saveObject() {
if (!selectedObject.value) {
console.error('No object selected')
return
}
gameStore.connection?.emit(
'gm:object:update',
{
id: selectedObject.value.id,
name: objectName.value,
tags: objectTags.value,
originX: objectOriginX.value,
originY: objectOriginY.value,
isAnimated: objectIsAnimated.value,
frameSpeed: objectFrameSpeed.value,
frameWidth: objectFrameWidth.value,
frameHeight: objectFrameHeight.value
},
(response: boolean) => {
if (!response) {
console.error('Failed to save object')
return
}
refreshObjectList(false)
}
)
}
watch(selectedObject, (object: Object | null) => {
if (!object) return
objectName.value = object.name
objectTags.value = object.tags
objectOriginX.value = object.originX
objectOriginY.value = object.originY
objectIsAnimated.value = object.isAnimated
objectFrameSpeed.value = object.frameSpeed
objectFrameWidth.value = object.frameWidth
objectFrameHeight.value = object.frameHeight
})
onMounted(() => {
if (!selectedObject.value) return
})
onBeforeUnmount(() => {
assetManagerStore.setSelectedObject(null)
})
</script>

View File

@ -1,7 +1,7 @@
<template>
<div class="h-full overflow-auto">
<div class="relative p-4 flex flex-col">
<div class="flex flex-wrap gap-2">
<div class="relative flex flex-col">
<div class="flex flex-wrap gap-2 p-2.5 rounded-md default-border bg-gray">
<div class="w-full flex flex-col">
<label class="mb-1.5 font-titles" for="name">Name</label>
<input v-model="spriteName" class="input-field" type="text" name="name" placeholder="New sprite" />
@ -10,16 +10,23 @@
<div class="w-full flex gap-2 mt-2 pb-4 relative">
<button class="btn-cyan px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="saveSprite">Save</button>
<button class="btn-red px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="deleteSprite">Delete</button>
<div class="w-[calc(100%_+_32px)] absolute left-[-15px] bottom-0 h-px bg-gray-500"></div>
<button class="btn bg-indigo-500 hover:bg-indigo-600 rounded text-white px-4 py-2 flex-1 sm:flex-none" type="button" @click.prevent="copySprite">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</button>
</div>
</div>
<button class="btn-cyan py-2 my-4" type="button" @click.prevent="addNewImage">New action</button>
<Accordion v-for="action in spriteActions" :key="action.id">
<template #header>
<div class="flex justify-between items-center">
<div class="flex items-center">
{{ action.action }}
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="() => spriteActions.splice(spriteActions.indexOf(action), 1)">Delete</button>
<div class="ml-auto space-x-2">
<button class="btn-cyan px-4 py-1.5 min-w-24" type="button" @click.stop.prevent="openPreviewModal(action)">View</button>
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.stop.prevent="() => spriteActions.splice(spriteActions.indexOf(action), 1)">Delete</button>
</div>
</div>
</template>
<template #content>
@ -36,42 +43,42 @@
<label for="origin-y">Origin Y</label>
<input v-model.number="action.originY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
</div>
<div class="form-field-half">
<label for="is-animated">Is animated</label>
<select v-model="action.isAnimated" class="input-field" name="is-animated">
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>
</div>
<div class="form-field-half" v-if="action.isAnimated">
<label for="is-looping">Is looping</label>
<select v-model="action.isLooping" class="input-field" name="is-looping">
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>
</div>
<div class="form-field-full" v-if="action.isAnimated">
<label for="frame-speed">Frame speed</label>
<input v-model.number="action.frameSpeed" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame speed" />
<div class="form-field-full">
<label for="frame-speed">Frame rate</label>
<input v-model.number="action.frameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
</div>
<div class="form-field-full">
<SpriteActionsInput v-model="action.sprites" />
<SpriteActionsInput
v-model="action.sprites"
@tempOffsetChange="(index, offset) => handleTempOffsetChange(action, index, offset)"
/>
</div>
</form>
</template>
</Accordion>
<SpritePreview
v-if="selectedAction"
:sprites="selectedAction.sprites"
:frame-rate="selectedAction.frameRate"
:is-modal-open="isModalOpen"
:temp-offset-index="tempOffsetData.index"
:temp-offset="tempOffsetData.offset"
@update:frame-rate="updateFrameRate"
@update:is-modal-open="isModalOpen = $event"
/>
</div>
</div>
</template>
<script setup lang="ts">
import type { Sprite, SpriteAction } from '@/types'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import type { Sprite, SpriteAction, UUID } from '@/application/types'
import { uuidv4 } from '@/application/utilities'
import SpriteActionsInput from '@/components/gameMaster/assetManager/partials/sprite/partials/SpriteImagesInput.vue'
import SpritePreview from '@/components/gameMaster/assetManager/partials/sprite/partials/SpritePreview.vue'
import Accordion from '@/components/utilities/Accordion.vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore'
import Accordion from '@/components/utilities/Accordion.vue'
import SpriteActionsInput from '@/components/gameMaster/assetManager/partials/sprite/partials/SpriteImagesInput.vue'
import { uuidv4 } from '@/utilities'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore()
@ -80,6 +87,8 @@ const selectedSprite = computed(() => assetManagerStore.selectedSprite)
const spriteName = ref('')
const spriteActions = ref<SpriteAction[]>([])
const isModalOpen = ref(false)
const selectedAction = ref<SpriteAction | null>(null)
if (!selectedSprite.value) {
console.error('No sprite selected')
@ -87,7 +96,7 @@ if (!selectedSprite.value) {
if (selectedSprite.value) {
spriteName.value = selectedSprite.value.name
spriteActions.value = selectedSprite.value.spriteActions
spriteActions.value = sortSpriteActions(selectedSprite.value.spriteActions)
}
function deleteSprite() {
@ -100,6 +109,16 @@ function deleteSprite() {
})
}
function copySprite() {
gameStore.connection?.emit('gm:sprite:copy', { id: selectedSprite.value?.id }, (response: boolean) => {
if (!response) {
console.error('Failed to copy sprite')
return
}
refreshSpriteList(false)
})
}
function refreshSpriteList(unsetSelectedSprite = true) {
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
assetManagerStore.setSpriteList(response)
@ -126,9 +145,7 @@ function saveSprite() {
sprites: action.sprites,
originX: action.originX,
originY: action.originY,
isAnimated: action.isAnimated,
isLooping: action.isLooping,
frameSpeed: action.frameSpeed,
frameRate: action.frameRate,
frameWidth: action.frameWidth,
frameHeight: action.frameHeight
}
@ -148,16 +165,13 @@ function addNewImage() {
if (!selectedSprite.value) return
const newImage: SpriteAction = {
id: uuidv4(), // Temporary ID, should be replaced by server-generated ID
spriteId: selectedSprite.value.id,
sprite: selectedSprite.value,
id: uuidv4(),
sprite: selectedSprite.value.id,
action: 'new_action',
sprites: [],
originX: 0,
originY: 0,
isAnimated: false,
isLooping: false,
frameSpeed: 0,
frameRate: 0,
frameWidth: 0,
frameHeight: 0
}
@ -166,13 +180,46 @@ function addNewImage() {
spriteActions.value = []
}
spriteActions.value.push(newImage)
spriteActions.value = sortSpriteActions([...spriteActions.value, newImage])
}
function sortSpriteActions(actions: SpriteAction[]): SpriteAction[] {
if (!actions) return []
return [...actions].sort((a, b) => a.action.localeCompare(b.action))
}
function openPreviewModal(action: SpriteAction) {
selectedAction.value = action
isModalOpen.value = true
}
function updateFrameRate(value: number) {
if (selectedAction.value) {
selectedAction.value.frameRate = value
}
}
const tempOffsetData = ref<{ index: number | undefined; offset: { x: number; y: number } | undefined }>({
index: undefined,
offset: undefined
})
function handleTempOffsetChange(action: SpriteAction, index: number, offset: { x: number; y: number }) {
if (selectedAction.value === action) {
tempOffsetData.value = { index, offset }
}
}
watch(selectedSprite, (sprite: Sprite | null) => {
if (!sprite) return
spriteName.value = sprite.name
spriteActions.value = sprite.spriteActions
spriteActions.value = sortSpriteActions(sprite.spriteActions)
})
watch(isModalOpen, (newValue) => {
if (!newValue) {
selectedAction.value = null
}
})
onMounted(() => {

View File

@ -1,35 +1,35 @@
<template>
<div class="relative p-2.5 flex items-center gap-x-2.5">
<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" />
<button @click.prevent="newButtonClickHandler" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
<button @click.prevent="newButtonClickHandler" 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">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
</button>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</div>
<div v-bind="containerProps" class="overflow-y-auto relative" @scroll="onScroll">
<div v-bind="wrapperProps" ref="elementToScroll">
<a v-for="{ data: sprite } in list" :key="sprite.id" class="relative p-2.5 cursor-pointer block" :class="{ 'bg-cyan/80': assetManagerStore.selectedSprite?.id === sprite.id }" @click="assetManagerStore.setSelectedSprite(sprite as Sprite)">
<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: sprite } in list" :key="sprite.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedSprite?.id === sprite.id }" @click="assetManagerStore.setSelectedSprite(sprite as Sprite)">
<div class="flex items-center gap-2.5">
<span :class="{ 'text-white': assetManagerStore.selectedSprite?.id === sprite.id }">{{ sprite.name }}</span>
</div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
</div>
<button class="left-[calc(50%_-_60px)] fixed bottom-2.5 min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
<img class="absolute invert w-8 h-8 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
</button>
<div 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 config from '@/config'
import { useGameStore } from '@/stores/gameStore'
import { onMounted, ref, computed } from 'vue'
import config from '@/application/config'
import type { Sprite } from '@/application/types'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore'
import { useVirtualList } from '@vueuse/core'
import type { Sprite } from '@/types'
import { computed, onMounted, ref } from 'vue'
const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore()

View File

@ -1,14 +1,48 @@
<template>
<div class="flex flex-wrap gap-3">
<div v-for="(image, index) in modelValue" :key="index" class="h-20 w-20 p-4 bg-gray-50 bg-opacity-50 rounded text-center relative group cursor-move" draggable="true" @dragstart="dragStart($event, index)" @dragover.prevent @dragenter.prevent @drop="drop($event, index)">
<img :src="image" class="max-w-full max-h-full object-contain pointer-events-none" alt="Uploaded image" />
<button @click.stop="deleteImage(index)" class="absolute top-1 right-1 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" aria-label="Delete image">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<div v-for="(image, index) in modelValue" :key="index" class="h-20 w-20 p-4 bg-gray-300 bg-opacity-50 rounded text-center relative group cursor-move" draggable="true" @dragstart="dragStart($event, index)" @dragover.prevent @dragenter.prevent @drop="drop($event, index)">
<img :src="image.url" class="max-w-full max-h-full object-contain pointer-events-none" alt="Uploaded image" @load="updateImageDimensions($event, index)" />
<div v-if="image.dimensions" class="absolute bottom-1 right-1 bg-black/50 text-white text-xs px-1 py-0.5 rounded transition-opacity font-default">
{{ image.dimensions.width }}x{{ image.dimensions.height }}
</div>
<div class="absolute top-1 left-1 flex-row space-y-1">
<button @click.stop="deleteImage(index)" class="bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" aria-label="Delete image">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<button @click.stop="openOffsetModal(index)" class="bg-blue-500 text-white rounded-full w-6 h-6 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" aria-label="Scope image">
<svg width="50px" height="50px" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.29289 3.70711L1 11V15H5L12.2929 7.70711L8.29289 3.70711Z" fill="white" />
<path d="M9.70711 2.29289L13.7071 6.29289L15.1716 4.82843C15.702 4.29799 16 3.57857 16 2.82843C16 1.26633 14.7337 0 13.1716 0C12.4214 0 11.702 0.297995 11.1716 0.828428L9.70711 2.29289Z" fill="white" />
</svg>
</button>
</div>
<Modal :is-modal-open="selectedImageIndex === index" :modal-width="300" :modal-height="210" :is-resizable="false" :bg-style="'none'" @modal:close="closeOffsetModal">
<template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">Action offset ({{ selectedImageIndex }})</h3>
</template>
<template #modalBody>
<div class="m-4">
<form method="post" @submit.prevent="saveOffset(index)" class="inline">
<div class="gap-2.5 flex flex-wrap">
<div class="form-field-half">
<label for="offsetX">Offset X</label>
<input class="input-field max-w-64" v-model="tempOffset.x" name="offsetX" id="offsetX" type="number" />
</div>
<div class="form-field-half">
<label for="offsetY">Offset Y</label>
<input class="input-field max-w-64" v-model="tempOffset.y" name="offsetY" id="offsetY" type="number" />
</div>
</div>
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
</form>
</div>
</template>
</Modal>
</div>
<div class="h-20 w-20 p-4 bg-gray-100 bg-opacity-50 rounded justify-center items-center flex hover:cursor-pointer" @click="triggerFileInput" @drop.prevent="onDrop" @dragover.prevent>
<div class="h-20 w-20 p-4 bg-gray-200 bg-opacity-50 rounded justify-center items-center flex hover:cursor-pointer" @click="triggerFileInput" @drop.prevent="onDrop" @dragover.prevent>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 invert" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
@ -18,10 +52,23 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import Modal from '@/components/utilities/Modal.vue'
import { ref, watch } from 'vue'
interface SpriteImage {
url: string
offset: {
x: number
y: number
}
dimensions?: {
width: number
height: number
}
}
interface Props {
modelValue: string[]
modelValue: SpriteImage[]
}
const props = withDefaults(defineProps<Props>(), {
@ -29,11 +76,15 @@ const props = withDefaults(defineProps<Props>(), {
})
const emit = defineEmits<{
(e: 'update:modelValue', value: string[]): void
(e: 'update:modelValue', value: SpriteImage[]): void
(e: 'close'): void
(e: 'tempOffsetChange', index: number, offset: { x: number, y: number }): void
}>()
const fileInput = ref<HTMLInputElement | null>(null)
const draggedIndex = ref<number | null>(null)
const selectedImageIndex = ref<number | null>(null)
const tempOffset = ref({ x: 0, y: 0 })
const triggerFileInput = () => {
fileInput.value?.click()
@ -54,19 +105,25 @@ const onDrop = (event: DragEvent) => {
const handleFiles = (files: FileList) => {
Array.from(files).forEach((file) => {
if (file.type.startsWith('image/')) {
const reader = new FileReader()
reader.onload = (e) => {
if (typeof e.target?.result === 'string') {
updateImages([...props.modelValue, e.target.result])
}
}
reader.readAsDataURL(file)
if (!file.type.startsWith('image/')) {
return
}
const reader = new FileReader()
reader.onload = (e) => {
if (typeof e.target?.result === 'string') {
const newImage: SpriteImage = {
url: e.target.result,
offset: { x: 0, y: 0 }
}
updateImages([...props.modelValue, newImage])
}
}
reader.readAsDataURL(file)
})
}
const updateImages = (newImages: string[]) => {
const updateImages = (newImages: SpriteImage[]) => {
emit('update:modelValue', newImages)
}
@ -94,4 +151,44 @@ const drop = (event: DragEvent, dropIndex: number) => {
}
draggedIndex.value = null
}
const openOffsetModal = (index: number) => {
selectedImageIndex.value = index
tempOffset.value = { ...props.modelValue[index].offset }
}
const closeOffsetModal = () => {
selectedImageIndex.value = null
}
const saveOffset = (index: number) => {
const newImages = [...props.modelValue]
newImages[index] = {
...newImages[index],
offset: { ...tempOffset.value }
}
updateImages(newImages)
closeOffsetModal()
}
const onOffsetChange = () => {
if (selectedImageIndex.value !== null) {
emit('tempOffsetChange', selectedImageIndex.value, tempOffset.value)
}
}
watch(tempOffset, onOffsetChange, { deep: true })
const updateImageDimensions = (event: Event, index: number) => {
const img = event.target as HTMLImageElement
const newImages = [...props.modelValue]
newImages[index] = {
...newImages[index],
dimensions: {
width: img.naturalWidth,
height: img.naturalHeight
}
}
updateImages(newImages)
}
</script>

View File

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

View File

@ -1,12 +1,9 @@
<template>
<div class="h-full overflow-auto">
<div class="relative p-2.5 flex flex-col items-center justify-between h-72">
<div class="filler"></div>
<img class="max-h-72" :src="`${config.server_endpoint}/assets/tiles/${selectedTile?.id}.png`" :alt="'Tile ' + selectedTile?.id" />
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="deleteTile">Delete</button>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
<div class="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray">
<img class="max-h-72" :src="`${config.server_endpoint}/textures/tiles/${selectedTile?.id}.png`" :alt="'Tile ' + selectedTile?.id" />
</div>
<div class="m-2.5 p-2.5 block">
<div class="mt-5 block">
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveTile">
<div class="form-field-full">
<label for="name">Name</label>
@ -16,24 +13,27 @@
<label for="origin-x">Tags</label>
<ChipsInput v-model="tileTags" @update:modelValue="tileTags = $event" />
</div>
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
<div class="flex gap-4">
<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="deleteTile">Delete</button>
</div>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import type { Tile } from '@/types'
import { computed, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { useGameStore } from '@/stores/gameStore'
import config from '@/config'
import config from '@/application/config'
import type { Tile } from '@/application/types'
import ChipsInput from '@/components/forms/ChipsInput.vue'
import { TileStorage } from '@/storage/storages'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore'
import { computed, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue'
const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore()
const zoneEditorStore = useZoneEditorStore()
const tileStorage = new TileStorage()
const selectedTile = computed(() => assetManagerStore.selectedTile)
@ -55,12 +55,13 @@ watch(selectedTile, (tile: Tile | null) => {
tileTags.value = tile.tags
})
function deleteTile() {
gameStore.connection?.emit('gm:tile:delete', { id: selectedTile.value?.id }, (response: boolean) => {
async function deleteTile() {
gameStore.connection?.emit('gm:tile:delete', { id: selectedTile.value?.id }, async (response: boolean) => {
if (!response) {
console.error('Failed to delete tile')
return
}
await tileStorage.delete(selectedTile.value!.id)
refreshTileList()
})
}
@ -72,10 +73,6 @@ function refreshTileList(unsetSelectedTile = true) {
if (unsetSelectedTile) {
assetManagerStore.setSelectedTile(null)
}
if (zoneEditorStore.active) {
zoneEditorStore.setTileList(response)
}
})
}

View File

@ -1,39 +1,39 @@
<template>
<div class="relative p-2.5 flex items-center gap-x-2.5">
<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="upload-asset" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
<label for="upload-asset" class="bg-cyan text-white border border-solid border-white/25 rounded p-2.5 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
<input class="hidden" id="upload-asset" ref="tileUploadField" type="file" accept="image/png" multiple @change="handleFileUpload" />
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
</label>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</div>
<div v-bind="containerProps" class="overflow-y-auto relative" @scroll="onScroll">
<div v-bind="wrapperProps" ref="elementToScroll">
<a v-for="{ data: tile } in list" :key="tile.id" class="relative p-2.5 cursor-pointer block" :class="{ 'bg-cyan/80': assetManagerStore.selectedTile?.id === tile.id }" @click="assetManagerStore.setSelectedTile(tile)">
<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: tile } in list" :key="tile.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedTile?.id === tile.id }" @click="assetManagerStore.setSelectedTile(tile)">
<div class="flex items-center gap-2.5">
<div class="h-7 w-16 max-w-16 flex justify-center">
<img class="h-7" :src="`${config.server_endpoint}/assets/tiles/${tile.id}.png`" alt="Tile" />
<img class="h-7" :src="`${config.server_endpoint}/textures/tiles/${tile.id}.png`" alt="Tile" />
</div>
<span :class="{ 'text-white': assetManagerStore.selectedTile?.id === tile.id }">{{ tile.name }}</span>
<span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedTile?.id === tile.id }">{{ tile.name }}</span>
</div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
</div>
<button class="left-[calc(50%_-_60px)] fixed bottom-2.5 min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
<img class="absolute invert w-8 h-8 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
</button>
<div 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 config from '@/config'
import { useGameStore } from '@/stores/gameStore'
import { onMounted, ref, computed } from 'vue'
import config from '@/application/config'
import type { Tile } from '@/application/types'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import type { Tile } from '@/types'
import { useGameStore } from '@/stores/gameStore'
import { useVirtualList } from '@vueuse/core'
import { computed, onMounted, ref } from 'vue'
const gameStore = useGameStore()
const tileUploadField = ref(null)

View File

@ -0,0 +1,54 @@
<template>
<MapTiles ref="mapTiles" @tileMap:create="tileMap = $event" />
<PlacedMapObjects ref="mapObjects" v-if="tileMap" :tileMap="tileMap as Phaser.Tilemaps.Tilemap" />
<MapEventTiles ref="eventTiles" v-if="tileMap" :tileMap="tileMap as Phaser.Tilemaps.Tilemap" />
</template>
<script setup lang="ts">
import MapEventTiles from '@/components/gameMaster/mapEditor/mapPartials/MapEventTiles.vue'
import MapTiles from '@/components/gameMaster/mapEditor/mapPartials/MapTiles.vue'
import PlacedMapObjects from '@/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { useScene } from 'phavuer'
import { onMounted, onUnmounted, shallowRef, useTemplateRef } from 'vue'
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
const mapEditor = useMapEditorComposable()
const scene = useScene()
const mapTiles = useTemplateRef('mapTiles')
const mapObjects = useTemplateRef('mapObjects')
const eventTiles = useTemplateRef('eventTiles')
function handlePointer(pointer: Phaser.Input.Pointer) {
if (!mapTiles.value || !mapObjects.value || !eventTiles.value) return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if draw mode is tile
switch (mapEditor.drawMode.value) {
case 'tile':
mapTiles.value.handlePointer(pointer)
case 'object':
mapObjects.value.handlePointer(pointer)
case 'teleport':
eventTiles.value.handlePointer(pointer)
}
}
onMounted(() => {
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointer)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, handlePointer)
})
onUnmounted(() => {
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointer)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, handlePointer)
mapEditor.reset()
})
</script>

View File

@ -0,0 +1,100 @@
<template>
<Image v-for="tile in mapEditor.currentMap.value?.mapEventTiles" v-bind="getImageProps(tile)" />
</template>
<script setup lang="ts">
import { MapEventTileType, type MapEventTile, type Map as MapT } from '@/application/types'
import { uuidv4 } from '@/application/utilities'
import { getTile, tileToWorldX, tileToWorldY } from '@/composables/mapComposable'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { Image, useScene } from 'phavuer'
import { shallowRef } from 'vue'
const mapEditor = useMapEditorComposable()
defineExpose({ handlePointer })
const props = defineProps<{
tileMap: Phaser.Tilemaps.Tilemap
}>()
const tileLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
function getImageProps(tile: MapEventTile) {
return {
x: tileToWorldX(props.tileMap, tile.positionX, tile.positionY),
y: tileToWorldY(props.tileMap, tile.positionX, tile.positionY),
texture: tile.type,
depth: 999
}
}
function pencil(pointer: Phaser.Input.Pointer, map: MapT) {
if (!tileLayer.value) return
// Check if there is a tile
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if event tile already exists on position
const existingEventTile = map.mapEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y)
if (existingEventTile) return
// If teleport, check if there is a selected map
if (mapEditor.drawMode.value === 'teleport' && !mapEditor.teleportSettings.value.toMapId) return
const newEventTile = {
id: uuidv4(),
mapId: map?.id,
map: map?.id,
type: mapEditor.drawMode.value === 'blocking tile' ? MapEventTileType.BLOCK : MapEventTileType.TELEPORT,
positionX: tile.x,
positionY: tile.y,
teleport:
mapEditor.drawMode.value === 'teleport'
? {
toMap: mapEditor.teleportSettings.value.toMapId,
toPositionX: mapEditor.teleportSettings.value.toPositionX,
toPositionY: mapEditor.teleportSettings.value.toPositionY,
toRotation: mapEditor.teleportSettings.value.toRotation
}
: undefined
}
map!.mapEventTiles = map!.mapEventTiles.concat(newEventTile as MapEventTile)
}
function erase(pointer: Phaser.Input.Pointer, map: MapT) {
if (!tileLayer.value) return
// Check if there is a tile
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if event tile already exists on position
const existingEventTile = map.mapEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y)
if (!existingEventTile) return
// Remove existing event tile
map.mapEventTiles = map.mapEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id)
}
function handlePointer(pointer: Phaser.Input.Pointer) {
const map = mapEditor.currentMap.value
if (!map) return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
switch (mapEditor.tool.value) {
case 'pencil':
pencil(pointer, map)
break
case 'eraser':
erase(pointer, map)
break
}
}
</script>

View File

@ -0,0 +1,194 @@
<template>
<Controls v-if="tileLayer" :layer="tileLayer" :depth="0" />
</template>
<script setup lang="ts">
import config from '@/application/config'
import Controls from '@/components/utilities/Controls.vue'
import { createTileArray, getTile, placeTile, setLayerTiles } from '@/composables/mapComposable'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { TileStorage } from '@/storage/storages'
import { useScene } from 'phavuer'
import { onMounted, onUnmounted, shallowRef, watch } from 'vue'
import Tileset = Phaser.Tilemaps.Tileset
const emit = defineEmits(['tileMap:create'])
const scene = useScene()
const mapEditor = useMapEditorComposable()
const tileStorage = new TileStorage()
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
const tileLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
defineExpose({ handlePointer })
function createTileMap() {
const mapData = new Phaser.Tilemaps.MapData({
width: mapEditor.currentMap.value?.width,
height: mapEditor.currentMap.value?.height,
tileWidth: config.tile_size.width,
tileHeight: config.tile_size.height,
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
format: Phaser.Tilemaps.Formats.ARRAY_2D
})
const newTileMap = new Phaser.Tilemaps.Tilemap(scene, mapData)
emit('tileMap:create', newTileMap)
return newTileMap
}
async function createTileLayer(currentTileMap: Phaser.Tilemaps.Tilemap) {
const tiles = await tileStorage.getAll()
const tilesetImages = []
for (const tile of tiles) {
tilesetImages.push(currentTileMap.addTilesetImage(tile.id, tile.id, config.tile_size.width, config.tile_size.height, 1, 2, tilesetImages.length + 1, { x: 0, y: -config.tile_size.height }))
}
// Add blank tile
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
}
function pencil(pointer: Phaser.Input.Pointer) {
let map = mapEditor.currentMap.value
if (!map) return
// Check if there is a selected tile
if (!mapEditor.selectedTile.value) return
if (!tileMap.value || !tileLayer.value) return
// Check if there is a tile
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
if (!tile) return
// Place tile
placeTile(tileMap.value, tileLayer.value, tile.x, tile.y, mapEditor.selectedTile.value)
// Adjust mapEditorStore.map.tiles
map.tiles[tile.y][tile.x] = mapEditor.selectedTile.value
}
function eraser(pointer: Phaser.Input.Pointer) {
let map = mapEditor.currentMap.value
if (!map) return
if (!tileMap.value || !tileLayer.value) return
// Check if there is a tile
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
if (!tile) return
// Place tile
placeTile(tileMap.value, tileLayer.value, tile.x, tile.y, 'blank_tile')
// Adjust mapEditorStore.map.tiles
map.tiles[tile.y][tile.x] = 'blank_tile'
}
function paint(pointer: Phaser.Input.Pointer) {
if (!tileMap.value || !tileLayer.value) return
// Set new tileArray with selected tile
const tileArray = createTileArray(tileMap.value.width, tileMap.value.height, mapEditor.selectedTile.value)
setLayerTiles(tileMap.value, tileLayer.value, tileArray)
// Adjust mapEditorStore.map.tiles
if (mapEditor.currentMap.value) {
mapEditor.currentMap.value.tiles = tileArray
}
}
// When alt is pressed, and the pointer is down, select the tile that the pointer is over
function tilePicker(pointer: Phaser.Input.Pointer) {
let map = mapEditor.currentMap.value
if (!map) return
if (!tileMap.value || !tileLayer.value) return
// Check if there is a tile
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
if (!tile) return
// Select the tile
mapEditor.setSelectedTile(map.tiles[tile.y][tile.x])
}
watch(
() => mapEditor.shouldClearTiles,
(shouldClear) => {
if (shouldClear && mapEditor.currentMap.value && tileMap.value && tileLayer.value) {
const blankTiles = createTileArray(tileLayer.value.width, tileLayer.value.height, 'blank_tile')
setLayerTiles(tileMap.value, tileLayer.value, blankTiles)
mapEditor.currentMap.value.tiles = blankTiles
mapEditor.resetClearTilesFlag()
}
}
)
function handlePointer(pointer: Phaser.Input.Pointer) {
if (!tileMap.value || !tileLayer.value) return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if alt is pressed
if (pointer.event.altKey) {
tilePicker(pointer)
return
}
// Check if draw mode is tile
switch (mapEditor.tool.value) {
case 'pencil':
pencil(pointer)
break
case 'eraser':
eraser(pointer)
break
case 'paint':
paint(pointer)
break
}
}
onMounted(async () => {
if (!mapEditor.currentMap.value) return
tileMap.value = createTileMap()
tileLayer.value = await createTileLayer(tileMap.value)
// First fill the entire map with blank tiles using current map dimensions
const blankTiles = createTileArray(mapEditor.currentMap.value.width, mapEditor.currentMap.value.height, 'blank_tile')
// Then overlay the map tiles, but only within the current map dimensions
const mapTiles = mapEditor.currentMap.value.tiles
for (let y = 0; y < mapEditor.currentMap.value.height; y++) {
for (let x = 0; x < mapEditor.currentMap.value.width; x++) {
if (mapTiles[y] && mapTiles[y][x] !== undefined) {
blankTiles[y][x] = mapTiles[y][x]
}
}
}
setLayerTiles(tileMap.value, tileLayer.value, blankTiles)
})
onUnmounted(() => {
if (tileMap.value) {
tileMap.value.destroyLayer('tiles')
tileMap.value.removeAllLayers()
tileMap.value.destroy()
}
})
</script>

View File

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

View File

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

View File

@ -1,9 +1,8 @@
<template>
<Modal :isModalOpen="true" @modal:close="() => zoneEditorStore.toggleCreateZoneModal()" :modal-width="300" :modal-height="420" :is-resizable="false" :disable-bg-texture="true">
<Modal ref="modalRef" :modal-width="300" :modal-height="420" :is-resizable="false" :bg-style="'none'">
<template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">Create new zone</h3>
<h3 class="m-0 font-medium shrink-0 text-white">Create new map</h3>
</template>
<template #modalBody>
<div class="m-4">
<form method="post" @submit.prevent="submit" class="inline">
@ -14,15 +13,15 @@
</div>
<div class="form-field-half">
<label for="name">Width</label>
<input class="input-field max-w-64" v-model="width" name="name" id="name" type="number" />
<input class="input-field max-w-64" v-model="width" name="width" id="width" type="number" />
</div>
<div class="form-field-half">
<label for="name">Height</label>
<input class="input-field max-w-64" v-model="height" name="name" id="name" type="number" />
<input class="input-field max-w-64" v-model="height" name="height" id="height" type="number" />
</div>
<div class="form-field-full">
<label for="name">PVP enabled</label>
<select class="input-field" name="pvp" id="pvp">
<select class="input-field" v-model="pvp" name="pvp" id="pvp">
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>
@ -36,23 +35,52 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { Map } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { MapStorage } from '@/storage/storages'
import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import type { Zone } from '@/types'
import { ref, useTemplateRef } from 'vue'
const emit = defineEmits(['create'])
const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore()
const mapStorage = new MapStorage()
const modalRef = useTemplateRef('modalRef')
const name = ref('')
const width = ref(0)
const height = ref(0)
const pvp = ref(false)
function submit() {
gameStore.connection.emit('gm:zone_editor:zone:create', { name: name.value, width: width.value, height: height.value }, (response: Zone[]) => {
zoneEditorStore.setZoneList(response)
defineExpose({ open: () => modalRef.value?.open() })
async function submit() {
gameStore.connection?.emit('gm:map:create', { name: name.value, width: width.value, height: height.value }, async (response: Map | false) => {
if (!response) {
gameStore.addNotification({
title: 'Error',
message: 'Failed to create map.'
})
return
}
// Reset form
name.value = ''
width.value = 0
height.value = 0
pvp.value = false
// Add map to storage
console.log(response)
await mapStorage.add(response)
// Let list know to fetch new maps
emit('create')
})
zoneEditorStore.toggleCreateZoneModal()
// Close modal
modalRef.value?.close()
}
</script>

View File

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

View File

@ -1,7 +1,7 @@
<template>
<Modal :isModalOpen="zoneEditorStore.isObjectListModalShown" :modal-width="645" :modal-height="260" @modal:close="() => (zoneEditorStore.isObjectListModalShown = false)" :disable-bg-texture="true">
<Modal ref="modalRef" :modal-width="645" :modal-height="260" :bg-style="'none'">
<template #modalHeader>
<h3 class="text-lg text-white">Objects</h3>
<h3 class="text-lg text-white">Map objects</h3>
</template>
<template #modalBody>
<div class="flex pt-4 pl-4">
@ -20,16 +20,16 @@
</div>
<div class="h-full overflow-auto">
<div class="flex justify-between flex-wrap gap-2.5 items-center">
<div v-for="(object, index) in filteredObjects" :key="index" class="max-w-1/4 inline-block">
<div v-for="(mapObject, index) in filteredMapObjects" :key="index" class="max-w-1/4 inline-block">
<img
class="border-2 border-solid max-w-full"
:src="`${config.server_endpoint}/assets/objects/${object.id}.png`"
:src="`${config.server_endpoint}/textures/map_objects/${mapObject.id}.png`"
alt="Object"
@click="zoneEditorStore.setSelectedObject(object)"
@click="mapEditor.setSelectedMapObject(mapObject)"
:class="{
'cursor-pointer transition-all duration-300': true,
'border-cyan shadow-lg scale-105': zoneEditorStore.selectedObject?.id === object.id,
'border-transparent hover:border-gray-300': zoneEditorStore.selectedObject?.id !== object.id
'border-cyan shadow-lg scale-105': mapEditor.selectedMapObject.value?.id === mapObject.id,
'border-transparent hover:border-gray-300': mapEditor.selectedMapObject.value?.id !== mapObject.id
}"
/>
</div>
@ -41,26 +41,34 @@
</template>
<script setup lang="ts">
import config from '@/config'
import { ref, onMounted, computed } from 'vue'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { useGameStore } from '@/stores/gameStore'
import config from '@/application/config'
import type { MapObject } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue'
import type { Object, ZoneObject } from '@/types'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { MapObjectStorage } from '@/storage/storages'
import { liveQuery } from 'dexie'
import { computed, onMounted, onUnmounted, ref, useTemplateRef } from 'vue'
const gameStore = useGameStore()
const mapObjectStorage = new MapObjectStorage()
const isModalOpen = ref(false)
const zoneEditorStore = useZoneEditorStore()
const mapEditor = useMapEditorComposable()
const searchQuery = ref('')
const selectedTags = ref<string[]>([])
const mapObjectList = ref<MapObject[]>([])
const modalRef = useTemplateRef('modalRef')
defineExpose({
open: () => modalRef.value?.open(),
close: () => modalRef.value?.close()
})
const uniqueTags = computed(() => {
const allTags = zoneEditorStore.objectList.flatMap((obj) => obj.tags || [])
const allTags = mapObjectList.value.flatMap((obj) => obj.tags || [])
return Array.from(new Set(allTags))
})
const filteredObjects = computed(() => {
return zoneEditorStore.objectList.filter((object) => {
const filteredMapObjects = computed(() => {
return mapObjectList.value.filter((object) => {
const matchesSearch = !searchQuery.value || object.name.toLowerCase().includes(searchQuery.value.toLowerCase())
const matchesTags = selectedTags.value.length === 0 || (object.tags && selectedTags.value.some((tag) => object.tags.includes(tag)))
return matchesSearch && matchesTags
@ -75,10 +83,23 @@ const toggleTag = (tag: string) => {
}
}
onMounted(async () => {
let subscription: any = null
onMounted(() => {
isModalOpen.value = true
gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => {
zoneEditorStore.setObjectList(response)
subscription = liveQuery(() => mapObjectStorage.liveQuery()).subscribe({
next: (result) => {
mapObjectList.value = result
},
error: (error) => {
console.error('Failed to fetch tiles:', error)
}
})
})
onUnmounted(() => {
if (subscription) {
subscription.unsubscribe()
}
})
</script>

View File

@ -1,7 +1,7 @@
<template>
<Modal :is-modal-open="zoneEditorStore.isSettingsModalShown" @modal:close="() => zoneEditorStore.toggleSettingsModal()" :modal-width="600" :modal-height="350" :disable-bg-texture="true">
<Modal ref="modalRef" :modal-width="600" :modal-height="430" :bg-style="'none'">
<template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">Zone settings</h3>
<h3 class="m-0 font-medium shrink-0 text-white">Map settings</h3>
</template>
<template #modalBody>
@ -34,7 +34,7 @@
</div>
</form>
<form method="post" @submit.prevent="" class="inline" v-if="screen === 'effects'">
<div v-for="(effect, index) in zoneEffects" :key="effect.id" class="mb-2 flex items-center space-x-2 mt-4">
<div v-for="(effect, index) in mapEffects" :key="effect.id" class="mb-2 flex items-center space-x-2 mt-4">
<input class="input-field flex-grow" v-model="effect.effect" placeholder="Effect name" />
<input class="input-field w-20" v-model.number="effect.strength" type="number" placeholder="Strength" />
<button class="btn-red py-1 px-2" type="button" @click="removeEffect(index)">Delete</button>
@ -46,61 +46,57 @@
</Modal>
</template>
<script setup>
import { ref, watch } from 'vue'
<script setup lang="ts">
import type { UUID } from '@/application/types'
import { uuidv4 } from '@/application/utilities'
import Modal from '@/components/utilities/Modal.vue'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { ref, useTemplateRef, watch } from 'vue'
const zoneEditorStore = useZoneEditorStore()
const mapEditor = useMapEditorComposable()
const screen = ref('settings')
zoneEditorStore.setZoneName(zoneEditorStore.zone?.name)
zoneEditorStore.setZoneWidth(zoneEditorStore.zone?.width)
zoneEditorStore.setZoneHeight(zoneEditorStore.zone?.height)
zoneEditorStore.setZonePvp(zoneEditorStore.zone?.pvp)
zoneEditorStore.setZoneEffects(zoneEditorStore.zone?.zoneEffects)
const name = ref(mapEditor.currentMap.value?.name)
const width = ref(mapEditor.currentMap.value?.width)
const height = ref(mapEditor.currentMap.value?.height)
const pvp = ref(mapEditor.currentMap.value?.pvp)
const mapEffects = ref(mapEditor.currentMap.value?.mapEffects || [])
const modalRef = useTemplateRef('modalRef')
const name = ref(zoneEditorStore.zoneSettings?.name)
const width = ref(zoneEditorStore.zoneSettings?.width)
const height = ref(zoneEditorStore.zoneSettings?.height)
const pvp = ref(zoneEditorStore.zoneSettings?.pvp)
const zoneEffects = ref(zoneEditorStore.zoneSettings?.zoneEffects || [])
defineExpose({
open: () => modalRef.value?.open()
})
watch(name, (value) => {
zoneEditorStore.setZoneName(value)
mapEditor.updateProperty('name', value!)
})
watch(width, (value) => {
zoneEditorStore.setZoneWidth(value)
mapEditor.updateProperty('width', value!)
})
watch(height, (value) => {
zoneEditorStore.setZoneHeight(value)
mapEditor.updateProperty('height', value!)
})
watch(pvp, (value) => {
zoneEditorStore.setZonePvp(value)
mapEditor.updateProperty('pvp', value!)
})
watch(
zoneEffects,
(value) => {
zoneEditorStore.setZoneEffects(value)
},
{ deep: true }
)
watch(mapEffects, (value) => {
mapEditor.updateProperty('mapEffects', value!)
})
const addEffect = () => {
zoneEffects.value.push({
id: Date.now().toString(), // Simple unique id generation
zoneId: zoneEditorStore.zone?.id,
zone: zoneEditorStore.zone,
mapEffects.value.push({
id: uuidv4() as UUID, // Simple unique id generation
map: mapEditor.currentMap.value!,
effect: '',
strength: 1
})
}
const removeEffect = (index) => {
zoneEffects.value.splice(index, 1)
const removeEffect = (index: number) => {
mapEffects.value.splice(index, 1)
}
</script>

View File

@ -11,23 +11,23 @@
</template>
<script setup lang="ts">
import type { ZoneObject } from '@/types'
import type { PlacedMapObject } from '@/application/types'
const props = defineProps<{
zoneObject: ZoneObject
placedMapObject: PlacedMapObject
}>()
const emit = defineEmits(['move', 'rotate', 'delete'])
const handleMove = () => {
emit('move', props.zoneObject.id)
emit('move', props.placedMapObject.id)
}
const handleRotate = () => {
emit('rotate', props.zoneObject.id)
emit('rotate', props.placedMapObject.id)
}
const handleDelete = () => {
emit('delete', props.zoneObject.id)
emit('delete', props.placedMapObject.id)
}
</script>

View File

@ -1,5 +1,5 @@
<template>
<Modal :is-modal-open="showTeleportModal" @modal:close="() => zoneEditorStore.setTool('move')" :modal-width="300" :modal-height="350" :is-resizable="false" :disable-bg-texture="true">
<Modal ref="modalRef" @modal:close="() => mapEditorStore.setTool('move')" :modal-width="300" :modal-height="350" :is-resizable="false" :bg-style="'none'">
<template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">Teleport settings</h3>
</template>
@ -25,10 +25,10 @@
</select>
</div>
<div class="form-field-full">
<label for="toZoneId">Zone to teleport to</label>
<select v-model="toZoneId" class="input-field" name="toZoneId" id="toZoneId">
<option :value="0">Select zone</option>
<option v-for="zone in zoneEditorStore.zoneList" :key="zone.id" :value="zone.id">{{ zone.name }}</option>
<label for="toMap">Map to teleport to</label>
<select v-model="toMap" class="input-field" name="toMap" id="toMap">
<option :value="null">Select map</option>
<option v-for="map in mapList" :key="map.id" :value="map">{{ map.name }}</option>
</select>
</div>
</div>
@ -39,44 +39,50 @@
</template>
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import type { Map } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { useGameStore } from '@/stores/gameStore'
import type { Zone } from '@/types'
import { useMapEditorStore } from '@/stores/mapEditorStore'
import { computed, onMounted, ref, useTemplateRef, watch } from 'vue'
const showTeleportModal = computed(() => zoneEditorStore.tool === 'pencil' && zoneEditorStore.drawMode === 'teleport')
const zoneEditorStore = useZoneEditorStore()
const showTeleportModal = computed(() => mapEditorStore.tool === 'pencil' && mapEditorStore.drawMode === 'teleport')
const mapEditorStore = useMapEditorStore()
const gameStore = useGameStore()
const mapList = ref<Map[]>([])
const modalRef = useTemplateRef('modalRef')
onMounted(fetchZones)
defineExpose({
open: () => modalRef.value?.open()
})
function fetchZones() {
gameStore.connection?.emit('gm:zone_editor:zone:list', {}, (response: Zone[]) => {
zoneEditorStore.setZoneList(response)
onMounted(fetchMaps)
function fetchMaps() {
gameStore.connection?.emit('gm:map:list', {}, (response: Map[]) => {
mapList.value = response
})
}
const { toPositionX, toPositionY, toRotation, toZoneId } = useRefTeleportSettings()
const { toPositionX, toPositionY, toRotation, toMap } = useRefTeleportSettings()
function useRefTeleportSettings() {
const settings = zoneEditorStore.teleportSettings
const settings = mapEditorStore.teleportSettings
return {
toPositionX: ref(settings.toPositionX),
toPositionY: ref(settings.toPositionY),
toRotation: ref(settings.toRotation),
toZoneId: ref(settings.toZoneId)
toMap: ref(settings.toMapId)
}
}
watch([toPositionX, toPositionY, toRotation, toZoneId], updateTeleportSettings)
watch([toPositionX, toPositionY, toRotation, toMap], updateTeleportSettings)
function updateTeleportSettings() {
zoneEditorStore.setTeleportSettings({
mapEditorStore.setTeleportSettings({
toPositionX: toPositionX.value,
toPositionY: toPositionY.value,
toRotation: toRotation.value,
toZoneId: toZoneId.value
toMapId: toMap.value
})
}
</script>

View File

@ -1,5 +1,5 @@
<template>
<Modal :isModalOpen="zoneEditorStore.isTileListModalShown" :modal-width="645" :modal-height="600" @modal:close="() => (zoneEditorStore.isTileListModalShown = false)" :disable-bg-texture="true">
<Modal ref="modalRef" :modal-width="645" :modal-height="600" :bg-style="'none'">
<template #modalHeader>
<h3 class="text-lg text-white">Tiles</h3>
</template>
@ -24,7 +24,7 @@
<div v-for="group in groupedTiles" :key="group.parent.id" class="flex flex-col items-center justify-center relative">
<img
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
:src="`${config.server_endpoint}/assets/tiles/${group.parent.id}.png`"
:src="`${config.server_endpoint}/textures/tiles/${group.parent.id}.png`"
:alt="group.parent.name"
@click="openGroup(group)"
@load="() => processTile(group.parent)"
@ -50,7 +50,7 @@
<div class="flex flex-col items-center justify-center">
<img
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
:src="`${config.server_endpoint}/assets/tiles/${selectedGroup.parent.id}.png`"
:src="`${config.server_endpoint}/textures/tiles/${selectedGroup.parent.id}.png`"
:alt="selectedGroup.parent.name"
@click="selectTile(selectedGroup.parent.id)"
:class="{
@ -63,7 +63,7 @@
<div v-for="childTile in selectedGroup.children" :key="childTile.id" class="flex flex-col items-center justify-center">
<img
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
:src="`${config.server_endpoint}/assets/tiles/${childTile.id}.png`"
:src="`${config.server_endpoint}/textures/tiles/${childTile.id}.png`"
:alt="childTile.name"
@click="selectTile(childTile.id)"
:class="{
@ -81,29 +81,36 @@
</template>
<script setup lang="ts">
import config from '@/config'
import { ref, onMounted, computed } from 'vue'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { useGameStore } from '@/stores/gameStore'
import config from '@/application/config'
import type { Tile } from '@/application/types'
import Modal from '@/components/utilities/Modal.vue'
import type { Tile } from '@/types'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { TileStorage } from '@/storage/storages'
import { liveQuery } from 'dexie'
import { computed, onMounted, onUnmounted, ref, useTemplateRef } from 'vue'
const gameStore = useGameStore()
const isModalOpen = ref(false)
const zoneEditorStore = useZoneEditorStore()
const tileStorage = new TileStorage()
const mapEditor = useMapEditorComposable()
const searchQuery = ref('')
const selectedTags = ref<string[]>([])
const tileCategories = ref<Map<string, string>>(new Map())
const selectedGroup = ref<{ parent: Tile; children: Tile[] } | null>(null)
const tiles = ref<Tile[]>([])
const modalRef = useTemplateRef('modalRef')
defineExpose({
open: () => modalRef.value?.open(),
close: () => modalRef.value?.close()
})
const uniqueTags = computed(() => {
const allTags = zoneEditorStore.tileList.flatMap((tile) => tile.tags || [])
const allTags = tiles.value.flatMap((tile) => tile.tags || [])
return Array.from(new Set(allTags))
})
const groupedTiles = computed(() => {
const groups: { parent: Tile; children: Tile[] }[] = []
const filteredTiles = zoneEditorStore.tileList.filter((tile) => {
const filteredTiles = tiles.value.filter((tile) => {
const matchesSearch = !searchQuery.value || tile.name.toLowerCase().includes(searchQuery.value.toLowerCase())
const matchesTags = selectedTags.value.length === 0 || (tile.tags && selectedTags.value.some((tag) => tile.tags.includes(tag)))
return matchesSearch && matchesTags
@ -169,7 +176,7 @@ function processTile(tile: Tile) {
tileColorData.value.set(tile.id, getDominantColor(imageData))
tileEdgeData.value.set(tile.id, getEdgeComplexity(imageData))
}
img.src = `${config.server_endpoint}/assets/tiles/${tile.id}.png`
img.src = `${config.server_endpoint}/textures/tiles/${tile.id}.png`
}
function getDominantColor(imageData: ImageData) {
@ -177,6 +184,7 @@ function getDominantColor(imageData: ImageData) {
g = 0,
b = 0,
total = 0
for (let i = 0; i < imageData.data.length; i += 4) {
if (imageData.data[i + 3] > 0) {
// Only consider non-transparent pixels
@ -186,6 +194,7 @@ function getDominantColor(imageData: ImageData) {
total++
}
}
return {
r: Math.round(r / total),
g: Math.round(g / total),
@ -219,18 +228,29 @@ function closeGroup() {
}
function selectTile(tile: string) {
zoneEditorStore.setSelectedTile(tile)
mapEditor.setSelectedTile(tile)
}
function isActiveTile(tile: Tile): boolean {
return zoneEditorStore.selectedTile === tile.id
return mapEditor.selectedTile.value === tile.id
}
onMounted(async () => {
isModalOpen.value = true
gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => {
zoneEditorStore.setTileList(response)
response.forEach((tile) => processTile(tile))
let subscription: any = null
onMounted(() => {
subscription = liveQuery(() => tileStorage.liveQuery()).subscribe({
next: (result) => {
tiles.value = result
},
error: (error) => {
console.error('Failed to fetch tiles:', error)
}
})
})
onUnmounted(() => {
if (subscription) {
subscription.unsubscribe()
}
})
</script>

View File

@ -1,94 +1,99 @@
<template>
<div class="flex justify-center p-5">
<div class="toolbar fixed bottom-0 left-0 m-3 rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10">
<div ref="toolbar" class="tools flex gap-2.5" v-if="zoneEditorStore.zone">
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': zoneEditorStore.tool === 'move' }" @click="handleClick('move')">
<img class="invert w-5 h-5" src="/assets/icons/zoneEditor/move.svg" alt="Move camera" /> <span class="h-5" :class="{ 'ml-2.5': zoneEditorStore.tool !== 'move' }">(M)</span>
<div ref="toolbar" class="tools flex gap-2.5" v-if="mapEditor.currentMap.value">
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditor.tool.value === 'move' }" @click="handleClick('move')">
<img class="invert w-5 h-5" src="/assets/icons/mapEditor/move.svg" alt="Move camera" /> <span class="h-5" :class="{ 'ml-2.5': mapEditor.tool.value !== 'move' }">(M)</span>
</button>
<div class="w-px bg-cyan"></div>
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': zoneEditorStore.tool === 'pencil' }" @click="handleClick('pencil')">
<img class="invert w-5 h-5" src="/assets/icons/zoneEditor/pencil.svg" alt="Pencil" /> <span class="h-5" :class="{ 'ml-2.5': zoneEditorStore.tool !== 'pencil' }">(P)</span>
<div class="select" v-if="zoneEditorStore.tool === 'pencil'">
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditor.tool.value === 'pencil' }" @click="handleClick('pencil')">
<img class="invert w-5 h-5" src="/assets/icons/mapEditor/pencil.svg" alt="Pencil" /> <span class="h-5" :class="{ 'ml-2.5': mapEditor.tool.value !== 'pencil' }">(P)</span>
<div class="select" v-if="mapEditor.tool.value === 'pencil'">
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectPencilOpen }">
{{ zoneEditorStore.drawMode }}
<img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/zoneEditor/chevron.svg" />
{{ mapEditor.drawMode.value.replace('_', ' ') }}
<img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/mapEditor/chevron.svg" alt="" />
</div>
<div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectPencilOpen && zoneEditorStore.tool === 'pencil'">
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('tile')">
<div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectPencilOpen && mapEditor.tool.value === 'pencil'">
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('tile', 'pencil')">
Tile
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('object')">
Object
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('map_object', 'pencil')">
Map object
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('teleport')">
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('teleport', 'pencil')">
Teleport
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setDrawMode('blocking tile')">Blocking tile</span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('blocking tile', 'pencil')">Blocking tile</span>
</div>
</div>
</button>
<div class="w-px bg-cyan"></div>
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': zoneEditorStore.tool === 'eraser' }" @click="handleClick('eraser')">
<img class="invert w-5 h-5" src="/assets/icons/zoneEditor/eraser.svg" alt="Eraser" /> <span class="h-5" :class="{ 'ml-2.5': zoneEditorStore.tool !== 'eraser' }">(E)</span>
<div class="select" v-if="zoneEditorStore.tool === 'eraser'">
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditor.tool.value === 'eraser' }" @click="handleClick('eraser')">
<img class="invert w-5 h-5" src="/assets/icons/mapEditor/eraser.svg" alt="Eraser" /> <span class="h-5" :class="{ 'ml-2.5': mapEditor.tool.value !== 'eraser' }">(E)</span>
<div class="select" v-if="mapEditor.tool.value === 'eraser'">
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectEraserOpen }">
{{ zoneEditorStore.eraserMode }}
<img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/zoneEditor/chevron.svg" />
{{ mapEditor.drawMode.value.replace('_', ' ') }}
<img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/mapEditor/chevron.svg" />
</div>
<div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectEraserOpen">
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('tile')">
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('tile', 'eraser')">
Tile
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('object')">
Object
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('map_object', 'eraser')">
Map object
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('teleport')">
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('teleport', 'eraser')">
Teleport
<div class="absolute w-4/5 left-1/2 -translate-x-1/2 bottom-0 h-px bg-cyan"></div>
</span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="setEraserMode('blocking tile')">Blocking tile</span>
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('blocking tile', 'eraser')">Blocking tile</span>
</div>
</div>
</button>
<div class="w-px bg-cyan"></div>
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': zoneEditorStore.tool === 'paint' }" @click="handleClick('paint')">
<img class="invert w-5 h-5" src="/assets/icons/zoneEditor/paint.svg" alt="Paint bucket" /> <span class="h-5" :class="{ 'ml-2.5': zoneEditorStore.tool !== 'paint' }">(B)</span>
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditor.tool.value === 'paint' }" @click="handleClick('paint')">
<img class="invert w-5 h-5" src="/assets/icons/mapEditor/paint.svg" alt="Paint bucket" /> <span class="h-5" :class="{ 'ml-2.5': mapEditor.tool.value !== 'paint' }">(B)</span>
</button>
<div class="w-px bg-cyan"></div>
<button class="flex justify-center items-center min-w-10 p-0 relative" @click="handleClick('settings')" v-if="zoneEditorStore.zone"><img class="invert w-5 h-5" src="/assets/icons/zoneEditor/gear.svg" alt="Zone settings" /> <span class="h-5 ml-2.5">(Z)</span></button>
<button class="flex justify-center items-center min-w-10 p-0 relative" @click="handleClick('settings')"><img class="invert w-5 h-5" src="/assets/icons/mapEditor/gear.svg" alt="Map settings" /> <span class="h-5 ml-2.5">(Z)</span></button>
<div class="w-px bg-cyan"></div>
<label class="my-auto gap-0" for="checkbox">Continuous Drawing</label>
<input type="checkbox" />
</div>
<div class="toolbar fixed bottom-0 right-0 m-3 rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10 space-x-2">
<button class="btn-cyan px-3.5" @click="() => zoneEditorStore.toggleZoneListModal()">Load</button>
<button class="btn-cyan px-3.5" @click="() => emit('save')" v-if="zoneEditorStore.zone">Save</button>
<button class="btn-cyan px-3.5" @click="() => emit('clear')" v-if="zoneEditorStore.zone">Clear</button>
<button class="btn-cyan px-3.5" @click="() => zoneEditorStore.toggleActive()">Exit</button>
<button class="btn-cyan px-3.5" @click="() => emit('open-maps')">Load</button>
<button class="btn-cyan px-3.5" @click="() => emit('save')" v-if="mapEditor.currentMap.value">Save</button>
<button class="btn-cyan px-3.5" @click="() => emit('clear')" v-if="mapEditor.currentMap.value">Clear</button>
<button class="btn-cyan px-3.5" @click="() => emit('close-editor')">Exit</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
import { onClickOutside } from '@vueuse/core'
import { onBeforeUnmount, onMounted, ref } from 'vue'
const zoneEditorStore = useZoneEditorStore()
const mapEditor = useMapEditorComposable()
const emit = defineEmits(['save', 'clear'])
const emit = defineEmits(['save', 'clear', 'open-maps', 'open-settings', 'close-editor', 'open-tile-list', 'open-map-object-list', 'close-lists'])
// track when clicked outside of toolbar items
const toolbar = ref(null)
@ -97,26 +102,45 @@ const toolbar = ref(null)
let selectPencilOpen = ref(false)
let selectEraserOpen = ref(false)
let tileListShown = ref(false)
let mapObjectListShown = ref(false)
defineExpose({ tileListShown, mapObjectListShown })
// drawMode
function setDrawMode(value: string) {
zoneEditorStore.isTileListModalShown = value === 'tile'
zoneEditorStore.isObjectListModalShown = value === 'object'
if (mapEditor.tool.value === 'paint' || mapEditor.tool.value === 'pencil') {
emit('close-lists')
if (value === 'tile') emit('open-tile-list')
if (value === 'map_object') emit('open-map-object-list')
}
zoneEditorStore.setDrawMode(value)
mapEditor.setDrawMode(value)
selectPencilOpen.value = false
selectEraserOpen.value = false
}
function setPencilMode() {
mapEditor.setTool('pencil')
selectPencilOpen.value = false
}
// drawMode
function setEraserMode(value: string) {
zoneEditorStore.setEraserMode(value)
function setEraserMode() {
mapEditor.setTool('eraser')
selectEraserOpen.value = false
}
function handleModeClick(mode: string, type: 'pencil' | 'eraser') {
setDrawMode(mode)
type === 'pencil' ? setPencilMode() : setEraserMode()
}
function handleClick(tool: string) {
if (tool === 'settings') {
zoneEditorStore.toggleSettingsModal()
emit('open-settings')
} else {
zoneEditorStore.setTool(tool)
mapEditor.setTool(tool)
}
selectPencilOpen.value = tool === 'pencil' ? !selectPencilOpen.value : false
@ -124,22 +148,17 @@ function handleClick(tool: string) {
}
function cycleToolMode(tool: 'pencil' | 'eraser') {
const modes = ['tile', 'object', 'teleport', 'blocking tile']
const currentMode = tool === 'pencil' ? zoneEditorStore.drawMode : zoneEditorStore.eraserMode
const currentIndex = modes.indexOf(currentMode)
const modes = ['tile', 'map_object', 'teleport', 'blocking tile']
const currentIndex = modes.indexOf(mapEditor.drawMode.value)
const nextIndex = (currentIndex + 1) % modes.length
const nextMode = modes[nextIndex]
if (tool === 'pencil') {
setDrawMode(nextMode)
} else {
setEraserMode(nextMode)
}
setDrawMode(nextMode)
}
function initKeyShortcuts(event: KeyboardEvent) {
// Check if zone is set
if (!zoneEditorStore.zone) return
// Check if map is set
if (!mapEditor.currentMap.value) return
// prevent if focused on composables
if (document.activeElement?.tagName === 'INPUT') return
@ -154,7 +173,7 @@ function initKeyShortcuts(event: KeyboardEvent) {
if (keyActions.hasOwnProperty(event.key)) {
const tool = keyActions[event.key]
if ((tool === 'pencil' || tool === 'eraser') && zoneEditorStore.tool === tool) {
if ((tool === 'pencil' || tool === 'eraser') && mapEditor.tool.value === tool) {
cycleToolMode(tool)
} else {
handleClick(tool)

View File

@ -1,73 +0,0 @@
<template>
<ZoneTiles @tileMap:create="tileMap = $event" />
<ZoneObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
<ZoneEventTiles v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
<Toolbar @save="save" @clear="clear" />
<ZoneList />
<TileList />
<ObjectList />
<ZoneSettings />
<TeleportModal />
</template>
<script setup lang="ts">
import { onUnmounted, ref } from 'vue'
import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { type Zone } from '@/types'
// Components
import Toolbar from '@/components/gameMaster/zoneEditor/partials/Toolbar.vue'
import TileList from '@/components/gameMaster/zoneEditor/partials/TileList.vue'
import ObjectList from '@/components/gameMaster/zoneEditor/partials/ObjectList.vue'
import ZoneSettings from '@/components/gameMaster/zoneEditor/partials/ZoneSettings.vue'
import ZoneList from '@/components/gameMaster/zoneEditor/partials/ZoneList.vue'
import TeleportModal from '@/components/gameMaster/zoneEditor/partials/TeleportModal.vue'
import ZoneTiles from '@/components/gameMaster/zoneEditor/zonePartials/ZoneTiles.vue'
import ZoneObjects from '@/components/gameMaster/zoneEditor/zonePartials/ZoneObjects.vue'
import ZoneEventTiles from '@/components/gameMaster/zoneEditor/zonePartials/ZoneEventTiles.vue'
const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore()
const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null)
function clear() {
if (!zoneEditorStore.zone) return
// Clear objects, event tiles and tiles
zoneEditorStore.zone.zoneObjects = []
zoneEditorStore.zone.zoneEventTiles = []
zoneEditorStore.triggerClearTiles()
}
function save() {
if (!zoneEditorStore.zone) return
const data = {
zoneId: zoneEditorStore.zone.id,
name: zoneEditorStore.zoneSettings.name,
width: zoneEditorStore.zoneSettings.width,
height: zoneEditorStore.zoneSettings.height,
tiles: zoneEditorStore.zone.tiles,
pvp: zoneEditorStore.zone.pvp,
zoneEffects: zoneEditorStore.zone.zoneEffects.map(({ id, zoneId, effect, strength }) => ({ id, zoneId, effect, strength })),
zoneEventTiles: zoneEditorStore.zone.zoneEventTiles.map(({ id, zoneId, type, positionX, positionY, teleport }) => ({ id, zoneId, type, positionX, positionY, teleport })),
zoneObjects: zoneEditorStore.zone.zoneObjects.map(({ id, zoneId, objectId, depth, isRotated, positionX, positionY }) => ({ id, zoneId, objectId, depth, isRotated, positionX, positionY }))
}
if (zoneEditorStore.isSettingsModalShown) {
zoneEditorStore.toggleSettingsModal()
}
gameStore.connection?.emit('gm:zone_editor:zone:update', data, (response: Zone) => {
zoneEditorStore.setZone(response)
})
}
onUnmounted(() => {
zoneEditorStore.reset()
})
</script>

View File

@ -1,61 +0,0 @@
<template>
<CreateZone v-if="zoneEditorStore.isCreateZoneModalShown" />
<Modal :is-modal-open="zoneEditorStore.isZoneListModalShown" @modal:close="() => zoneEditorStore.toggleZoneListModal()" :is-resizable="false" :modal-width="300" :modal-height="360" :disable-bg-texture="true">
<template #modalHeader>
<h3 class="text-lg text-white">Zones</h3>
</template>
<template #modalBody>
<div class="my-4 mx-auto">
<div class="text-center mb-4 px-2 flex gap-2.5">
<button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="fetchZones">Refresh</button>
<button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="() => zoneEditorStore.toggleCreateZoneModal()">New</button>
</div>
<div class="relative p-2.5 cursor-pointer flex gap-y-2.5 gap-x-5 flex-wrap" v-for="(zone, index) in zoneEditorStore.zoneList" :key="zone.id">
<div class="absolute left-0 top-0 w-full h-px bg-gray-500" v-if="index === 0"></div>
<div class="flex gap-3 items-center w-full" @click="() => loadZone(zone.id)">
<span>{{ zone.name }}</span>
<span class="ml-auto gap-1 flex">
<button class="btn-red w-7 h-7 z-50 flex items-center justify-center" @click.stop="() => deleteZone(zone.id)">x</button>
</span>
</div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</div>
</div>
</template>
</Modal>
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
import { useGameStore } from '@/stores/gameStore'
import Modal from '@/components/utilities/Modal.vue'
import type { Zone } from '@/types'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import CreateZone from '@/components/gameMaster/zoneEditor/partials/CreateZone.vue'
const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore()
onMounted(async () => {
fetchZones()
})
function fetchZones() {
gameStore.connection?.emit('gm:zone_editor:zone:list', {}, (response: Zone[]) => {
zoneEditorStore.setZoneList(response)
})
}
function loadZone(id: number) {
gameStore.connection?.emit('gm:zone_editor:zone:request', { zoneId: id }, (response: Zone) => {
zoneEditorStore.setZone(response)
})
zoneEditorStore.toggleZoneListModal()
}
function deleteZone(id: number) {
gameStore.connection?.emit('gm:zone_editor:zone:delete', { zoneId: id }, () => {
fetchZones()
})
}
</script>

View File

@ -1,118 +0,0 @@
<template>
<Image v-for="tile in zoneEditorStore.zone?.zoneEventTiles" v-bind="getImageProps(tile)" />
</template>
<script setup lang="ts">
import { type ZoneEventTile, ZoneEventTileType } from '@/types'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { Image, useScene } from 'phavuer'
import { getTile, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
import { uuidv4 } from '@/utilities'
import { onMounted, onUnmounted } from 'vue'
const scene = useScene()
const zoneEditorStore = useZoneEditorStore()
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
}>()
function getImageProps(tile: ZoneEventTile) {
return {
x: tileToWorldX(props.tilemap, tile.positionX, tile.positionY),
y: tileToWorldY(props.tilemap, tile.positionX, tile.positionY),
texture: tile.type,
depth: 999
}
}
function pencil(pointer: Phaser.Input.Pointer) {
// Check if zone is set
if (!zoneEditorStore.zone) return
// Check if tool is pencil
if (zoneEditorStore.tool !== 'pencil') return
// Check if draw mode is blocking tile or teleport
if (zoneEditorStore.drawMode !== 'blocking tile' && zoneEditorStore.drawMode !== 'teleport') return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if there is a tile
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if event tile already exists on position
const existingEventTile = zoneEditorStore.zone.zoneEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y)
if (existingEventTile) return
// If teleport, check if there is a selected zone
if (zoneEditorStore.drawMode === 'teleport' && !zoneEditorStore.teleportSettings.toZoneId) return
const newEventTile = {
id: uuidv4(),
zoneId: zoneEditorStore.zone.id,
zone: zoneEditorStore.zone,
type: zoneEditorStore.drawMode === 'blocking tile' ? ZoneEventTileType.BLOCK : ZoneEventTileType.TELEPORT,
positionX: tile.x,
positionY: tile.y,
teleport:
zoneEditorStore.drawMode === 'teleport'
? {
toZoneId: zoneEditorStore.teleportSettings.toZoneId,
toPositionX: zoneEditorStore.teleportSettings.toPositionX,
toPositionY: zoneEditorStore.teleportSettings.toPositionY,
toRotation: zoneEditorStore.teleportSettings.toRotation
}
: undefined
}
zoneEditorStore.zone.zoneEventTiles = zoneEditorStore.zone.zoneEventTiles.concat(newEventTile as ZoneEventTile)
}
function eraser(pointer: Phaser.Input.Pointer) {
// Check if zone is set
if (!zoneEditorStore.zone) return
// Check if tool is pencil
if (zoneEditorStore.tool !== 'eraser') return
// Check if draw mode is blocking tile or teleport
if (zoneEditorStore.eraserMode !== 'blocking tile' && zoneEditorStore.eraserMode !== 'teleport') return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if there is a tile
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if event tile already exists on position
const existingEventTile = zoneEditorStore.zone.zoneEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y)
if (!existingEventTile) return
// Remove existing event tile
zoneEditorStore.zone.zoneEventTiles = zoneEditorStore.zone.zoneEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id)
}
onMounted(() => {
scene.input.on(Phaser.Input.Events.POINTER_DOWN, pencil)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, eraser)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
})
onUnmounted(() => {
scene.input.off(Phaser.Input.Events.POINTER_DOWN, pencil)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, eraser)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
})
</script>

View File

@ -1,45 +0,0 @@
<template>
<Image v-if="gameStore.getLoadedAsset(props.zoneObject.object.id)" v-bind="imageProps" />
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { Image, useScene } from 'phavuer'
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
import { loadTexture } from '@/composables/gameComposable'
import type { AssetDataT, ZoneObject } from '@/types'
import { useGameStore } from '@/stores/gameStore'
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
zoneObject: ZoneObject
selectedZoneObject: ZoneObject | null
movingZoneObject: ZoneObject | null
}>()
const gameStore = useGameStore()
const scene = useScene()
const imageProps = computed(() => ({
alpha: props.movingZoneObject?.id === props.zoneObject.id ? 0.5 : 1,
tint: props.selectedZoneObject?.id === props.zoneObject.id ? 0x00ff00 : 0xffffff,
depth: calculateIsometricDepth(props.zoneObject.positionX, props.zoneObject.positionY, props.zoneObject.object.frameWidth, props.zoneObject.object.frameHeight),
x: tileToWorldX(props.tilemap, props.zoneObject.positionX, props.zoneObject.positionY),
y: tileToWorldY(props.tilemap, props.zoneObject.positionX, props.zoneObject.positionY),
flipX: props.zoneObject.isRotated,
texture: props.zoneObject.object.id,
originY: Number(props.zoneObject.object.originX),
originX: Number(props.zoneObject.object.originY)
}))
loadTexture(scene, {
key: props.zoneObject.object.id,
data: '/assets/objects/' + props.zoneObject.object.id + '.png',
group: 'objects',
updatedAt: props.zoneObject.object.updatedAt,
frameWidth: props.zoneObject.object.frameWidth,
frameHeight: props.zoneObject.object.frameHeight
} as AssetDataT).catch((error) => {
console.error('Error loading texture:', error)
})
</script>

View File

@ -1,248 +0,0 @@
<template>
<SelectedZoneObject v-if="selectedZoneObject" :zoneObject="selectedZoneObject" :movingZoneObject="movingZoneObject" @move="moveZoneObject" @rotate="rotateZoneObject" @delete="deleteZoneObject" />
<ZoneObject v-for="zoneObject in zoneEditorStore.zone?.zoneObjects" :tilemap="tilemap" :zoneObject :selectedZoneObject :movingZoneObject @pointerup="clickZoneObject(zoneObject)" />
</template>
<script setup lang="ts">
import { uuidv4 } from '@/utilities'
import { getTile } from '@/composables/zoneComposable'
import { useScene } from 'phavuer'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import SelectedZoneObject from '@/components/gameMaster/zoneEditor/partials/SelectedZoneObject.vue'
import { onMounted, onUnmounted, ref, watch } from 'vue'
import ZoneObject from '@/components/gameMaster/zoneEditor/zonePartials/ZoneObject.vue'
import type { ZoneObject as ZoneObjectT } from '@/types'
const scene = useScene()
const zoneEditorStore = useZoneEditorStore()
const selectedZoneObject = ref<ZoneObjectT | null>(null)
const movingZoneObject = ref<ZoneObjectT | null>(null)
const props = defineProps<{
tilemap: Phaser.Tilemaps.Tilemap
}>()
function pencil(pointer: Phaser.Input.Pointer) {
// Check if zone is set
if (!zoneEditorStore.zone) return
// Check if tool is pencil
if (zoneEditorStore.tool !== 'pencil') return
// Check if draw mode is object
if (zoneEditorStore.drawMode !== 'object') return
// Check if there is a selected object
if (!zoneEditorStore.selectedObject) return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if alt is pressed, this means we are selecting the object
if (pointer.event.altKey) return
// Check if there is a tile
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if object already exists on position
const existingObject = zoneEditorStore.zone?.zoneObjects.find((object) => object.positionX === tile.x && object.positionY === tile.y)
if (existingObject) return
const newObject = {
id: uuidv4(),
zoneId: zoneEditorStore.zone.id,
zone: zoneEditorStore.zone,
objectId: zoneEditorStore.selectedObject.id,
object: zoneEditorStore.selectedObject,
depth: 0,
isRotated: false,
positionX: tile.x,
positionY: tile.y
}
// Add new object to zoneObjects
zoneEditorStore.zone.zoneObjects = zoneEditorStore.zone.zoneObjects.concat(newObject as ZoneObjectT)
}
function eraser(pointer: Phaser.Input.Pointer) {
// Check if zone is set
if (!zoneEditorStore.zone) return
// Check if tool is eraser
if (zoneEditorStore.tool !== 'eraser') return
// Check if draw mode is object
if (zoneEditorStore.eraserMode !== 'object') return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if alt is pressed, this means we are selecting the object
if (pointer.event.altKey) return
// Check if there is a tile
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if object already exists on position
const existingObject = zoneEditorStore.zone.zoneObjects.find((object) => object.positionX === tile.x && object.positionY === tile.y)
if (!existingObject) return
// Remove existing object
zoneEditorStore.zone.zoneObjects = zoneEditorStore.zone.zoneObjects.filter((object) => object.id !== existingObject.id)
}
function objectPicker(pointer: Phaser.Input.Pointer) {
// Check if zone is set
if (!zoneEditorStore.zone) return
// Check if tool is pencil
if (zoneEditorStore.tool !== 'pencil') return
// Check if draw mode is object
if (zoneEditorStore.drawMode !== 'object') return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// If alt is not pressed, return
if (!pointer.event.altKey) return
// Check if there is a tile
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
// Check if object already exists on position
const existingObject = zoneEditorStore.zone.zoneObjects.find((object) => object.positionX === tile.x && object.positionY === tile.y)
if (!existingObject) return
// Select the object
zoneEditorStore.setSelectedObject(existingObject)
}
function moveZoneObject(id: string) {
// Check if zone is set
if (!zoneEditorStore.zone) return
movingZoneObject.value = zoneEditorStore.zone.zoneObjects.find((object) => object.id === id) as ZoneObjectT
function handlePointerMove(pointer: Phaser.Input.Pointer) {
if (!movingZoneObject.value) return
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
if (!tile) return
movingZoneObject.value.positionX = tile.x
movingZoneObject.value.positionY = tile.y
}
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
function handlePointerUp() {
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
movingZoneObject.value = null
}
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
}
function rotateZoneObject(id: string) {
// Check if zone is set
if (!zoneEditorStore.zone) return
zoneEditorStore.zone.zoneObjects = zoneEditorStore.zone.zoneObjects.map((object) => {
if (object.id === id) {
return {
...object,
isRotated: !object.isRotated
}
}
return object
})
}
function deleteZoneObject(id: string) {
// Check if zone is set
if (!zoneEditorStore.zone) return
zoneEditorStore.zone.zoneObjects = zoneEditorStore.zone.zoneObjects.filter((object) => object.id !== id)
selectedZoneObject.value = null
}
function clickZoneObject(zoneObject: ZoneObjectT) {
selectedZoneObject.value = zoneObject
// If alt is pressed, select the object
if (scene.input.activePointer.event.altKey) {
zoneEditorStore.setSelectedObject(zoneObject.object)
}
}
onMounted(() => {
scene.input.on(Phaser.Input.Events.POINTER_DOWN, pencil)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, eraser)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, objectPicker)
})
onUnmounted(() => {
scene.input.off(Phaser.Input.Events.POINTER_DOWN, pencil)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, eraser)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, objectPicker)
})
// watch zoneEditorStore.objectList and update originX and originY of objects in zoneObjects
watch(
() => zoneEditorStore.objectList,
(newObjects) => {
if (!zoneEditorStore.zone) return
const updatedZoneObjects = zoneEditorStore.zone.zoneObjects.map((zoneObject) => {
const updatedObject = newObjects.find((obj) => obj.id === zoneObject.object.id)
if (updatedObject) {
return {
...zoneObject,
object: {
...zoneObject.object,
originX: updatedObject.originX,
originY: updatedObject.originY
}
}
}
return zoneObject
})
// Update the zone with the new zoneObjects
zoneEditorStore.setZone({
...zoneEditorStore.zone,
zoneObjects: updatedZoneObjects
})
// Update selectedObject if it's set
if (zoneEditorStore.selectedObject) {
const updatedObject = newObjects.find((obj) => obj.id === zoneEditorStore.selectedObject?.id)
if (updatedObject) {
zoneEditorStore.setSelectedObject({
...zoneEditorStore.selectedObject,
originX: updatedObject.originX,
originY: updatedObject.originY
})
}
}
},
{ deep: true }
)
</script>

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