Compare commits

...

147 Commits

Author SHA1 Message Date
ba8af589a7 Merge remote-tracking branch 'origin/feature/#245-character-hair' into feature/#250
# Conflicts:
#	src/components/screens/Characters.vue
2024-11-23 15:34:42 +01:00
301340327a Added components to manage hair types 2024-11-23 15:29:49 +01:00
1e4c58c79e npm update 2024-11-23 15:29:38 +01:00
f87cd063ee Center elements in characters screen 2024-11-23 15:29:28 +01:00
9593298389 #250 - Changed focus styling, adjusted where needed 2024-11-23 00:27:11 +01:00
ad4651844d Updated TS types 2024-11-21 03:00:09 +01:00
3748c459f8 Merge remote-tracking branch 'origin/main' into feature/#248 2024-11-20 21:19:22 +01:00
50ea3ecdab npm update 2024-11-20 21:19:05 +01:00
8910390f7b #248 - Unfocus chat input when clicking outside 2024-11-19 22:24:34 +01:00
d820490b2b Added update logic for character types, added isEnabledForCharCreation field 2024-11-17 17:52:24 +01:00
2c96caee4f Text white when item is active in asset manager 2024-11-17 17:00:07 +01:00
84939a7d32 Disabled texture in GM utility modals , fixed btn padding for zone delete button 2024-11-17 00:46:05 +01:00
1e3fc2b0f8 Added disableBgTexture option to modals 2024-11-17 00:39:28 +01:00
7c8b5f3e82 npm update 2024-11-16 01:21:43 +01:00
570d315bf5 Small improvements 2024-11-14 23:46:13 +01:00
7871b34c60 Don't open GM panel if focus is on any input or textarea 2024-11-14 22:28:10 +01:00
85d64f23eb #238: Remove hash from URL with JS instead of full redirect 2024-11-14 22:20:46 +01:00
bdb6dd0d54 #161: Store chats into database 2024-11-14 20:42:52 +01:00
faf887163a Added resize event to update to particle window size when resizing 2024-11-13 21:25:54 +01:00
dd5baa530d Removed console.log(), fixed small bug 2024-11-13 19:38:51 +01:00
d9947e29cf Prevent loading anims more than once 2024-11-13 15:42:21 +01:00
1888521762 Fixed modal fullscreen icons, made types compatible with server changes made for #174, npm update, npm run format, minor improvements 2024-11-13 13:22:20 +01:00
48fef2313b Renamed and moved assets for clarity, removed unused svg's 2024-11-08 22:03:10 +01:00
0a99d2c430 Finished base layout of character select/create 2024-11-08 21:22:50 +01:00
ed6f592606 WIP character select 2024-11-06 23:05:37 +01:00
46ebfaec01 npm update 2024-11-05 23:18:10 +01:00
1384f50406 npm run format 2024-11-05 23:16:47 +01:00
d71f4e7b59 #192: Update light and other effects based on server date / weather state 2024-11-05 23:07:05 +01:00
58929290ab #184: Listen for weather updates from client 2024-11-05 22:46:21 +01:00
63146106c0 Moved clock pos. 2024-11-05 22:02:46 +01:00
7c5602f204 #197: Added background image loader 2024-11-05 22:01:28 +01:00
e711e124ce Map editor tiles improvement 2024-11-05 21:31:28 +01:00
e1b39c42ec Several map editor improvements 2024-11-05 21:28:12 +01:00
d81c889426 Removed GM tools, added event listener for shift + G to open GM panel 2024-11-05 20:53:39 +01:00
afb0edacf6 #184: Added clock component 2024-11-05 20:42:52 +01:00
6d7d568746 Moved login partial components 2024-11-05 20:36:00 +01:00
8df5b6eb76 #239: Add loading indicator to password reset submit button for better UX 2024-11-05 20:21:52 +01:00
270d12821a Renamed component, inform user when password reset mail has been sent, added comment for #238 2024-11-05 01:02:27 +01:00
9c244e980c Cleaned component 2024-11-05 00:35:26 +01:00
25ba54c8ac Moved ResetPassword component to correct dir 2024-11-05 00:33:28 +01:00
9c4bef864b Updated default value 2024-11-05 00:01:36 +01:00
bdc566e30f #236: Fixed clear button in map editor 2024-11-04 23:44:34 +01:00
a653b61b51 Moved zone editor partials into folder 2024-11-04 23:36:19 +01:00
7b61f71fa9 #231 : Remove logic that prevents modals from being dragged outside of the view & refactor modal TS 2024-11-04 23:34:41 +01:00
42539cc73d Fix for paint tool 2024-11-04 21:06:40 +01:00
864369860c Typo fixes, started working on bug fix, npm update 2024-11-04 21:04:44 +01:00
89938b7f93 npm run format 2024-11-04 01:06:15 +01:00
f076661d4a npm update 2024-11-03 22:35:22 +01:00
0b018f53a7 Merge remote-tracking branch 'origin/main' into feature/#182-reset-password 2024-11-03 22:30:45 +01:00
32a13f0f3c Added more whitespace under error on register form 2024-11-03 22:12:14 +01:00
f203bf9534 Merge conflict fix 2024-11-03 22:00:29 +01:00
67b6339ffc FInished password reset (hopefully) 2024-11-03 21:56:19 +01:00
6233da0044 Temporarily disable sound 2024-11-03 03:26:07 +01:00
56f455a08e TS declaration improvement, moved files 2024-11-03 01:32:37 +01:00
9d541cc46a Merge remote-tracking branch 'origin/main' into feature/#182-reset-password
# Conflicts:
#	src/components/screens/Characters.vue
2024-11-03 01:27:27 +01:00
adf86d369b #216: Added tile & object picker in map editor 2024-11-03 01:26:13 +01:00
bbcb84ed03 Refactored screens & login forms, fixed pw reset modal, WIP new pw form 2024-11-03 00:47:02 +01:00
6f40c774ea TS shit 2024-11-03 00:34:03 +01:00
e3b70df46f npm update 2024-11-03 00:33:00 +01:00
584262a59b Merge remote-tracking branch 'origin/main' into feature/#182-reset-password
# Conflicts:
#	src/screens/Login.vue
#	src/stores/gameStore.ts
2024-11-02 21:47:57 +01:00
7e4e613405 Moved music to character selection screen, disable loader 2024-11-02 16:02:43 +01:00
e2c60bfd5a #210 : Play music on login screen 2024-11-02 14:05:37 +01:00
e38c402266 Fix button sound playing when clicked outside of buttons 2024-11-02 13:54:16 +01:00
a299d5dc55 #209: Play sound when a button is pressed 2024-11-02 13:51:07 +01:00
43c0f0ab1e Added vite-plugin-compression 2024-11-02 13:34:52 +01:00
ed17e7f16e #217: Solved duplicate texture loading issue, #215: Finished Dexie implementation. 2024-11-02 02:20:34 +01:00
7d723530e6 Renamed pw reset form variable 2024-11-02 01:47:23 +01:00
a3532a5940 Typo 2024-10-31 12:33:04 +01:00
74cbf3f2c8 Animations now load dynamically and are cached 2024-10-31 12:31:30 +01:00
d402744955 Map editor fixes (select & move), character sprites are now dynamically loaded and cached, moved repeated code into a composable, updated types 2024-10-30 15:27:37 +01:00
39b65b3884 npm run format 2024-10-30 10:07:22 +01:00
c62ff2efc1 Refactor assetManager, renamed assetManager to assetStorage, renamed AssetT to AssetDataT, added better error handling in authentication service, continued working on dynamic asset loading for both maps and map editor 2024-10-30 09:36:16 +01:00
08f55c9680 #190 : Cookie domain improvement 2024-10-30 07:17:07 +01:00
1afc50ea6a Moved reset password, adjusted modal open logic (also WIP) 2024-10-29 22:50:11 +01:00
7c259f455c Load all tiles in zone editor 2024-10-28 23:41:26 +01:00
be854a79b8 Added reset password modal form 2024-10-27 21:31:50 +01:00
a71890ab68 Clean 2024-10-27 21:09:19 +01:00
dc2b6b9851 WIP: updated zone manager to match with zone logic (dynamic asset/texture loading) 2024-10-27 21:04:08 +01:00
d091aabeb3 Removed onMounted() 2024-10-27 18:53:18 +01:00
c261937cf5 Typo fix 2024-10-27 18:50:30 +01:00
4aa1309797 Small cleanup 2024-10-27 18:50:13 +01:00
4f8517a50c Proof of concept dynamic tile loading 2 2024-10-27 18:48:19 +01:00
446e049e6e npm update, added email field 2024-10-27 17:48:04 +01:00
7db2ba322c Minor improvements, more work on dynamic asset loading 2024-10-26 02:41:16 +02:00
70fb732051 finally some good fucking code 2024-10-26 00:15:31 +02:00
5128aa83f9 Added code comment 2024-10-25 22:25:49 +02:00
f75528b2af Merge remote-tracking branch 'origin/feature/#215-dexie-caching-implementation' into feature/character-profile
# Conflicts:
#	src/screens/Game.vue
2024-10-25 22:20:34 +02:00
9f50b062b0 Worked on dynamic texture loading 2024-10-25 22:17:02 +02:00
141cd225c8 Fix hotkey event not working 2024-10-22 20:28:49 +02:00
2870fea15e Added global listener for character profile hotkey
Also re-organised assets folder
2024-10-22 20:23:58 +02:00
8374dc91da WIP Character Profile popup, cleaned up old assets 2024-10-21 22:39:28 +02:00
347fc0e1e8 MVP Dexie loader 2024-10-21 18:35:36 +02:00
715fe5c318 Cookies 2024-10-21 02:15:29 +02:00
df19c1094c feature/#215-dexie-caching-implementation 2024-10-21 02:08:27 +02:00
279b9bc7a3 Reversed order of login screen (img>form) 2024-10-20 01:10:29 +02:00
6adfc727c5 return if shift is pressed for tiles, objects and event tiles (camera drag = active) 2024-10-19 22:36:08 +02:00
1aa07daa35 Also added overflow to tile group 2024-10-19 22:25:25 +02:00
36901782ea Adjusted gm panel + toolbar styling, fixed tilelist overflow 2024-10-19 22:11:20 +02:00
62f2e1124c #211 Fixed overflow scroll in left panel not working 2024-10-19 21:27:04 +02:00
f6e1a54e74 Body view fixes? 2024-10-19 21:20:43 +02:00
6e2885cba6 Added tile sorting logic, minor improvements 2024-10-19 21:15:13 +02:00
252d9c87fd Firefox styling fixes 2024-10-19 17:13:06 +02:00
856829b605 Possible fix for text alignment on Windows 2024-10-19 17:00:58 +02:00
a0f0b40ed3 Replaced modal resize icon, started writing components for character type management, spride field made optional 2024-10-19 02:15:48 +02:00
68222ab511 Styling fixes 2024-10-18 23:08:01 +02:00
fe804037d0 #137 : Listen for zoneEffects in effects component 2024-10-18 22:41:42 +02:00
5d288772b5 #137 : Added logic to set effects per zone in zone editor 2024-10-18 22:23:56 +02:00
3c9b92ccbd Better func. naming 2024-10-18 19:53:02 +02:00
13cb46658f nginx conf. test 2024-10-18 19:45:25 +02:00
222614b856 Moved GmTools and GmPanel to App.vue 2024-10-18 19:27:22 +02:00
9f10db142b don't have ZE enabled by default 2024-10-18 19:21:06 +02:00
860fe705c6 Saving maps works again 2024-10-18 19:10:41 +02:00
352ec3fad8 Moved search input into modal 2024-10-18 17:55:58 +02:00
c7a3d74408 Removed todo comment 2024-10-18 17:47:11 +02:00
1e0da5f7cc Removed unused imports, re-added paint func, more refactor work 2024-10-18 17:45:50 +02:00
7ce054191a Re-added & Improved delete logics for tiles, zone objects and event tiles. Added more code comments for better DX. 2024-10-18 15:43:50 +02:00
34f547f0a6 Added deleteTile() func. to zone composable 2024-10-18 15:40:28 +02:00
14e07aa4a1 npm update 2024-10-18 15:40:14 +02:00
95dcf237cf npm run format 2024-10-18 02:49:35 +02:00
5cc1821922 Removed unused import 2024-10-18 02:49:04 +02:00
9d774bcb18 Removed redundant code , refactor event tile code 2024-10-18 02:48:27 +02:00
c6869f47b1 Typescript improvements, added move zone object logic. 2024-10-18 02:33:51 +02:00
390b9517e0 Moved styling to main.scss as it's global 2024-10-18 00:42:15 +02:00
2497da30b7 Re-added initial-scale=1 2024-10-18 00:40:50 +02:00
66e56d3626 Renamed Keybindings > Hotkeys 2024-10-18 00:33:06 +02:00
d68ee120ab Merge remote-tracking branch 'origin/feature/refactor-zone-editor'
# Conflicts:
#	src/components/gui/Keybindings.vue
#	src/components/gui/Minimap.vue
2024-10-18 00:21:03 +02:00
aff32c33c7 npm update, mobile scroll fix, zone editor object refactor work, removed redundant div 2024-10-18 00:20:30 +02:00
3c8744dc75 Reworked layout for keybindings, hide minimap on smaller screens 2024-10-17 23:26:25 +02:00
774871510e Reordered func. params getTile(), npm run format, refactor zone object part in zone editor, other improvements 2024-10-17 19:26:45 +02:00
e61b705031 Smaller GmTools modal height 2024-10-17 18:41:14 +02:00
3902c611fa Fixed placing tiles 2024-10-17 18:39:18 +02:00
3765cfe5e9 Started refactoring zone editor 2024-10-17 02:32:39 +02:00
2fad54fd26 More styling tweaks, updated confirmation modal 2024-10-16 22:23:00 +02:00
be3cbf77bf Added global empty btn styling, adjusted and WIP modal styling 2024-10-16 20:47:29 +02:00
f24a498246 Re-added Vue Dev Tools. Problem fixed
https://github.com/vuejs/devtools-next/issues/635
2024-10-16 18:50:38 +02:00
9686381745 Removed unused impots, use config.name instead of hardcoded name 2024-10-16 16:07:35 +02:00
e42c530685 Bug fix for toggling zoneEditor 2024-10-16 16:06:18 +02:00
e56e078042 Separated Game and ZoneEditor screens 2024-10-16 16:04:43 +02:00
13be1a38fa Refactor App.vue 2024-10-16 16:04:29 +02:00
27e857b9a6 npm update, removed vue dev tools bc bug 2024-10-16 15:45:41 +02:00
2b2c290db0 Removed unused const. 2024-10-16 14:15:30 +02:00
245b50c1fd Moved component attributes to properties function for more streamlined experience 2024-10-16 14:15:01 +02:00
32dc7a2963 Removed register, it's in the same screen now 2024-10-16 14:11:43 +02:00
a9c2b209d9 Works partially 💩 2024-10-15 23:48:50 +02:00
a6c22df528 Fixed some bugs in login form, updated logo name and icons, wip of minimap 2024-10-15 20:27:54 +02:00
7dd2d70eca npm update 2024-10-15 15:49:12 +02:00
5fc3547d9c Replaced logo 2024-10-14 23:25:41 +02:00
152 changed files with 4910 additions and 2628 deletions

View File

@ -1,4 +1,4 @@
VITE_NAME=New Quest
VITE_NAME=Sylvan Quest
VITE_DEVELOPMENT=true
VITE_SERVER_ENDPOINT=http://localhost:4000
VITE_TILE_SIZE_X=64

View File

@ -27,5 +27,6 @@ RUN npm run build-ntc
# Production stage
FROM nginx:1.26.1-alpine
COPY --from=builder /usr/src/app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

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

16
nginx.conf Normal file
View File

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

1445
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -18,11 +18,12 @@
"@vueuse/core": "^10.5.0",
"@vueuse/integrations": "^10.5.0",
"axios": "^1.7.7",
"phaser": "^3.85.2",
"dexie": "^4.0.8",
"phaser": "^3.86.0",
"pinia": "^2.1.6",
"socket.io-client": "^4.8.0",
"universal-cookie": "^6.1.3",
"vue": "^3.5.10",
"vue": "^3.5.12",
"zod": "^3.22.2"
},
"devDependencies": {
@ -48,8 +49,9 @@
"sass": "^1.79.4",
"tailwindcss": "^3.4.13",
"typescript": "~5.6.2",
"vite": "^5.4.8",
"vite-plugin-vue-devtools": "^7.4.6",
"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: 4.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 440 KiB

View File

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

Before

Width:  |  Height:  |  Size: 453 B

View File

Before

Width:  |  Height:  |  Size: 597 KiB

After

Width:  |  Height:  |  Size: 597 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 600 KiB

After

Width:  |  Height:  |  Size: 600 KiB

View File

Before

Width:  |  Height:  |  Size: 600 KiB

After

Width:  |  Height:  |  Size: 600 KiB

View File

Before

Width:  |  Height:  |  Size: 599 KiB

After

Width:  |  Height:  |  Size: 599 KiB

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#b1b2b5" d="M226.125 32.032a84.287 84.287 0 0 0-3.51.026c-11.4.318-24.464 2.935-40.945 8.63l-2.682.926-2.726-.777c-38.965-11.123-65.026.52-90.262 14.957 5.557 9.074 10.35 19.569 8.365 32.049l-.045.29-.066.286c-2.043 9.01-7.64 22.012-14.363 35.676-1.723 3.502-3.57 6.712-5.37 10.1 12.791-6.326 24.681-12.344 38.95-13.885l2.53-.274 2.302 1.094c-.018-.008 2.436.899 5.261 1.853 2.826.955 6.331 2.16 9.977 3.68 5.207 2.172 10.979 4.697 15.512 9.256 22.303-6.732 40.75-6.51 60.802-1.414 18.213-10.407 34.866-11.39 47.99-6.512 8.553 3.178 15.291 8.373 20.93 13.842l10.557-22.625 20.805 3.62c9.72-15.164 9.644-33.821 3.234-49.167-7.012-16.786-20.608-27.857-34.928-26.795l-2.404.178-2.172-1.047c-16.758-8.082-30.442-13.73-47.742-13.967zM112.791 138.688a44.845 44.845 0 0 0-3.287.745c-7.307 47.909-26.314 187.24-6.9 293.404 158.558 12.92 313.945 3.207 334.82 1.793.099-2.715.066-5.485-.121-8.313-1.354-20.44-10.822-42.312-22.235-55.045-6.318-7.049-23.99-13.695-48.029-16.789-24.04-3.094-54.057-3.4-85.057-2.056l-6.337.275c-21.183 1.115-40.742 2.29-58.89 5.111-4.051 8.895-13.356 14.998-23.735 14.998-13.904 0-25.885-10.947-25.885-24.892s11.981-24.89 25.885-24.89c5.778 0 11.22 1.896 15.613 5.107 8.084-10.448 16.365-21.222 25.375-31.897-8.386-8.22-17.33-15.993-27.44-22.722-3.941 2.49-8.595 3.943-13.548 3.943-13.842 0-25.39-11.288-25.39-25.14 0-13.854 11.548-25.141 25.39-25.141 2.64 0 5.194.413 7.603 1.174l33.02-38.875c-8.516-9.7-17.736-19.149-28.409-28.37-4.074 2.75-8.973 4.364-14.2 4.364-10.517 0-19.703-6.518-23.513-15.723-5.762 1.045-11.908 2.718-18.841 5.137l-7.551 2.634-3.504-7.189c-.353-.723-5.078-4.58-11.012-7.055-2.967-1.237-6.073-2.315-8.81-3.24-1.348-.455-2.457-.895-3.582-1.31l-1.43-.038zm96.748 2.565c13.9 11.186 25.49 22.656 35.904 34.33l18.24-21.475c-.711-3.822-1.422-7.642-2.142-11.478l-52.002-1.377zm58.313 35.754l-10.586 12.463c5.451 6.674 10.675 13.403 15.857 20.174-1.556-10.837-3.35-21.714-5.271-32.637zm-22.471 26.455l-30.586 36.012a24.652 24.652 0 0 1 3.613 12.845c0 2.354-.34 4.63-.96 6.793 10.699 7.168 20.067 15.208 28.613 23.489 9.55-10.298 19.972-20.277 31.78-29.457a476.305 476.305 0 0 0-.661-8.694c-11.152-14.168-21.175-27.806-31.8-40.988zm-52.361 41.717c-4.261 0-7.39 3.165-7.39 7.14 0 3.976 3.129 7.14 7.39 7.14 4.26 0 7.388-3.164 7.388-7.14 0-3.975-3.128-7.14-7.388-7.14zm85.732 30.572c-7.128 6.278-13.756 12.912-20.057 19.762 6.337 6.753 12.324 13.432 18.213 19.724 1.44-13.148 2-26.304 1.844-39.486zm-32.096 33.514c-8.101 9.677-15.774 19.602-23.41 29.44 15.249-1.938 31.118-2.91 47.58-3.776-8.238-8.276-16.088-17.033-24.17-25.664zm-53.636 31.763c-4.748 0-7.885 3.282-7.885 6.89 0 3.61 3.137 6.893 7.885 6.893 4.747 0 7.884-3.283 7.884-6.892 0-3.609-3.137-6.89-7.884-6.89zm-86.577 110.164c1.968 8.31 4.2 16.358 6.746 24.059 103.476 5.837 209.68 7.195 303.832-1.3 8.328-5.386 13.8-12.612 16.975-21.06-36.35 2.27-180.346 9.84-327.553-1.699z"/></svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#b1b2b5" d="M162 35.75l-94.49 27.1c-12.05 6.3-23.47 23.9-31.01 46.35-6.07 18.2-9.62 38.9-10.93 58.3L136.7 112zm188 .1L375.4 112l111 55.6c-1.3-19.3-4.9-40.2-10.9-58.3-5.7-17.05-13.6-31.35-22.5-40.05-2.7-2.8-5.5-4.9-8.4-6.4zm-172.9 11.5l-25.7 77.45-92.9 46.4 14.08 53.5 88.82 44.4 94.6-15.9 94.6 15.9 88.8-44.4 14.1-53.5-92.8-46.4-25.8-77.35h-10.5l-59.3 73.95-.1 61.1h-18.1l.1-61-59.3-74.15zM78.65 247.7l22.05 83.9 146.2-43.8v-14.7l-88.4 14.7zm354.75 0l-80 40.1-88.4-14.7v14.7l146.3 43.8zm-186.5 58.7l-31.6 9.6-35.1 70.2 66.7-33.3zm18.1 0v46.5l66.9 33.4-35.2-70.3zM191.7 323l-86.4 26 25.3 96.3zm128.6.1l61.1 122.1 25.3-96.2zm-55.3 50l.1 43.2 100.7 37.8-20.4-40.8zm-18.1 0l-80.2 40.1-20.5 40.9L247 416.3zm.1 62.4l-81.6 30.6 81.6 10.2zm18.1 0v40.7l81.7-10.2z"/></svg>

Before

Width:  |  Height:  |  Size: 985 B

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#b1b2b5" d="M256 68.02L169.7 240.7l14.8 44.5-17 5.6-15.2-45.6-66.42-53-12.18 60.9 12.62 12.6-12.66 12.6-15.21-15.2-30.53-20.4 25.15 163.4 39.17-65.2 89.66 35.8 30.9 46.4h86.4l30.9-46.4 89.7-35.8 39.1 65.2 25.2-163.4-30.6 20.4-15.2 15.2-12.6-12.6 12.6-12.6-12.2-60.9-66.4 53-15.2 45.6-17-5.6 14.8-44.5zm0 122.58l55.7 92.8 1.9 3.2-17.5 70-20.4 20.3h-39.4l-20.4-20.3-17.5-70zm0 34.8l-38.4 64 14.5 58 11.6 11.7h24.6l11.6-11.7 14.5-58z"/></svg>

Before

Width:  |  Height:  |  Size: 661 B

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#b1b2b5" d="M256 16c-36.446 0-73.264 13.433-139.97 40h279.94C329.263 29.433 292.445 16 256 16zM95.344 72L64 448c56 28 112 31.5 168 31.938V240H112v-48h288v48H280v239.938C336 479.5 392 476 448 448L416.656 72H95.344zm64.875 88a8 8 0 0 1 7.78 8 8 8 0 0 1-16 0 8 8 0 0 1 8.22-8zm48 0a8 8 0 0 1 7.78 8 8 8 0 0 1-16 0 8 8 0 0 1 8.22-8zm48 0a8 8 0 0 1 7.78 8 8 8 0 0 1-16 0 8 8 0 0 1 8.22-8zm48 0a8 8 0 0 1 7.78 8 8 8 0 0 1-16 0 8 8 0 0 1 8.22-8zm48 0a8 8 0 0 1 7.78 8 8 8 0 0 1-16 0 8 8 0 0 1 8.22-8zM248 240v240c2.667.002 5.333 0 8 0s5.333.002 8 0V240h-16zm-120 48h16v16h-16v-16zm32 0h16v16h-16v-16zm32 0h16v16h-16v-16zm112 0h16v16h-16v-16zm32 0h16v16h-16v-16zm32 0h16v16h-16v-16zm-240 32h16v16h-16v-16zm32 0h16v16h-16v-16zm32 0h16v16h-16v-16zm112 0h16v16h-16v-16zm32 0h16v16h-16v-16zm32 0h16v16h-16v-16zm-240 32h16v16h-16v-16zm32 0h16v16h-16v-16zm32 0h16v16h-16v-16zm112 0h16v16h-16v-16zm32 0h16v16h-16v-16zm32 0h16v16h-16v-16z"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#b1b2b5" d="M144 37.28c-25.883 0-63.05 25.96-65.22 82.845l65.345 28.47 71.625-28.626C213.124 63.69 169.852 37.28 144 37.28zm224 0c-25.852 0-69.124 26.412-71.75 82.69l71.625 28.624 65.344-28.47c-2.17-56.883-39.337-82.843-65.22-82.843zM76.594 136.626c-10.887 40.29-12.11 90.026-9.844 141.03 46.136 10.675 93.357 7.68 141.406.44 8.464-50.25 12.923-99.296 3.625-139.314l-64.81 25.907-3.095 1.25-3.063-1.343-64.218-27.97zm358.812 0l-64.22 27.97-3.06 1.343-3.095-1.25-64.81-25.907c-9.3 40.02-4.84 89.064 3.624 139.314 48.05 7.242 95.27 10.236 141.406-.438 2.266-51.005 1.043-100.74-9.844-141.03zm-250.25 160.72c-31.678 3.654-63.5 4.865-95.25.75 12.238 12.217 23.424 24.845 35.47 36.217 2.198 2.077 4.425 4.107 6.686 6.094 18.086-12.095 35.813-26.27 53.094-43.062zm141.688 0c17.28 16.792 35.008 30.966 53.094 43.06 2.26-1.986 4.488-4.016 6.687-6.092 12.045-11.373 23.23-24 35.47-36.22-31.752 4.117-63.573 2.906-95.25-.75zm-258.97 1.842c1.515 24.774 3.633 49.29 5.69 73.188 15.054-6.598 29.912-14.187 44.53-23.03-1.24-1.13-2.477-2.264-3.688-3.408-16.52-15.597-30.655-32.307-46.53-46.75zm376.25 0c-15.875 14.443-30.01 31.153-46.53 46.75-1.21 1.144-2.448 2.278-3.688 3.407 14.618 8.844 29.476 16.433 44.53 23.03 2.057-23.897 4.175-48.413 5.69-73.187zm-240.093 1.844c-19.095 19.538-38.774 35.968-58.936 49.845 12.672 9.234 27 16.825 44.78 21.625 4.59-23.255 9.642-47.365 14.157-71.47zm103.94 0c4.514 24.105 9.566 48.215 14.155 71.47 17.78-4.8 32.11-12.39 44.78-21.625-20.16-13.877-39.84-30.307-58.936-49.844zM130.936 360.19c-15.606 9.753-31.47 18.076-47.5 25.28 11.79 18.345 27.05 33.88 44.282 47.97 18.94-13.89 36.69-28.745 51.124-47.532-18.94-6.09-34.415-15.227-47.906-25.72zm250.125 0c-13.49 10.49-28.966 19.628-47.906 25.718 14.435 18.787 32.183 33.642 51.125 47.53 17.234-14.087 32.494-29.623 44.283-47.967-16.03-7.206-31.894-15.53-47.5-25.283zM76.125 403.125c1.866 23.033 3.352 44.754 3.75 64.22 11.39-8.415 22.977-16.29 34.313-24.25-14.114-11.857-27.09-24.923-38.063-39.97zm359.75 0c-10.973 15.047-23.95 28.113-38.063 39.97 11.336 7.96 22.924 15.835 34.313 24.25.398-19.466 1.884-41.187 3.75-64.22zm-252.156 2.063c-12.975 14.874-27.61 27.23-42.75 38.53 11.24 8.348 23.104 16.282 35.218 24.126.75-19.013 3.624-40.192 7.53-62.656zm144.56 0c3.908 22.464 6.783 43.643 7.533 62.656 12.113-7.844 23.977-15.778 35.218-24.125-15.14-11.303-29.775-23.658-42.75-38.533zm-200.967 48.375c-13.318 9.394-26.726 18.344-39.47 27.812 26.524 12.555 53.04 11.06 79.563-.156-13.63-8.813-27.194-17.85-40.094-27.658zm257.375 0c-12.9 9.808-26.464 18.844-40.094 27.656 26.523 11.214 53.04 12.71 79.562.155-12.743-9.468-26.15-18.418-39.47-27.813z"/></svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#b1b2b5" d="M186.438 20.56l-13.184 26.365c6.8-.26 13.626-.488 20.47-.686l3.84-7.68h116.874l3.77 7.54c6.838.187 13.658.408 20.456.66l-13.102-26.2H186.437zm69.56 42.742c-45.757.056-91.452 1.566-135.38 4.363-3.24 50.58-8.4 100.987-.786 145.824 89.297 12.395 180.102 12.985 272.764-.054 7.055-30.988 5.117-84.68-1.04-145.89-43.974-2.893-89.73-4.3-135.558-4.244zm153.783 5.54c6.42 64.12 9.113 119.825-1.135 155.22l-1.61 5.56-5.726.842c-98.8 14.528-195.613 13.81-290.605.002l-6.285-.914-1.246-6.23c-9.89-49.49-4.085-102.785-.664-154.42-4.89.354-9.765.72-14.602 1.107-8.596 58.568-9.39 116.957-.05 175.292 110.24 12.088 222.275 12.205 336.203-.01 8.502-57.83 8.29-116.25-.017-175.313-4.725-.4-9.485-.776-14.262-1.14zM255.966 92.3c32.526-.025 65.067 2.746 97.574 8.39l7.46 1.295v7.572c0 15.554 1.683 35.105-12.69 50.25-9.912 10.444-25.655 17.337-51.31 20.585v18.164h-82v-18.452c-23.992-3.37-39.352-10.175-49.363-20.185C150.807 145.093 151 125.56 151 109.56v-7.594l7.484-1.278c32.444-5.54 64.955-8.362 97.48-8.386zm.012 17.994c-28.96.022-57.913 2.444-86.858 6.996.265 12.28 1.635 22.296 9.243 29.904 5.914 5.914 16.952 11.416 36.637 14.582v-29.22h82v29.51c21.367-3.115 32.66-8.755 38.254-14.65 7.033-7.41 7.696-17.502 7.73-30.124-29-4.63-58.006-7.02-87.007-6.998zM233 150.56v30h46v-30h-46zm209.674 92.42c-.503 3.625-1.042 7.25-1.61 10.87.214 2.352.42 4.706.63 7.06L471 290.213v-22.24l-28.326-24.995zm-373.485.12L41 267.973v22.24l29.318-29.318c.205-2.327.406-4.655.616-6.982-.618-3.605-1.202-7.21-1.745-10.813zm354.634 20.397c-10.29 1.09-20.564 2.076-30.824 2.967v74.095h16v66h-16v80.615c10.318-.633 20.63-1.313 30.928-2.082 9.445-74.01 6.478-147.698-.104-221.596zm-335.576.03C81.725 338.09 78.58 412.1 88.06 485.1c10.324.79 20.638 1.504 30.94 2.145V406.56h-16v-66h16v-74.024c-10.266-.902-20.517-1.903-30.752-3.01zm286.752 4.4c-10.014.76-20.014 1.424-30 1.992v70.64h30v-72.632zm-238 .085v72.547h30v-70.55c-10.015-.568-20.014-1.237-30-1.997zm190 2.825c-47.65 2.173-94.984 2.19-142 .078v19.314c23.95-5.165 47.8-7.652 71.516-7.59 23.638.06 47.145 2.654 70.484 7.626v-19.43zM68.05 288.62L41 315.67v56.89h23.06c.376-27.987 1.88-55.975 3.99-83.94zm375.948.047c2.12 27.872 3.61 55.83 3.957 83.892H471v-56.89l-27.002-27.003zm-187.52 11.95c-23.68-.063-47.487 2.577-71.478 8.052v31.89h16v18.443c17.033 5.346 31.73 8.493 46 9.426v-2.87h18v2.868c14.27-.932 28.967-4.08 46-9.425V340.56h16v-31.866c-23.42-5.267-46.907-8.016-70.523-8.078zM121 358.558v30h22v-23h18v23h22v-30h-62zm208 0v30h22v-23h18v23h22v-30h-62zM201 377.8v28.76h-16v15.857c48.528 10.865 95.713 10.664 142 .045V406.56h-16V377.8c-16.332 4.747-31.283 7.52-46 8.326v11.433h-18v-11.434c-14.717-.806-29.668-3.58-46-8.326zM41 390.56v14h23.14c-.09-4.667-.143-9.334-.163-14H41zm407.012 0c-.027 4.663-.083 9.33-.18 14H471v-14h-22.988zM137 406.56v19.798c6.137 7.214 11.222 9.77 14.934 9.844 3.734.075 8.697-2.122 15.066-9.79V406.56h-6v7h-18v-7h-6zm208 0v19.798c6.137 7.214 11.222 9.77 14.934 9.844 3.734.075 8.697-2.122 15.066-9.79V406.56h-6v7h-18v-7h-6zm-304 16v35.154c5.596 5.51 8.677 8.25 11.846 9.306 2.454.818 7.713 1.15 15.045 1.317-1.544-15.25-2.586-30.51-3.204-45.778H41zm406.27 0c-.628 15.224-1.674 30.483-3.21 45.78 7.358-.168 12.635-.5 15.094-1.32 3.17-1.056 6.25-3.795 11.846-9.306V422.56h-23.73zM185 440.842v49.498c47.55 1.51 94.877 1.446 142-.074V440.9c-46.316 10.03-93.74 10.185-142-.057zm-48 9.123v38.318c10.01.54 20.01 1.008 30 1.408v-39.678c-4.86 2.786-10.01 4.293-15.43 4.184-5.192-.104-10.036-1.624-14.57-4.232zm208 0v39.654c10.01-.403 20.01-.878 30-1.412v-38.194c-4.86 2.786-10.01 4.293-15.43 4.184-5.192-.104-10.036-1.624-14.57-4.232z"/></svg>

Before

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#b1b2b5" d="M329.5 29.12l-8.1 11.4L359 67.16l8.1-11.44zm-88 5.04l24.2 45.36 1.8 1.29 14.8-40.36zm57.6 12.63l-16.4 44.8 40.7 28.81 35.3-31.54c-.9-.58-1.9-1.19-2.8-1.84zM59.83 48.56l10.84 45.83 29.63 2.6 2.7-29.63zM470.9 75.41c-5.6 4.71-12.2 8.59-19.5 11.74 5 46.45-14.7 83.45-45.2 109.75-26.5 22.9-60.9 38.4-95 47.9-2.5 4.8-5 9.2-7.4 13.1 41.5 5.4 93.2-21.2 129.2-60 19.8-21.3 34.8-45.9 41.1-69.2 5.2-19.4 4.7-37.42-3.2-53.29zm-351.3 8.71l-3 32.48-32.35-2.9 226.55 271 20-16.7 15.3-12.8zM434 93.09c-4.2 1-8.5 2-12.8 2.7-14.9 2.5-30.1 3.1-43.5.3l-41 36.61c4 7 5 15.7 4.5 24.5-.6 12.6-4.3 26.7-9.3 40.9-3 8.3-6.3 16.6-9.9 24.6 26.9-9.2 52.6-22.3 72.5-39.4 26.2-22.8 42.5-51.6 39.5-90.21zM274 107.4l-51.2 72.2 30.6 36.5 58.2-82.1zM173.8 248.8L34.53 445.2l37.53 26.6L204.3 285.3zm233 79.2L273.3 439.5l19.2 23.1L426 351zm-18.3 77.9l-35.3 29.4 39.7 47.6 35.3-29.4z"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

After

Width:  |  Height:  |  Size: 729 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 597 KiB

View File

Before

Width:  |  Height:  |  Size: 915 B

After

Width:  |  Height:  |  Size: 918 B

View File

Before

Width:  |  Height:  |  Size: 768 B

After

Width:  |  Height:  |  Size: 768 B

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="438.529px" height="438.529px" viewBox="0 0 438.529 438.529" style="enable-background:new 0 0 438.529 438.529;"
xml:space="preserve">
<g>
<g>
<path d="M180.156,225.828c-1.903-1.902-4.093-2.854-6.567-2.854c-2.475,0-4.665,0.951-6.567,2.854l-94.787,94.787l-41.112-41.117
c-3.617-3.61-7.895-5.421-12.847-5.421c-4.952,0-9.235,1.811-12.851,5.421c-3.617,3.621-5.424,7.905-5.424,12.854v127.907
c0,4.948,1.807,9.229,5.424,12.847c3.619,3.613,7.902,5.424,12.851,5.424h127.906c4.949,0,9.23-1.811,12.847-5.424
c3.615-3.617,5.424-7.898,5.424-12.847s-1.809-9.233-5.424-12.854l-41.112-41.104l94.787-94.793
c1.902-1.903,2.853-4.086,2.853-6.564c0-2.478-0.953-4.66-2.853-6.57L180.156,225.828z"/>
<path d="M433.11,5.424C429.496,1.807,425.212,0,420.263,0H292.356c-4.948,0-9.227,1.807-12.847,5.424
c-3.614,3.615-5.421,7.898-5.421,12.847s1.807,9.233,5.421,12.847l41.106,41.112l-94.786,94.787
c-1.901,1.906-2.854,4.093-2.854,6.567s0.953,4.665,2.854,6.567l32.552,32.548c1.902,1.903,4.086,2.853,6.563,2.853
s4.661-0.95,6.563-2.853l94.794-94.787l41.104,41.109c3.62,3.616,7.905,5.428,12.854,5.428s9.229-1.812,12.847-5.428
c3.614-3.614,5.421-7.898,5.421-12.847V18.268C438.53,13.315,436.734,9.04,433.11,5.424z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 375 B

After

Width:  |  Height:  |  Size: 375 B

View File

Before

Width:  |  Height:  |  Size: 652 B

After

Width:  |  Height:  |  Size: 652 B

View File

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

Before

Width:  |  Height:  |  Size: 346 B

After

Width:  |  Height:  |  Size: 346 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 485 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 325 B

After

Width:  |  Height:  |  Size: 597 KiB

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.15761 1.28332C3.96537 1.15583 3.71087 1.17639 3.54159 1.3331C3.23088 1.62074 2.98798 2.00897 2.80986 2.34288C2.69323 2.27414 2.5689 2.21392 2.43729 2.1694C2.27798 2.11552 2.10225 2.14533 1.96963 2.24875C1.83701 2.35216 1.76526 2.51531 1.77868 2.68295C1.96117 4.96193 2.51766 7.79669 3.14534 10.1876C3.15716 10.2326 3.17507 10.2753 3.19825 10.3146C2.89349 10.543 2.6183 10.8624 2.48626 11.2592C2.45915 11.3406 2.45376 11.4266 2.46923 11.5092L1.01702 12.5343C0.971683 12.5663 0.93199 12.6056 0.899569 12.6506C0.521457 13.1759 0.404537 13.8064 0.579107 14.3656C0.635354 14.5458 0.788273 14.679 0.974476 14.7099C2.62972 14.9848 3.76851 14.6009 4.99802 13.9624C5.09724 13.9109 5.17356 13.8294 5.21926 13.7331C5.28208 13.7365 5.3457 13.7281 5.40668 13.7074L6.36918 13.3813C6.58065 13.3096 6.71897 13.1064 6.70811 12.8834L6.66986 12.0978C6.6651 11.9999 6.6317 11.9057 6.5738 11.8266C6.56495 11.8145 6.55602 11.8025 6.54703 11.7905C6.68693 11.6781 6.75773 11.4969 6.72648 11.3158C6.51037 10.0625 6.7448 8.79384 7.001 7.4073C7.02616 7.27116 7.05153 7.13387 7.0767 6.99535C7.35132 5.48411 7.59458 3.84747 7.06807 2.17953C6.98994 1.93202 6.73543 1.78545 6.48214 1.84209C6.12136 1.92278 5.70715 2.0529 5.33106 2.21214C5.27748 2.23483 5.22893 2.26639 5.1871 2.30496C5.15959 2.27265 5.13071 2.23811 5.09985 2.201C5.09087 2.1902 5.08173 2.17919 5.07242 2.16799C5.00591 2.0879 4.93138 1.99816 4.85218 1.90837C4.6703 1.7022 4.44556 1.47429 4.15761 1.28332ZM12.4584 1.33311C12.2891 1.1764 12.0346 1.15583 11.8424 1.28331C11.5544 1.47427 11.3296 1.70219 11.1478 1.90836C11.0685 1.99816 10.994 2.08791 10.9275 2.16801C10.9182 2.1792 10.9091 2.19021 10.9001 2.201C10.8692 2.2381 10.8404 2.27263 10.8129 2.30493C10.771 2.26634 10.7224 2.23476 10.6688 2.21207C10.2927 2.05285 9.87856 1.9228 9.51784 1.8421C9.26454 1.78544 9.01 1.93201 8.93187 2.17953C8.40536 3.84747 8.64862 5.48411 8.92324 6.99535C8.94841 7.13386 8.97378 7.27114 8.99893 7.40728C9.25513 8.79381 9.48956 10.0625 9.27343 11.3157C9.24218 11.4969 9.31299 11.6781 9.45292 11.7905C9.44392 11.8025 9.435 11.8145 9.42615 11.8266C9.36825 11.9056 9.33484 11.9999 9.33008 12.0978L9.29183 12.8834C9.28097 13.1064 9.41929 13.3096 9.63076 13.3813L10.5933 13.7074C10.6543 13.7281 10.7179 13.7365 10.7807 13.7331C10.8264 13.8294 10.9028 13.9109 11.002 13.9624C12.2315 14.6008 13.3703 14.9848 15.0255 14.7099C15.2117 14.679 15.3647 14.5458 15.4209 14.3656C15.5955 13.8064 15.4785 13.1759 15.1004 12.6506C15.068 12.6056 15.0283 12.5663 14.983 12.5343L13.5307 11.5091C13.5462 11.4266 13.5408 11.3406 13.5137 11.2592C13.3816 10.8624 13.1064 10.543 12.8017 10.3146C12.8248 10.2753 12.8428 10.2326 12.8546 10.1876C13.4823 7.79669 14.0388 4.9619 14.2212 2.68292C14.2347 2.51528 14.1629 2.35213 14.0303 2.24872C13.8977 2.14531 13.7219 2.11549 13.5626 2.16937C13.431 2.21389 13.3067 2.27411 13.1901 2.34286C13.0119 2.00898 12.769 1.62075 12.4584 1.33311Z" stroke="#666666" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -0,0 +1,3 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.504 1.63801L12.252 1.84501C11.652 2.34001 11.352 2.58801 11 2.58801C10.648 2.58801 10.348 2.34001 9.74799 1.84501L9.49599 1.63801C9.00899 1.23701 8.76599 1.03601 8.47499 1.00401C8.18299 0.973011 7.90499 1.11601 7.34699 1.40401L7.04399 1.55901C6.76699 1.70201 6.62799 1.77401 6.55299 1.91901C6.47799 2.06401 6.49499 2.20301 6.52999 2.48101C6.80599 4.70301 8.49099 6.79701 10.61 7.42601C10.735 7.47216 10.8668 7.49717 11 7.50001C11.1332 7.49717 11.265 7.47216 11.39 7.42601C13.509 6.79701 15.194 4.70301 15.47 2.48101C15.505 2.20301 15.522 2.06401 15.447 1.91901C15.372 1.77401 15.233 1.70201 14.957 1.55901L14.653 1.40301C14.096 1.11601 13.817 0.973011 13.525 1.00401C13.234 1.03601 12.991 1.23701 12.505 1.63801M6.47799 3.00001C4.39199 3.40701 2.99599 4.15001 1.71899 5.23001C1.34599 5.54501 1.15899 5.70301 1.07099 5.95401C0.780994 6.78001 1.41499 9.62101 2.19699 9.94901C2.68399 10.153 3.36499 9.73501 4.72799 8.89901C5.96499 8.14001 7.48299 7.35501 8.99999 6.93201M15.522 3.00001C17.608 3.40701 19.004 4.15001 20.281 5.23001C20.654 5.54501 20.841 5.70301 20.929 5.95401C21.219 6.78001 20.585 9.62101 19.803 9.94901C19.316 10.153 18.635 9.73501 17.271 8.89901C16.036 8.14001 14.518 7.35501 13 6.93201M17 9.00001L16.395 14.442C16.207 16.134 16.113 16.98 15.544 17.49C14.974 18 14.123 18 12.42 18H9.57999C7.87699 18 7.02599 18 6.45599 17.49C5.88599 16.98 5.79299 16.134 5.60499 14.442L4.99999 9.00001M14.385 18H7.61499C7.35999 18 7.23299 18 7.11199 18.014C6.67794 18.064 6.271 18.2506 5.94999 18.547C5.86099 18.628 5.77999 18.724 5.61699 18.915C5.29699 19.291 5.13599 19.479 5.07699 19.622C5.02428 19.7507 4.9991 19.8889 5.00309 20.0279C5.00709 20.1669 5.04016 20.3034 5.10017 20.4288C5.16019 20.5543 5.24582 20.6657 5.35155 20.756C5.45727 20.8462 5.58074 20.9134 5.71399 20.953C5.86399 21 6.11499 21 6.61599 21H15.384C15.884 21 16.135 21 16.286 20.953C16.4193 20.9136 16.5429 20.8467 16.6487 20.7565C16.7545 20.6664 16.8403 20.5551 16.9004 20.4297C16.9605 20.3044 16.9937 20.1678 16.9977 20.0289C17.0018 19.8899 16.9767 19.7516 16.924 19.623C16.864 19.479 16.704 19.291 16.384 18.915C16.22 18.724 16.139 18.628 16.05 18.547C15.7286 18.2512 15.3219 18.0646 14.888 18.014C14.767 18 14.64 18 14.385 18Z" stroke="#666666" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,3 @@
<svg width="20" height="22" viewBox="0 0 20 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.00001 19H12M10 7H10.009M2.53801 14.094C0.888011 7.61 3.99601 3.564 10 1C16.005 3.563 19.112 7.61 17.462 14.094C17.342 14.564 17.358 15.063 17.534 15.514L18.91 19.038C19.445 20.409 17.466 20.735 16.625 20.912C14.311 21.399 12.067 19.811 11.693 17.42C11.429 15.733 12.383 15.456 13.631 14.817C13.631 14.817 15.187 14.286 15.187 12.691C15.187 11.517 14.257 10.566 13.112 10.566C12.394 10.566 11.762 10.774 11.135 11.452C10.527 12.109 10.223 12.437 10 12.437C9.77701 12.437 9.47301 12.109 8.86601 11.452C8.23801 10.774 7.60601 10.566 6.88801 10.566C5.74201 10.566 4.81301 11.517 4.81301 12.691C4.81301 14.286 6.36901 14.817 6.36901 14.817C7.61701 15.456 8.57101 15.733 8.30701 17.42C7.93301 19.81 5.68901 21.4 3.37501 20.912C2.53401 20.735 0.555011 20.409 1.09101 19.038L2.46601 15.514C2.64201 15.063 2.65701 14.564 2.53801 14.094Z" stroke="#666666" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1010 B

View File

@ -0,0 +1,3 @@
<svg width="20" height="22" viewBox="0 0 20 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.16 4V3C16.16 2.057 16.16 1.586 15.86 1.293C15.559 1 15.075 1 14.107 1H5.89301C4.92501 1 4.44101 1 4.14001 1.293C3.83901 1.586 3.84001 2.057 3.84001 3V4M16.16 4H3.84001M16.16 4L17.6096 11M3.84001 4L2.39047 11M2.39047 11L1.78774 14.3265M2.39047 11C3.48635 11.3237 4.13173 12.9685 4.99991 13C6.07854 13.0392 7.37081 11.3877 8.76949 11M8.76949 11C9.1675 9.93337 9.99991 9 9.99991 9C9.99991 9 10.8327 9.93306 11.2308 11M8.76949 11C8.55307 11.58 8.31985 12.3176 8.03701 13.213L7.68545 14.3265M11.2308 11C11.4472 11.5799 11.6802 12.3175 11.963 13.213L12.3148 14.3265M11.2308 11C12.6566 11.3875 13.9154 13.041 14.9999 13C15.8598 12.9675 16.5386 11.3237 17.6096 11M17.6096 11L18.2123 14.326M1.78774 14.3265L1.18501 18.653C0.987006 19.741 0.889006 20.285 1.19701 20.643C1.50401 21 2.07201 21 3.20701 21H4.37601C5.10701 21 5.47401 21 5.74601 20.804C6.01801 20.608 6.12501 20.268 6.34001 19.588L7.68545 14.3265M1.78774 14.3265C2.76898 14.5762 3.5689 16.9715 4.37601 17C5.35607 17.0346 6.34671 14.6477 7.68545 14.3265M12.3148 14.3265L13.661 19.588C13.875 20.269 13.983 20.609 14.255 20.804C14.527 21 14.893 21 15.623 21H16.793C17.928 21 18.496 21 18.803 20.642C19.111 20.285 19.012 19.742 18.815 18.652L18.2123 14.326M12.3148 14.3265C13.6343 14.7183 14.621 16.5451 15.623 16.5C16.4184 16.4642 17.2235 14.6375 18.2123 14.326" stroke="#666666" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

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

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

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

After

Width:  |  Height:  |  Size: 490 B

View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.5859 10.0001L0.792969 2.20718L2.20718 0.792969L10.0001 8.58582L17.793 0.792969L19.2072 2.20718L11.4143 10.0001L19.2072 17.7929L17.793 19.2072L10.0001 11.4143L2.20718 19.2072L0.792969 17.7929L8.5859 10.0001Z" fill="#999999"/>
</svg>

After

Width:  |  Height:  |  Size: 340 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

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

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

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

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

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

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

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

Before

Width:  |  Height:  |  Size: 1.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 470 KiB

View File

Before

Width:  |  Height:  |  Size: 471 KiB

After

Width:  |  Height:  |  Size: 471 KiB

View File

Before

Width:  |  Height:  |  Size: 598 KiB

After

Width:  |  Height:  |  Size: 598 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 470 KiB

After

Width:  |  Height:  |  Size: 470 KiB

View File

Before

Width:  |  Height:  |  Size: 301 KiB

After

Width:  |  Height:  |  Size: 301 KiB

View File

Before

Width:  |  Height:  |  Size: 400 KiB

After

Width:  |  Height:  |  Size: 400 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 335 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 453 KiB

View File

@ -1,36 +1,59 @@
<template>
<div class="overflow-hidden">
<Notifications />
<Login v-if="screen === 'login'" />
<!-- <Register v-if="screen === 'register'" />-->
<Characters v-if="screen === 'characters'" />
<Game v-if="screen === 'game'" />
</div>
<Notifications />
<BackgroundImageLoader />
<GmPanel v-if="gameStore.character?.role === 'gm'" />
<component :is="currentScreen" />
</template>
<script setup lang="ts">
import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import Notifications from '@/components/utilities/Notifications.vue'
import Login from '@/screens/Login.vue'
// import Register from '@/screens/Register.vue'
import Characters from '@/screens/Characters.vue'
import Game from '@/screens/Game.vue'
import { computed } from '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'
const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore()
const screen = computed(() => {
if (!gameStore.connection) {
return 'login'
} else if (gameStore.token && gameStore.connection) {
if (gameStore.character) {
return 'game'
}
return 'characters'
}
return 'login' // default fallback
const currentScreen = computed(() => {
if (!gameStore.connection) return Login
if (!gameStore.token) return Login
if (!gameStore.character) return Characters
if (zoneEditorStore.active) return ZoneEditor
return Game
})
// Disable right click
addEventListener('contextmenu', (event) => event.preventDefault())
// Watch zoneEditorStore.active and empty gameStore.game.loadedAssets
watch(
() => zoneEditorStore.active,
() => {
gameStore.game.loadedAssets = []
}
)
// #209: Play sound when a button is pressed
addEventListener('click', (event) => {
if (!(event.target instanceof HTMLButtonElement)) {
return
}
const audio = new Audio('/assets/music/click-btn.mp3')
audio.play()
})
// Watch for "G" key press and toggle the gm panel
addEventListener('keydown', (event) => {
if (gameStore.character?.role !== 'gm') return // Only allow toggling the gm panel if the character is a gm
// Check if no input is active or focus is on an input
if (event.repeat || event.isComposing || event.defaultPrevented || document.activeElement?.tagName.toUpperCase() === 'INPUT' || document.activeElement?.tagName.toUpperCase() === 'TEXTAREA') return
if (event.key === 'G') {
gameStore.toggleGmPanel()
}
})
</script>

View File

@ -12,7 +12,7 @@
//Globals
body {
@apply bg-black m-0 select-none overscroll-none overflow-hidden;
@apply bg-black m-0 select-none;
-ms-overflow-style: none;
scrollbar-width: none;
@ -20,6 +20,17 @@ body {
-webkit-user-select: none; /* Safari */
-ms-user-select: none; /* IE 10 and IE 11 */
user-select: none; /* Standard syntax */
// 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,
@ -55,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 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 {
@ -87,7 +105,7 @@ button {
@apply text-center;
&.btn-cyan {
@apply bg-cyan text-gray-50 text-base rounded py-2.5;
@apply bg-cyan text-gray-50 text-base leading-5 rounded py-2.5;
&.active,
&:hover {
@ -96,7 +114,7 @@ button {
}
&.btn-red {
@apply bg-red text-gray-50 text-base rounded py-2.5;
@apply bg-red text-gray-50 text-base leading-5 rounded py-2.5;
&.active,
&:hover {
@ -104,6 +122,16 @@ button {
}
}
&.btn-empty {
@apply text-gray-50 border-2 border-solid border-gray-500 text-base leading-5 rounded py-2.5;
&.active,
&.selected,
&:hover {
@apply bg-gray-700 border-gray-700;
}
}
&:hover {
@apply cursor-pointer;
}
@ -111,6 +139,16 @@ button {
&.eye-open {
@apply bg-[url('/assets/icons/eye-closed.svg')] w-5 h-4 right-2.5;
}
&.selected {
@apply bg-gray-500 border-gray-400;
}
}
.character {
&.active {
@apply pr-px border-r-0;
}
}
.text-pixel {
@ -120,3 +158,21 @@ button {
::-webkit-scrollbar {
@apply hidden;
}
.scrollbar {
&::-webkit-scrollbar {
@apply block w-0.5 bg-gray-300 rounded-sm;
}
&::-webkit-scrollbar-thumb {
@apply bg-gray-175;
}
}
canvas {
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-crisp-edges;
image-rendering: pixelated;
position: fixed;
left: 0;
top: 0;
}

View File

@ -1,28 +1,41 @@
<template>
<Scene name="effects" @preload="preloadScene" @create="createScene" @update="updateScene">
</Scene>
<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 { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { onBeforeMount, onBeforeUnmount, ref } from 'vue'
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 zoneEditorStore = useZoneEditorStore()
const zoneStore = useZoneStore()
// Scene ref
const sceneRef = ref<Phaser.Scene | null>(null)
// Effect-related refs
const dayNightCycle = ref<Phaser.GameObjects.Graphics | 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)
// Effect parameters
const dayNightDuration = 300000 // 5 minutes in milliseconds
const maxDarkness = 0.7
// 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')
@ -30,28 +43,24 @@ const preloadScene = async (scene: Phaser.Scene) => {
const createScene = async (scene: Phaser.Scene) => {
sceneRef.value = scene
createDayNightCycle(scene)
setupEffects(scene)
setupSocketListeners()
}
const updateScene = () => {
updateEffects()
}
// Effect setup
const setupEffects = (scene: Phaser.Scene) => {
createLightEffect(scene)
createRainEffect(scene)
createFogEffect(scene)
}
const updateScene = (scene: Phaser.Scene, time: number) => {
updateDayNightCycle(time)
updateFogEffect()
}
const createDayNightCycle = (scene: Phaser.Scene) => {
dayNightCycle.value = scene.add.graphics()
dayNightCycle.value.setDepth(1000)
}
const updateDayNightCycle = (time: number) => {
if (!dayNightCycle.value) return
const darkness = Math.sin((time % dayNightDuration) / dayNightDuration * Math.PI) * maxDarkness
dayNightCycle.value.clear()
dayNightCycle.value.fillStyle(0x000000, darkness)
dayNightCycle.value.fillRect(0, 0, window.innerWidth, window.innerHeight)
const createLightEffect = (scene: Phaser.Scene) => {
lightEffect.value = scene.add.graphics()
lightEffect.value.setDepth(1000)
}
const createRainEffect = (scene: Phaser.Scene) => {
@ -66,13 +75,7 @@ const createRainEffect = (scene: Phaser.Scene) => {
blendMode: 'ADD'
})
rainEmitter.value.setDepth(900)
toggleRain(true) // Start with rain off
}
const toggleRain = (isRaining: boolean) => {
if (rainEmitter.value) {
rainEmitter.value.setVisible(isRaining)
}
rainEmitter.value.stop()
}
const createFogEffect = (scene: Phaser.Scene) => {
@ -82,28 +85,134 @@ const createFogEffect = (scene: Phaser.Scene) => {
fogSprite.value.setDepth(950)
}
const updateFogEffect = () => {
if (fogSprite.value) {
// Example: Oscillate fog opacity
const fogOpacity = (Math.sin(Date.now() / 5000) + 1) / 2 * 0.3
fogSprite.value.setAlpha(fogOpacity)
// 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()
}
}
// Expose methods to control effects
const controlEffects = {
toggleRain,
setFogDensity: (density: number) => {
if (fogSprite.value) {
fogSprite.value.setAlpha(density)
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()
}
}
// Make control methods available to parent components
defineExpose(controlEffects)
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">
<span class="text-xs">{{ chip }}</span>
<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>
</div>
<input class="outline-none border-none p-1" placeholder="Tag name" v-model="currentInput" @keypress.enter.prevent="addChip" @keydown.backspace="handleBackspace" />
<input class="outline-none border-none p-1 text-gray-300" placeholder="Tag name" v-model="currentInput" @keypress.enter.prevent="addChip" @keydown.backspace="handleBackspace" />
</div>
</template>

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">
<Modal :isModalOpen="gameStore.uiSettings.isGmPanelOpen" @modal:close="() => gameStore.toggleGmPanel()" :modal-width="1000" :modal-height="650" :can-full-screen="true" :disable-bg-texture="true">
<template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-gray-300">GM Panel</h3>
<div class="flex gap-1.5 flex-wrap">
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">General</button>
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Users</button>
<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>
</div>
</template>
<template #modalBody>
@ -22,8 +22,10 @@ import { ref } from 'vue'
import Modal from '@/components/utilities/Modal.vue'
import AssetManager from '@/components/gameMaster/assetManager/AssetManager.vue'
import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore()
let toggle = ref('asset-manager')
</script>

View File

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

View File

@ -1,56 +1,72 @@
<template>
<div class="flex h-full w-full relative">
<div class="w-2/12 flex flex-col relative">
<div class="w-2/12 flex flex-col relative overflow-auto">
<!-- Asset Categories -->
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'tiles' }" @click="() => (selectedCategory = 'tiles')">
<span>Tiles</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
<span :class="{ 'text-white': selectedCategory === 'tiles' }">Tiles</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'objects' }" @click="() => (selectedCategory = 'objects')">
<span>Objects</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
<span :class="{ 'text-white': selectedCategory === 'objects' }">Objects</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'sprites' }" @click="() => (selectedCategory = 'sprites')">
<span>Sprites</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
<span :class="{ 'text-white': selectedCategory === 'sprites' }">Sprites</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
<a class="relative p-2.5 hover:cursor-pointer">
<span>Items</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
<a class="relative p-2.5 hover:cursor-pointer">
<span>NPC's</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
<a class="relative p-2.5 hover:cursor-pointer">
<span>Characters</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
<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>
<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>
<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>
<a class="relative p-2.5 hover:cursor-pointer">
<span>Mounts</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
<a class="relative p-2.5 hover:cursor-pointer">
<span>Pets</span>
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
<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>
</div>
<div class="absolute w-px bg-cyan-200 h-full top-0 left-1/6"></div>
<div class="absolute w-px bg-gray-500 h-full top-0 left-1/6"></div>
<!-- Assets list -->
<div class="overflow-auto h-full w-4/12 flex flex-col relative">
<TileList v-if="selectedCategory === 'tiles'" />
<ObjectList v-if="selectedCategory === 'objects'" />
<SpriteList v-if="selectedCategory === 'sprites'" />
<CharacterTypeList v-if="selectedCategory === 'characterTypes'" />
<CharacterHairList v-if="selectedCategory === 'characterHair'" />
</div>
<div class="absolute w-px bg-cyan-200 h-full top-0 left-1/2"></div>
<div class="absolute w-px bg-gray-500 h-full top-0 left-1/2"></div>
<!-- Asset details -->
<div class="flex w-1/2 after:hidden flex-col relative overflow-auto">
<TileDetails v-if="selectedCategory === 'tiles' && assetManagerStore.selectedTile" />
<ObjectDetails v-if="selectedCategory === 'objects' && assetManagerStore.selectedObject" />
<SpriteDetails v-if="selectedCategory === 'sprites' && assetManagerStore.selectedSprite" />
<CharacterTypeDetails v-if="selectedCategory === 'characterTypes' && assetManagerStore.selectedCharacterType" />
<CharacterHairDetails v-if="selectedCategory === 'characterHair' && assetManagerStore.selectedCharacterHair" />
</div>
</div>
</template>
@ -64,6 +80,10 @@ import ObjectList from '@/components/gameMaster/assetManager/partials/object/Obj
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 CharacterTypeDetails from '@/components/gameMaster/assetManager/partials/characterType/CharacterTypeDetails.vue'
import CharacterHairList from '@/components/gameMaster/assetManager/partials/characterHair/CharacterHairList.vue'
import CharacterHairDetails from '@/components/gameMaster/assetManager/partials/characterHair/CharacterHairDetails.vue'
const assetManagerStore = useAssetManagerStore()
const selectedCategory = ref('tiles')

View File

@ -0,0 +1,124 @@
<template>
<div class="h-full overflow-auto">
<div class="m-2.5 p-2.5 block">
<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="isEnabledForCharCreation">Is enabled for character creation</label>
<select v-model="characterIsEnabledForCharCreation" class="input-field" name="isEnabledForCharCreation">
<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 { CharacterHair, CharacterGender, Sprite } from '@/types'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore'
const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore()
const selectedCharacterHair = computed(() => assetManagerStore.selectedCharacterHair)
const characterName = ref('')
const characterGender = ref<CharacterGender>('MALE' as CharacterGender.MALE)
const characterIsEnabledForCharCreation = 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
characterIsEnabledForCharCreation.value = selectedCharacterHair.value.isEnabledForCharCreation
characterSpriteId.value = selectedCharacterHair.value.spriteId
}
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,
isEnabledForCharCreation: characterIsEnabledForCharCreation.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
characterIsEnabledForCharCreation.value = characterHair.isEnabledForCharCreation
characterSpriteId.value = characterHair.spriteId
})
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,93 @@
<template>
<div class="relative p-2.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">
<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 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: characterHair } in list" :key="characterHair.id" class="relative p-2.5 cursor-pointer block" :class="{ 'bg-cyan/80': assetManagerStore.selectedCharacterHair?.id === characterHair.id }" @click="assetManagerStore.setSelectedCharacterHair(characterHair as CharacterHair)">
<div class="flex items-center gap-2.5">
<span :class="{ 'text-white': assetManagerStore.selectedCharacterHair?.id === characterHair.id }">{{ characterHair.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>
</template>
<script setup lang="ts">
import { useGameStore } from '@/stores/gameStore'
import { onMounted, ref, computed } from 'vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import type { CharacterHair } from '@/types'
import { useVirtualList } from '@vueuse/core'
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

@ -0,0 +1,135 @@
<template>
<div class="h-full overflow-auto">
<div class="m-2.5 p-2.5 block">
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveCharacterType">
<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-half">
<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-half">
<label for="race">Race</label>
<select v-model="characterRace" class="input-field" name="race">
<option v-for="race in raceOptions" :key="race" :value="race">{{ race }}</option>
</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">
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>
</div>
<div class="form-field-full">
<label for="spriteId">Sprite</label>
<select v-model="characterSpriteId" class="input-field" name="spriteId">
<option disabled selected value="">Select sprite</option>
<option v-for="sprite in assetManagerStore.spriteList" :key="sprite.id" :value="sprite.id">{{ sprite.name }}</option>
</select>
</div>
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeCharacterType">Remove</button>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import type { CharacterType, CharacterGender, CharacterRace, Sprite } from '@/types'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore'
const gameStore = useGameStore()
const assetManagerStore = useAssetManagerStore()
const selectedCharacterType = computed(() => assetManagerStore.selectedCharacterType)
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 characterSpriteId = ref<string | null | undefined>(null)
const genderOptions: CharacterGender[] = ['MALE' as CharacterGender.MALE, 'FEMALE' as CharacterGender.FEMALE]
const raceOptions: CharacterRace[] = ['HUMAN' as CharacterRace.HUMAN, 'ELF' as CharacterRace.ELF, 'DWARF' as CharacterRace.DWARF, 'ORC' as CharacterRace.ORC, 'GOBLIN' as CharacterRace.GOBLIN]
if (!selectedCharacterType.value) {
console.error('No character type selected')
}
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
}
function removeCharacterType() {
if (!selectedCharacterType.value) return
gameStore.connection?.emit('gm:characterType:remove', { id: selectedCharacterType.value.id }, (response: boolean) => {
if (!response) {
console.error('Failed to remove character type')
return
}
refreshCharacterTypeList()
})
}
function refreshCharacterTypeList(unsetSelectedCharacterType = true) {
gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => {
assetManagerStore.setCharacterTypeList(response)
if (unsetSelectedCharacterType) {
assetManagerStore.setSelectedCharacterType(null)
}
})
}
function saveCharacterType() {
const characterTypeData = {
id: selectedCharacterType.value!.id,
name: characterName.value,
gender: characterGender.value,
race: characterRace.value,
isEnabledForCharCreation: characterIsEnabledForCharCreation.value,
spriteId: characterSpriteId.value
}
gameStore.connection?.emit('gm:characterType:update', characterTypeData, (response: boolean) => {
if (!response) {
console.error('Failed to save character type')
return
}
refreshCharacterTypeList(false)
})
}
watch(selectedCharacterType, (characterType: CharacterType | null) => {
if (!characterType) return
characterName.value = characterType.name
characterGender.value = characterType.gender
characterRace.value = characterType.race
characterIsEnabledForCharCreation.value = characterType.isEnabledForCharCreation
characterSpriteId.value = characterType.spriteId
})
onMounted(() => {
if (!selectedCharacterType.value) return
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
assetManagerStore.setSpriteList(response)
})
})
onBeforeUnmount(() => {
assetManagerStore.setSelectedCharacterType(null)
})
</script>

View File

@ -0,0 +1,93 @@
<template>
<div class="relative p-2.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">
<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 class="flex items-center gap-2.5">
<span :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>
</template>
<script setup lang="ts">
import { useGameStore } from '@/stores/gameStore'
import { onMounted, ref, computed } from 'vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore'
import type { CharacterType } from '@/types'
import { useVirtualList } from '@vueuse/core'
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 createNewCharacterType = () => {
gameStore.connection?.emit('gm:characterType:create', {}, (response: boolean) => {
if (!response) {
console.error('Failed to create new character type')
return
}
gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => {
assetManagerStore.setCharacterTypeList(response)
})
})
}
const filteredCharacterTypes = computed(() => {
if (!searchQuery.value) {
return assetManagerStore.characterTypeList
}
return assetManagerStore.characterTypeList.filter((character) => character.name.toLowerCase().includes(searchQuery.value.toLowerCase()))
})
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(filteredCharacterTypes, {
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:characterType:list', {}, (response: CharacterType[]) => {
assetManagerStore.setCharacterTypeList(response)
})
})
</script>

View File

@ -4,7 +4,7 @@
<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-cyan-200"></div>
<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">
@ -118,7 +118,6 @@ function saveObject() {
return
}
console.log(objectTags.value)
gameStore.connection?.emit(
'gm:object:update',
{

View File

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

View File

@ -10,7 +10,7 @@
<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-cyan-200"></div>
<div class="w-[calc(100%_+_32px)] absolute left-[-15px] bottom-0 h-px bg-gray-500"></div>
</div>
</div>

View File

@ -1,23 +1,23 @@
<template>
<div class="relative p-2.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/50 border border-solid border-white/25 rounded drop-shadow-20 p-2 inline-flex items-center justify-center hover:bg-cyan hover:cursor-pointer">
<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">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
</button>
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</div>
<div v-bind="containerProps" class="overflow-y-auto relative" @scroll="onScroll">
<div v-bind="wrapperProps" ref="elementToScroll">
<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 class="flex items-center gap-2.5">
<span>{{ sprite.name }}</span>
<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-cyan-200"></div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a>
</div>
<button class="left-[calc(50%_-_60px)] fixed bottom-2.5 min-w-[unset] w-12 h-12 rounded-md bg-cyan/50 p-0 hover:bg-cyan" v-show="hasScrolled" @click="toTop">
<button class="left-[calc(50%_-_60px)] fixed bottom-2.5 min-w-[unset] w-12 h-12 rounded-md bg-cyan 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>

View File

@ -4,7 +4,7 @@
<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-cyan-200"></div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</div>
<div class="m-2.5 p-2.5 block">
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveTile">

View File

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

View File

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

View File

@ -1,7 +1,7 @@
<template>
<Modal :isModalOpen="true" @modal:close="() => zoneEditorStore.toggleCreateZoneModal()" :modal-width="300" :modal-height="400" :is-resizable="false">
<Modal :isModalOpen="true" @modal:close="() => zoneEditorStore.toggleCreateZoneModal()" :modal-width="300" :modal-height="420" :is-resizable="false" :disable-bg-texture="true">
<template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-gray-300">Create new zone</h3>
<h3 class="m-0 font-medium shrink-0 text-white">Create new zone</h3>
</template>
<template #modalBody>

View File

@ -1,70 +1,59 @@
<template>
<Teleport to="body">
<Modal :isModalOpen="zoneEditorStore.isObjectListModalShown" :modal-width="645" :modal-height="260" @modal:close="() => (zoneEditorStore.isObjectListModalShown = false)">
<template #modalHeader>
<h3 class="text-lg text-gray-300">Objects</h3>
<div class="flex">
<div class="w-full flex gap-1.5 flex-row">
<div>
<label class="mb-1.5 font-titles hidden" for="search">Search...</label>
<input @mousedown.stop class="input-field" type="text" name="search" placeholder="Search" v-model="searchQuery" />
</div>
<!-- <div>-->
<!-- <label class="mb-1.5 font-titles hidden" for="depth">Depth</label>-->
<!-- <input v-model="objectDepth" @mousedown.stop class="input-field" type="number" name="depth" placeholder="Depth" />-->
<!-- </div>-->
<Modal :isModalOpen="zoneEditorStore.isObjectListModalShown" :modal-width="645" :modal-height="260" @modal:close="() => (zoneEditorStore.isObjectListModalShown = false)" :disable-bg-texture="true">
<template #modalHeader>
<h3 class="text-lg text-white">Objects</h3>
</template>
<template #modalBody>
<div class="flex pt-4 pl-4">
<div class="w-full flex gap-1.5 flex-row">
<div>
<label class="mb-1.5 font-titles hidden" for="search">Search...</label>
<input @mousedown.stop class="input-field" type="text" name="search" placeholder="Search" v-model="searchQuery" />
</div>
</div>
</template>
<template #modalBody>
<div class="flex flex-col h-full p-4">
<div class="mb-4 flex flex-wrap gap-2">
<button v-for="tag in uniqueTags" :key="tag" @click="toggleTag(tag)" class="btn-cyan" :class="{ 'opacity-50': !selectedTags.includes(tag) }">
{{ tag }}
</button>
</div>
<div class="h-full 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">
<img
class="border-2 border-solid max-w-full"
:src="`${config.server_endpoint}/assets/objects/${object.id}.png`"
alt="Object"
@click="zoneEditorStore.setSelectedObject(object)"
: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
}"
/>
</div>
</div>
<div class="flex flex-col h-full p-4">
<div class="mb-4 flex flex-wrap gap-2">
<button v-for="tag in uniqueTags" :key="tag" @click="toggleTag(tag)" class="btn-cyan" :class="{ 'opacity-50': !selectedTags.includes(tag) }">
{{ tag }}
</button>
</div>
<div class="h-full 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">
<img
class="border-2 border-solid max-w-full"
:src="`${config.server_endpoint}/assets/objects/${object.id}.png`"
alt="Object"
@click="zoneEditorStore.setSelectedObject(object)"
: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
}"
/>
</div>
</div>
</div>
</template>
</Modal>
</Teleport>
</div>
</template>
</Modal>
</template>
<script setup lang="ts">
import config from '@/config'
import { ref, onMounted, computed, watch } from 'vue'
import { ref, onMounted, computed } from 'vue'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { useGameStore } from '@/stores/gameStore'
import Modal from '@/components/utilities/Modal.vue'
import type { Object } from '@/types'
import type { Object, ZoneObject } from '@/types'
const gameStore = useGameStore()
const isModalOpen = ref(false)
const zoneEditorStore = useZoneEditorStore()
const searchQuery = ref('')
// const objectDepth = ref(0)
const selectedTags = ref<string[]>([])
// watch(objectDepth, (depth) => {
// zoneEditorStore.setObjectDepth(depth)
// })
const uniqueTags = computed(() => {
const allTags = zoneEditorStore.objectList.flatMap((obj) => obj.tags || [])
return Array.from(new Set(allTags))
@ -87,8 +76,6 @@ const toggleTag = (tag: string) => {
}
onMounted(async () => {
zoneEditorStore.setObjectDepth(0)
isModalOpen.value = true
gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => {
zoneEditorStore.setObjectList(response)

View File

@ -1,10 +1,6 @@
<template>
<div class="flex flex-col items-center py-5 px-3 fixed bottom-14 right-0" v-if="zoneEditorStore.selectedZoneObject">
<div class="flex flex-col items-center py-5 px-3 fixed bottom-14 right-0">
<div class="self-end mt-2 flex gap-2">
<div>
<label class="mb-1.5 font-titles block text-sm text-gray-700 hidden" for="depth">Depth</label>
<input v-model="objectDepth" @mousedown.stop @input="handleDepthInput" class="input-field max-w-24 px-2 py-1 border rounded" type="number" name="depth" placeholder="Depth" />
</div>
<button @mousedown.stop @click="handleDelete" class="btn-red py-1.5 px-4">
<img src="/assets/icons/trashcan.svg" class="w-4 h-4" alt="Delete" />
</button>
@ -15,38 +11,23 @@
</template>
<script setup lang="ts">
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { ref, computed, watch } from 'vue'
import type { ZoneObject } from '@/types'
const emit = defineEmits(['update_depth', 'move', 'delete', 'rotate'])
const zoneEditorStore = useZoneEditorStore()
const props = defineProps<{
zoneObject: ZoneObject
}>()
const objectDepth = ref(zoneEditorStore.objectDepth)
const emit = defineEmits(['move', 'rotate', 'delete'])
watch(
() => zoneEditorStore.selectedZoneObject,
(selectedZoneObject) => {
objectDepth.value = selectedZoneObject?.depth ?? 0
}
)
const handleDepthInput = () => {
const depth = parseFloat(objectDepth.value.toString())
if (!isNaN(depth)) {
emit('update_depth', depth)
}
const handleMove = () => {
emit('move', props.zoneObject.id)
}
const handleRotate = () => {
emit('rotate', zoneEditorStore.selectedZoneObject?.id)
}
const handleMove = () => {
emit('move')
emit('rotate', props.zoneObject.id)
}
const handleDelete = () => {
emit('delete', zoneEditorStore.selectedZoneObject?.id)
zoneEditorStore.setSelectedZoneObject(null)
emit('delete', props.zoneObject.id)
}
</script>

View File

@ -1,9 +1,8 @@
<template>
<Modal :is-modal-open="true" @modal:close="() => zoneEditorStore.setTool('move')" :modal-width="300" :modal-height="350" :is-resizable="false">
<Modal :is-modal-open="showTeleportModal" @modal:close="() => zoneEditorStore.setTool('move')" :modal-width="300" :modal-height="350" :is-resizable="false" :disable-bg-texture="true">
<template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-gray-300">Teleport settings</h3>
<h3 class="m-0 font-medium shrink-0 text-white">Teleport settings</h3>
</template>
<template #modalBody>
<div class="m-4">
<form method="post" @submit.prevent="" class="inline">
@ -40,12 +39,13 @@
</template>
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import Modal from '@/components/utilities/Modal.vue'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { useGameStore } from '@/stores/gameStore'
import type { Zone } from '@/types'
const showTeleportModal = computed(() => zoneEditorStore.tool === 'pencil' && zoneEditorStore.drawMode === 'teleport')
const zoneEditorStore = useZoneEditorStore()
const gameStore = useGameStore()

View File

@ -1,9 +1,11 @@
<template>
<Teleport to="body">
<Modal :isModalOpen="zoneEditorStore.isTileListModalShown" :modal-width="645" :modal-height="600" @modal:close="() => (zoneEditorStore.isTileListModalShown = false)">
<template #modalHeader>
<h3 class="text-lg text-gray-300">Tiles</h3>
<div class="flex">
<Modal :isModalOpen="zoneEditorStore.isTileListModalShown" :modal-width="645" :modal-height="600" @modal:close="() => (zoneEditorStore.isTileListModalShown = false)" :disable-bg-texture="true">
<template #modalHeader>
<h3 class="text-lg text-white">Tiles</h3>
</template>
<template #modalBody>
<div class="h-full overflow-auto" v-if="!selectedGroup">
<div class="flex pt-4 pl-4">
<div class="w-full flex gap-1.5 flex-row">
<div>
<label class="mb-1.5 font-titles hidden" for="search">Search...</label>
@ -11,8 +13,6 @@
</div>
</div>
</div>
</template>
<template #modalBody>
<div class="flex flex-col h-full p-4">
<div class="mb-4 flex flex-wrap gap-2">
<button v-for="tag in uniqueTags" :key="tag" @click="toggleTag(tag)" class="btn-cyan" :class="{ 'opacity-50': !selectedTags.includes(tag) }">
@ -21,25 +21,63 @@
</div>
<div class="h-[calc(100%_-_60px)] flex-grow overflow-y-auto">
<div class="grid grid-cols-8 gap-2 justify-items-center">
<div v-for="tile in filteredTiles" :key="tile.id" class="flex items-center justify-center">
<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"
:src="`${config.server_endpoint}/assets/tiles/${tile.id}.png`"
alt="Tile"
@click="zoneEditorStore.setSelectedTile(tile)"
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`"
:alt="group.parent.name"
@click="openGroup(group)"
@load="() => processTile(group.parent)"
:class="{
'cursor-pointer transition-all duration-300': true,
'border-cyan shadow-lg scale-105': zoneEditorStore.selectedTile?.id === tile.id,
'border-transparent hover:border-gray-300': zoneEditorStore.selectedTile?.id !== tile.id
'border-cyan shadow-lg scale-105': isActiveTile(group.parent),
'border-transparent hover:border-gray-300': !isActiveTile(group.parent)
}"
/>
<span class="text-xs mt-1">{{ getTileCategory(group.parent) }}</span>
<span v-if="group.children.length > 0" class="absolute top-0 right-0 bg-cyan text-white rounded-full w-5 h-5 flex items-center justify-center text-xs">
{{ group.children.length + 1 }}
</span>
</div>
</div>
</div>
</div>
</template>
</Modal>
</Teleport>
</div>
<div v-else class="h-full overflow-auto">
<div class="p-4">
<button @click="closeGroup" class="btn-cyan mb-4">Back to All Tiles</button>
<h4 class="text-lg mb-4">{{ selectedGroup.parent.name }} Group</h4>
<div class="grid grid-cols-8 gap-2 justify-items-center">
<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`"
:alt="selectedGroup.parent.name"
@click="selectTile(selectedGroup.parent.id)"
:class="{
'border-cyan shadow-lg scale-105': isActiveTile(selectedGroup.parent),
'border-transparent hover:border-gray-300': !isActiveTile(selectedGroup.parent)
}"
/>
<span class="text-xs mt-1">{{ getTileCategory(selectedGroup.parent) }}</span>
</div>
<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`"
:alt="childTile.name"
@click="selectTile(childTile.id)"
:class="{
'border-cyan shadow-lg scale-105': isActiveTile(childTile),
'border-transparent hover:border-gray-300': !isActiveTile(childTile)
}"
/>
<span class="text-xs mt-1">{{ getTileCategory(childTile) }}</span>
</div>
</div>
</div>
</div>
</template>
</Modal>
</template>
<script setup lang="ts">
@ -55,20 +93,60 @@ const isModalOpen = ref(false)
const zoneEditorStore = useZoneEditorStore()
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 uniqueTags = computed(() => {
const allTags = zoneEditorStore.tileList.flatMap((tile) => tile.tags || [])
return Array.from(new Set(allTags))
})
const filteredTiles = computed(() => {
return zoneEditorStore.tileList.filter((tile) => {
const groupedTiles = computed(() => {
const groups: { parent: Tile; children: Tile[] }[] = []
const filteredTiles = zoneEditorStore.tileList.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
})
filteredTiles.forEach((tile) => {
const parentGroup = groups.find((group) => areTilesRelated(group.parent, tile))
if (parentGroup && parentGroup.parent.id !== tile.id) {
parentGroup.children.push(tile)
} else {
groups.push({ parent: tile, children: [] })
}
})
return groups
})
const tileColorData = ref<Map<string, { r: number; g: number; b: number }>>(new Map())
const tileEdgeData = ref<Map<string, number>>(new Map())
function areTilesRelated(tile1: Tile, tile2: Tile): boolean {
const colorSimilarityThreshold = 30 // Adjust this value as needed
const edgeComplexitySimilarityThreshold = 20 // Adjust this value as needed
const color1 = tileColorData.value.get(tile1.id)
const color2 = tileColorData.value.get(tile2.id)
const edge1 = tileEdgeData.value.get(tile1.id)
const edge2 = tileEdgeData.value.get(tile2.id)
if (!color1 || !color2 || edge1 === undefined || edge2 === undefined) {
return false
}
const colorDifference = Math.sqrt(Math.pow(color1.r - color2.r, 2) + Math.pow(color1.g - color2.g, 2) + Math.pow(color1.b - color2.b, 2))
const edgeComplexityDifference = Math.abs(edge1 - edge2)
const namePrefix1 = tile1.name.split('_')[0]
const namePrefix2 = tile2.name.split('_')[0]
return colorDifference <= colorSimilarityThreshold && edgeComplexityDifference <= edgeComplexitySimilarityThreshold && namePrefix1 === namePrefix2
}
const toggleTag = (tag: string) => {
if (selectedTags.value.includes(tag)) {
selectedTags.value = selectedTags.value.filter((t) => t !== tag)
@ -77,10 +155,82 @@ const toggleTag = (tag: string) => {
}
}
function processTile(tile: Tile) {
const img = new Image()
img.crossOrigin = 'Anonymous'
img.onload = () => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = img.width
canvas.height = img.height
ctx!.drawImage(img, 0, 0, img.width, img.height)
const imageData = ctx!.getImageData(0, 0, canvas.width, canvas.height)
tileColorData.value.set(tile.id, getDominantColor(imageData))
tileEdgeData.value.set(tile.id, getEdgeComplexity(imageData))
}
img.src = `${config.server_endpoint}/assets/tiles/${tile.id}.png`
}
function getDominantColor(imageData: ImageData) {
let r = 0,
g = 0,
b = 0,
total = 0
for (let i = 0; i < imageData.data.length; i += 4) {
if (imageData.data[i + 3] > 0) {
// Only consider non-transparent pixels
r += imageData.data[i]
g += imageData.data[i + 1]
b += imageData.data[i + 2]
total++
}
}
return {
r: Math.round(r / total),
g: Math.round(g / total),
b: Math.round(b / total)
}
}
function getEdgeComplexity(imageData: ImageData) {
let edgePixels = 0
for (let y = 0; y < imageData.height; y++) {
for (let x = 0; x < imageData.width; x++) {
const i = (y * imageData.width + x) * 4
if (imageData.data[i + 3] > 0 && (x === 0 || y === 0 || x === imageData.width - 1 || y === imageData.height - 1 || imageData.data[i - 1] === 0 || imageData.data[i + 7] === 0)) {
edgePixels++
}
}
}
return edgePixels
}
function getTileCategory(tile: Tile): string {
return tileCategories.value.get(tile.id) || ''
}
function openGroup(group: { parent: Tile; children: Tile[] }) {
selectedGroup.value = group
}
function closeGroup() {
selectedGroup.value = null
}
function selectTile(tile: string) {
zoneEditorStore.setSelectedTile(tile)
}
function isActiveTile(tile: Tile): boolean {
return zoneEditorStore.selectedTile === tile.id
}
onMounted(async () => {
isModalOpen.value = true
gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => {
zoneEditorStore.setTileList(response)
response.forEach((tile) => processTile(tile))
})
})
</script>

View File

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

View File

@ -1,31 +1,28 @@
<template>
<CreateZone v-if="zoneEditorStore.isCreateZoneModalShown" />
<Teleport to="body">
<Modal @modal:close="() => zoneEditorStore.toggleZoneListModal()" :is-resizable="false" :is-modal-open="true" :modal-width="300" :modal-height="360">
<template #modalHeader>
<h3 class="text-lg text-gray-300">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-cyan-200" 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 py-0.5 px-2.5 z-50" @click.stop="() => deleteZone(zone.id)">X</button>
</span>
</div>
<div class="absolute left-0 bottom-0 w-full h-px bg-cyan-200"></div>
</div>
<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>
</template>
</Modal>
</Teleport>
<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">

View File

@ -1,24 +1,28 @@
<template>
<Modal :is-modal-open="zoneEditorStore.isSettingsModalShown" @modal:close="() => zoneEditorStore.toggleSettingsModal()" :modal-width="600" :modal-height="350">
<Modal :is-modal-open="zoneEditorStore.isSettingsModalShown" @modal:close="() => zoneEditorStore.toggleSettingsModal()" :modal-width="600" :modal-height="350" :disable-bg-texture="true">
<template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-gray-300">Zone settings</h3>
<h3 class="m-0 font-medium shrink-0 text-white">Zone settings</h3>
</template>
<template #modalBody>
<div class="m-4">
<form method="post" @submit.prevent="" class="inline">
<div class="gap-2.5 flex flex-wrap">
<div class="space-x-2">
<button class="btn-cyan py-1.5 px-4" type="button" @click.prevent="screen = 'settings'">Settings</button>
<button class="btn-cyan py-1.5 px-4" type="button" @click.prevent="screen = 'effects'">Effects</button>
</div>
<form method="post" @submit.prevent="" class="inline" v-if="screen === 'settings'">
<div class="gap-2.5 flex flex-wrap mt-4">
<div class="form-field-full">
<label for="name">Name</label>
<input class="input-field" v-model="name" name="name" id="name" />
</div>
<div class="form-field-half">
<label for="name">Width</label>
<input class="input-field" v-model="width" name="name" id="name" type="number" />
<label for="width">Width</label>
<input class="input-field" v-model="width" name="width" id="width" type="number" />
</div>
<div class="form-field-half">
<label for="name">Height</label>
<input class="input-field" v-model="height" name="name" id="name" type="number" />
<label for="height">Height</label>
<input class="input-field" v-model="height" name="height" id="height" type="number" />
</div>
<div class="form-field-full">
<label for="pvp">PVP enabled</label>
@ -29,6 +33,14 @@
</div>
</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">
<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>
</div>
<button class="btn-green py-1 px-2 mt-2" type="button" @click="addEffect">Add Effect</button>
</form>
</div>
</template>
</Modal>
@ -40,16 +52,19 @@ import Modal from '@/components/utilities/Modal.vue'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
const zoneEditorStore = useZoneEditorStore()
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.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(zoneEditorStore.zoneSettings.name)
const width = ref(zoneEditorStore.zoneSettings.width)
const height = ref(zoneEditorStore.zoneSettings.height)
const pvp = ref(zoneEditorStore.zoneSettings.pvp)
const name = ref(zoneEditorStore.zoneSettings?.name)
const width = ref(zoneEditorStore.zoneSettings?.width)
const height = ref(zoneEditorStore.zoneSettings?.height)
const pvp = ref(zoneEditorStore.zoneSettings?.pvp)
const zoneEffects = ref(zoneEditorStore.zoneSettings?.zoneEffects || [])
watch(name, (value) => {
zoneEditorStore.setZoneName(value)
@ -66,4 +81,26 @@ watch(height, (value) => {
watch(pvp, (value) => {
zoneEditorStore.setZonePvp(value)
})
watch(
zoneEffects,
(value) => {
zoneEditorStore.setZoneEffects(value)
},
{ deep: true }
)
const addEffect = () => {
zoneEffects.value.push({
id: Date.now().toString(), // Simple unique id generation
zoneId: zoneEditorStore.zone?.id,
zone: zoneEditorStore.zone,
effect: '',
strength: 1
})
}
const removeEffect = (index) => {
zoneEffects.value.splice(index, 1)
}
</script>

View File

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

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

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

View File

@ -0,0 +1,227 @@
<template>
<Controls :layer="tileLayer" :depth="0" />
</template>
<script setup lang="ts">
import config from '@/config'
import { useScene } from 'phavuer'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { onMounted, onUnmounted, watch } from 'vue'
import { createTileArray, getTile, placeTile, setLayerTiles } from '@/composables/zoneComposable'
import Controls from '@/components/utilities/Controls.vue'
import { useGameStore } from '@/stores/gameStore'
import type { AssetDataT } from '@/types'
const emit = defineEmits(['tileMap:create'])
const scene = useScene()
const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore()
const tileMap = createTileMap()
const tileLayer = createTileLayer()
/**
* A Tilemap is a container for Tilemap data.
* This isn't a display object, rather, it holds data about the map and allows you to add tilesets and tilemap layers to it.
* A map can have one or more tilemap layers, which are the display objects that actually render the tiles.
*/
function createTileMap() {
const zoneData = new Phaser.Tilemaps.MapData({
width: zoneEditorStore.zone?.width,
height: zoneEditorStore.zone?.height,
tileWidth: config.tile_size.x,
tileHeight: config.tile_size.y,
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
format: Phaser.Tilemaps.Formats.ARRAY_2D
})
const newTileMap = new Phaser.Tilemaps.Tilemap(scene, zoneData)
emit('tileMap:create', newTileMap)
return newTileMap
}
/**
* A Tileset is a combination of a single image containing the tiles and a container for data about each tile.
*/
function createTileLayer() {
const tilesArray = gameStore.getLoadedAssetsByGroup('tiles')
const tilesetImages = Array.from(tilesArray).map((tile: AssetDataT, index: number) => {
return tileMap.addTilesetImage(tile.key, tile.key, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y })
}) as any
// Add blank tile
tilesetImages.push(tileMap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 1, 2, 0, { x: 0, y: -config.tile_size.y }))
const layer = tileMap.createBlankLayer('tiles', tilesetImages, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
layer.setDepth(0)
layer.setCullPadding(2, 2)
return layer
}
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 tile
if (zoneEditorStore.drawMode !== 'tile') return
// Check if there is a selected tile
if (!zoneEditorStore.selectedTile) return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if there is a tile
const tile = getTile(tileLayer, pointer.worldX, pointer.worldY)
if (!tile) return
// Place tile
placeTile(tileMap, tileLayer, tile.x, tile.y, zoneEditorStore.selectedTile)
// Adjust zoneEditorStore.zone.tiles
zoneEditorStore.zone.tiles[tile.y][tile.x] = zoneEditorStore.selectedTile
}
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 tile
if (zoneEditorStore.eraserMode !== 'tile') return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if alt is pressed
if (pointer.event.altKey) return
// Check if there is a tile
const tile = getTile(tileLayer, pointer.worldX, pointer.worldY)
if (!tile) return
// Place tile
placeTile(tileMap, tileLayer, tile.x, tile.y, 'blank_tile')
// Adjust zoneEditorStore.zone.tiles
zoneEditorStore.zone.tiles[tile.y][tile.x] = 'blank_tile'
}
function paint(pointer: Phaser.Input.Pointer) {
// Check if zone is set
if (!zoneEditorStore.zone) return
// Check if tool is pencil
if (zoneEditorStore.tool !== 'paint') return
// Check if there is a selected tile
if (!zoneEditorStore.selectedTile) return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if alt is pressed
if (pointer.event.altKey) return
// Set new tileArray with selected tile
setLayerTiles(tileMap, tileLayer, createTileArray(tileMap.width, tileMap.height, zoneEditorStore.selectedTile))
// Adjust zoneEditorStore.zone.tiles
zoneEditorStore.zone.tiles = createTileArray(tileMap.width, tileMap.height, zoneEditorStore.selectedTile)
}
// When alt is pressed, and the pointer is down, select the tile that the pointer is over
function tilePicker(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 tile
if (zoneEditorStore.drawMode !== 'tile') return
// Check if left mouse button is pressed
if (!pointer.isDown) return
// Check if shift is not pressed, this means we are moving the camera
if (pointer.event.shiftKey) return
// Check if alt is pressed
if (!pointer.event.altKey) return
// Check if there is a tile
const tile = getTile(tileLayer, pointer.worldX, pointer.worldY)
if (!tile) return
// Select the tile
zoneEditorStore.setSelectedTile(zoneEditorStore.zone.tiles[tile.y][tile.x])
}
watch(
() => zoneEditorStore.shouldClearTiles,
(shouldClear) => {
if (shouldClear && zoneEditorStore.zone) {
const blankTiles = createTileArray(tileMap.width, tileMap.height, 'blank_tile')
setLayerTiles(tileMap, tileLayer, blankTiles)
zoneEditorStore.zone.tiles = blankTiles
zoneEditorStore.resetClearTilesFlag()
}
}
)
onMounted(() => {
if (!zoneEditorStore.zone?.tiles) {
return
}
// First fill the entire map with blank tiles using current zone dimensions
const blankTiles = createTileArray(zoneEditorStore.zone.width, zoneEditorStore.zone.height, 'blank_tile')
// Then overlay the zone tiles, but only within the current zone dimensions
const zoneTiles = zoneEditorStore.zone.tiles
for (let y = 0; y < zoneEditorStore.zone.height; y++) {
for (let x = 0; x < zoneEditorStore.zone.width; x++) {
// Only copy if the source tiles array has this position
if (zoneTiles[y] && zoneTiles[y][x] !== undefined) {
blankTiles[y][x] = zoneTiles[y][x]
}
}
}
setLayerTiles(tileMap, tileLayer, blankTiles)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, paint)
scene.input.on(Phaser.Input.Events.POINTER_DOWN, tilePicker)
})
onUnmounted(() => {
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, paint)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, tilePicker)
tileMap.destroyLayer('tiles')
tileMap.removeAllLayers()
tileMap.destroy()
})
</script>

View File

@ -0,0 +1,243 @@
<template>
<div class="relative" v-if="gameStore.uiSettings.isCharacterProfileOpen" :style="modalStyle">
<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 border border-solid border-gray-500 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 border border-solid border-gray-500 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 border border-solid border-gray-500 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 border border-solid border-gray-500 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 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600">
<img class="absolute w-6 h-6 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/profile/helmet.svg" />
</div>
<div class="w-9 h-9 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600">
<img class="absolute w-6 h-6 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/profile/chestplate.svg" />
</div>
<div class="flex gap-0.5 items-end">
<div class="w-6 h-6 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600">
<img class="absolute w-4 h-4 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/profile/boots.svg" />
</div>
<div class="w-9 h-9 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600">
<img class="absolute w-6 h-6 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" 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 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600"></div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref, watch, computed } from 'vue'
import { useGameStore } from '@/stores/gameStore'
const gameStore = useGameStore()
let startX = 0
let startY = 0
let initialX = 0
let initialY = 0
let modalPositionX = 0
let modalPositionY = 0
let modalWidth = 286
let modalHeight = 483
const width = ref(modalWidth)
const height = ref(modalHeight)
const x = ref(0)
const y = ref(0)
const isDragging = ref(false)
const modalStyle = computed(() => ({
top: `${y.value}px`,
left: `${x.value}px`,
width: `${width.value}px`,
height: `${height.value}px`,
maxWidth: '100vw',
maxHeight: '100vh'
}))
function startDrag(event: MouseEvent) {
isDragging.value = true
startX = event.clientX
startY = event.clientY
initialX = x.value
initialY = y.value
event.preventDefault()
}
function drag(event: MouseEvent) {
if (!isDragging.value) return
const dx = event.clientX - startX
const dy = event.clientY - startY
x.value = initialX + dx
y.value = initialY + dy
adjustPosition()
}
function stopDrag() {
isDragging.value = false
}
function adjustPosition() {
x.value = Math.min(x.value, window.innerWidth - width.value)
y.value = Math.min(y.value, window.innerHeight - height.value)
}
function initializePosition() {
width.value = Math.min(modalWidth, window.innerWidth)
height.value = Math.min(modalHeight, window.innerHeight)
if (modalPositionX !== 0 && modalPositionY !== 0) {
x.value = modalPositionX
y.value = modalPositionY
} else {
x.value = (window.innerWidth - width.value) / 2
y.value = (window.innerHeight - height.value) / 2
}
}
watch(
() => gameStore.uiSettings.isCharacterProfileOpen,
(value) => {
gameStore.uiSettings.isCharacterProfileOpen = value
if (value) {
initializePosition()
}
}
)
watch(
() => modalWidth,
(value) => {
width.value = Math.min(value, window.innerWidth)
}
)
watch(
() => modalHeight,
(value) => {
height.value = Math.min(value, window.innerHeight)
}
)
watch(
() => modalPositionX,
(value) => {
x.value = value
}
)
watch(
() => modalPositionY,
(value) => {
y.value = value
}
)
function keyPress(event: KeyboardEvent) {
if (event.altKey && event.key === 'c') {
gameStore.toggleCharacterProfile()
}
}
onMounted(() => {
addEventListener('keydown', keyPress)
addEventListener('mousemove', drag)
addEventListener('mouseup', stopDrag)
initializePosition()
})
onUnmounted(() => {
removeEventListener('keydown', keyPress)
removeEventListener('mousemove', drag)
removeEventListener('mouseup', stopDrag)
})
</script>

View File

@ -2,27 +2,29 @@
<div class="w-full md:min-w-[350px] max-w-[750px] flex flex-col absolute left-1/2 -translate-x-1/2 bottom-5">
<div ref="chatWindow" class="w-full overflow-auto h-32 mb-5 bg-gray rounded-md border-2 border-solid border-gray-500 text-gray-300" v-show="gameStore.uiSettings.isChatOpen">
<div v-for="message in chats" class="flex-col py-2 items-center p-3">
<span class="text-ellipsis overflow-hidden whitespace-nowrap text-sm" :class="{'text-cyan-300': gameStore.character?.role == 'gm'}">{{ message.character.name }}</span>
<span class="text-ellipsis overflow-hidden whitespace-nowrap text-sm" :class="{ 'text-cyan-300': gameStore.character?.role == 'gm' }">{{ message.character.name }}</span>
<p class="text-gray-50 m-0">{{ message.message }}</p>
</div>
</div>
<div class="w-64 mx-auto relative">
<img src="/assets/icons/chat-icon.svg" class="absolute top-1/2 -translate-y-1/2 left-1.5 h-4 w-4 opacity-50" />
<div class="w-96 mx-auto relative">
<img src="/assets/icons/ingameUI/chat-icon.svg" class="absolute top-1/2 -translate-y-1/2 left-2.5 h-4 w-4 opacity-50" />
<input
class="w-[204px] h-6 rounded-sm text-xs font-default pl-8 pr-4 py-0 bg-gray-600 border-2 border-solid border-gray-500 text-gray-300 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover focus:outline-none focus:ring-0 focus:border-cyan-800"
class="w-[332px] h-8 rounded-sm text-xs font-default pl-8 pr-4 py-0 bg-gray-600 border-2 border-solid border-gray-500 text-gray-300 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover focus:outline-none focus:ring-0 focus:border-cyan-800"
placeholder="Type something..."
v-model="message"
@keypress="handleKeyPress"
@submit="handleSubmit"
ref="chatInput"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, nextTick } from 'vue'
import { onBeforeUnmount, ref, nextTick } from 'vue'
import { onClickOutside } from '@vueuse/core'
import { useGameStore } from '@/stores/gameStore'
import type { Character, ChatMessage } from '@/types'
import type { Chat } from '@/types'
import { useZoneStore } from '@/stores/zoneStore'
import { useScene } from 'phavuer'
@ -31,12 +33,21 @@ const gameStore = useGameStore()
const zoneStore = useZoneStore()
const message = ref('')
const chats = ref([] as ChatMessage[])
const chats = ref([] as Chat[])
const chatWindow = ref<HTMLElement | null>(null)
const chatInput = ref<HTMLElement | null>(null)
onClickOutside(chatInput, event => unfocusChat(event, chatInput.value as HTMLElement))
function unfocusChat(event: Event, targetElement: HTMLElement) {
if (!(event.target instanceof Node) || !targetElement.contains(event.target)) {
targetElement.blur();
}
}
const sendMessage = () => {
if (!message.value.trim()) return
gameStore.connection?.emit('chat:send_message', { message: message.value }, (response: boolean) => {})
gameStore.connection?.emit('chat:message', { message: message.value }, (response: boolean) => {})
message.value = ''
}
@ -60,7 +71,7 @@ const scrollToBottom = () => {
})
}
gameStore.connection?.on('chat:message', (data: ChatMessage) => {
gameStore.connection?.on('chat:message', (data: Chat) => {
chats.value.push(data)
scrollToBottom()

View File

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

View File

@ -1,6 +1,4 @@
<template>
</template>
<template></template>
<script setup lang="ts">
import { useGameStore } from '@/stores/gameStore'

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