Compare commits

...

46 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
89 changed files with 1574 additions and 984 deletions

713
package-lock.json generated

File diff suppressed because it is too large Load Diff

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 600 KiB

After

Width:  |  Height:  |  Size: 600 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 600 KiB

After

Width:  |  Height:  |  Size: 600 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 599 KiB

After

Width:  |  Height:  |  Size: 599 KiB

View File

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

After

Width:  |  Height:  |  Size: 729 B

View File

@ -1,3 +1,3 @@
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.91481 9.08504C7.71955 9.2803 7.40297 9.2803 7.20771 9.08504L5.55709 7.43442C5.36183 7.23916 5.04524 7.23916 4.84998 7.43442L2.99578 9.28862C2.80052 9.48389 2.48393 9.48389 2.28867 9.28863L1.00416 8.00412C0.808899 7.80885 0.808899 7.49227 1.00416 7.29701L2.85837 5.4428C3.05363 5.24754 3.05363 4.93096 2.85837 4.7357L0.914865 2.7922C0.719603 2.59693 0.719603 2.28035 0.914865 2.08509L2.07053 0.929423C2.26579 0.734161 2.58238 0.734162 2.77764 0.929424L4.72114 2.87293C4.9164 3.06819 5.23298 3.06819 5.42825 2.87293L7.297 1.00417C7.49226 0.808906 7.80885 0.808906 8.00411 1.00417L9.28862 2.28868C9.48388 2.48394 9.48388 2.80052 9.28862 2.99578L7.41986 4.86454C7.2246 5.0598 7.2246 5.37639 7.41986 5.57165L9.07048 7.22227C9.26574 7.41753 9.26574 7.73411 9.07048 7.92937L7.91481 9.08504Z" fill="white"/> <path d="M7.91481 9.08504C7.71955 9.2803 7.40297 9.2803 7.20771 9.08504L5.55709 7.43442C5.36183 7.23916 5.04524 7.23916 4.84998 7.43442L2.99578 9.28862C2.80052 9.48389 2.48393 9.48389 2.28867 9.28863L1.00416 8.00412C0.808899 7.80885 0.808899 7.49227 1.00416 7.29701L2.85837 5.4428C3.05363 5.24754 3.05363 4.93096 2.85837 4.7357L0.914865 2.7922C0.719603 2.59693 0.719603 2.28035 0.914865 2.08509L2.07053 0.929423C2.26579 0.734161 2.58238 0.734162 2.77764 0.929424L4.72114 2.87293C4.9164 3.06819 5.23298 3.06819 5.42825 2.87293L7.297 1.00417C7.49226 0.808906 7.80885 0.808906 8.00411 1.00417L9.28862 2.28868C9.48388 2.48394 9.48388 2.80052 9.28862 2.99578L7.41986 4.86454C7.2246 5.0598 7.2246 5.37639 7.41986 5.57165L9.07048 7.22227C9.26574 7.41753 9.26574 7.73411 9.07048 7.92937L7.91481 9.08504Z" fill="white"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 915 B

After

Width:  |  Height:  |  Size: 918 B

View File

Before

Width:  |  Height:  |  Size: 768 B

After

Width:  |  Height:  |  Size: 768 B

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 375 B

After

Width:  |  Height:  |  Size: 375 B

View File

Before

Width:  |  Height:  |  Size: 652 B

After

Width:  |  Height:  |  Size: 652 B

View File

Before

Width:  |  Height:  |  Size: 346 B

After

Width:  |  Height:  |  Size: 346 B

View File

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

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

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

After

Width:  |  Height:  |  Size: 490 B

View File

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

After

Width:  |  Height:  |  Size: 340 B

View File

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

Before

Width:  |  Height:  |  Size: 470 KiB

After

Width:  |  Height:  |  Size: 470 KiB

View File

Before

Width:  |  Height:  |  Size: 471 KiB

After

Width:  |  Height:  |  Size: 471 KiB

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 470 KiB

After

Width:  |  Height:  |  Size: 470 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 301 KiB

After

Width:  |  Height:  |  Size: 301 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 400 KiB

After

Width:  |  Height:  |  Size: 400 KiB

View File

@ -1,6 +1,6 @@
<template> <template>
<Notifications /> <Notifications />
<GmTools v-if="gameStore.character?.role === 'gm'" /> <BackgroundImageLoader />
<GmPanel v-if="gameStore.character?.role === 'gm'" /> <GmPanel v-if="gameStore.character?.role === 'gm'" />
<component :is="currentScreen" /> <component :is="currentScreen" />
</template> </template>
@ -9,7 +9,7 @@
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore' import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import Notifications from '@/components/utilities/Notifications.vue' import Notifications from '@/components/utilities/Notifications.vue'
import GmTools from '@/components/gameMaster/GmTools.vue' import BackgroundImageLoader from '@/components/utilities/BackgroundImageLoader.vue'
import GmPanel from '@/components/gameMaster/GmPanel.vue' import GmPanel from '@/components/gameMaster/GmPanel.vue'
import Login from '@/components/screens/Login.vue' import Login from '@/components/screens/Login.vue'
import Characters from '@/components/screens/Characters.vue' import Characters from '@/components/screens/Characters.vue'
@ -44,4 +44,16 @@ addEventListener('click', (event) => {
const audio = new Audio('/assets/music/click-btn.mp3') const audio = new Audio('/assets/music/click-btn.mp3')
audio.play() 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> </script>

View File

@ -23,6 +23,14 @@ body {
// Disable pinch zoom // Disable pinch zoom
touch-action: pan-x pan-y; 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, h1,
@ -58,13 +66,20 @@ input {
appearance: textfield; appearance: textfield;
} }
&[type='number']::-webkit-inner-spin-button, &[type='number']::-webkit-inner-spin-button,
&[type='number']::-webkit-outer-spin-button { &[type='number']::-webkit-outer-spin-button,
&[type='radio']{
-webkit-appearance: none; -webkit-appearance: none;
} }
} }
.input-field { .input-field {
@apply px-4 py-2.5 text-base leading-5 focus-visible:outline-none bg-gray border border-solid border-gray-500 rounded text-gray-300; @apply px-4 py-2.5 text-base leading-5 bg-gray border border-solid border-gray-500 rounded text-gray-300;
&:focus-visible {
@apply outline-none border-cyan rounded bg-gray-900;
}
&::placeholder {
@apply focus-visible:text-gray-300/50;
}
&.inactive { &.inactive {
@apply bg-gray-600/50 hover:cursor-not-allowed; @apply bg-gray-600/50 hover:cursor-not-allowed;
&::placeholder { &::placeholder {
@ -111,6 +126,7 @@ button {
@apply text-gray-50 border-2 border-solid border-gray-500 text-base leading-5 rounded py-2.5; @apply text-gray-50 border-2 border-solid border-gray-500 text-base leading-5 rounded py-2.5;
&.active, &.active,
&.selected,
&:hover { &:hover {
@apply bg-gray-700 border-gray-700; @apply bg-gray-700 border-gray-700;
} }
@ -123,6 +139,16 @@ button {
&.eye-open { &.eye-open {
@apply bg-[url('/assets/icons/eye-closed.svg')] w-5 h-4 right-2.5; @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 { .text-pixel {
@ -133,6 +159,15 @@ button {
@apply hidden; @apply hidden;
} }
.scrollbar {
&::-webkit-scrollbar {
@apply block w-0.5 bg-gray-300 rounded-sm;
}
&::-webkit-scrollbar-thumb {
@apply bg-gray-175;
}
}
canvas { canvas {
image-rendering: -moz-crisp-edges; image-rendering: -moz-crisp-edges;
image-rendering: -webkit-crisp-edges; image-rendering: -webkit-crisp-edges;

View File

@ -1,21 +1,41 @@
<template> <template>
<Scene name="effects" @preload="preloadScene" @create="createScene" @update="updateScene"> </Scene> <Scene name="effects" @preload="preloadScene" @create="createScene" @update="updateScene" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Scene } from 'phavuer' import { Scene } from 'phavuer'
import { useZoneStore } from '@/stores/zoneStore' import { useZoneStore } from '@/stores/zoneStore'
import { onBeforeUnmount, ref, watch } from 'vue' import { useGameStore } from '@/stores/gameStore'
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
import type { WeatherState } from '@/types'
// Constants
const SUNRISE_HOUR = 6
const SUNSET_HOUR = 20
const DAY_STRENGTH = 100
const NIGHT_STRENGTH = 30
// Stores
const gameStore = useGameStore()
const zoneStore = useZoneStore() const zoneStore = useZoneStore()
// Scene ref
const sceneRef = ref<Phaser.Scene | null>(null) const sceneRef = ref<Phaser.Scene | null>(null)
// Effect-related refs // Effect refs
const lightEffect = ref<Phaser.GameObjects.Graphics | null>(null) const lightEffect = ref<Phaser.GameObjects.Graphics | null>(null)
const rainEmitter = ref<Phaser.GameObjects.Particles.ParticleEmitter | null>(null) const rainEmitter = ref<Phaser.GameObjects.Particles.ParticleEmitter | null>(null)
const fogSprite = ref<Phaser.GameObjects.Sprite | null>(null) const fogSprite = ref<Phaser.GameObjects.Sprite | null>(null)
// State refs
const weatherState = ref<WeatherState>({
isRainEnabled: false,
rainPercentage: 0,
isFogEnabled: false,
fogDensity: 0
})
// Scene lifecycle methods
const preloadScene = async (scene: Phaser.Scene) => { const preloadScene = async (scene: Phaser.Scene) => {
scene.load.image('raindrop', 'assets/raindrop.png') scene.load.image('raindrop', 'assets/raindrop.png')
scene.load.image('fog', 'assets/fog.png') scene.load.image('fog', 'assets/fog.png')
@ -23,15 +43,21 @@ const preloadScene = async (scene: Phaser.Scene) => {
const createScene = async (scene: Phaser.Scene) => { const createScene = async (scene: Phaser.Scene) => {
sceneRef.value = scene sceneRef.value = scene
createLightEffect(scene) setupEffects(scene)
createRainEffect(scene) setupSocketListeners()
createFogEffect(scene)
} }
const updateScene = () => { const updateScene = () => {
updateEffects() updateEffects()
} }
// Effect setup
const setupEffects = (scene: Phaser.Scene) => {
createLightEffect(scene)
createRainEffect(scene)
createFogEffect(scene)
}
const createLightEffect = (scene: Phaser.Scene) => { const createLightEffect = (scene: Phaser.Scene) => {
lightEffect.value = scene.add.graphics() lightEffect.value = scene.add.graphics()
lightEffect.value.setDepth(1000) lightEffect.value.setDepth(1000)
@ -59,14 +85,55 @@ const createFogEffect = (scene: Phaser.Scene) => {
fogSprite.value.setDepth(950) fogSprite.value.setDepth(950)
} }
// Lighting calculations
const calculateLightStrength = (time: Date): number => {
const hour = time.getHours()
const minute = time.getMinutes()
let strength = DAY_STRENGTH
// Night time (10 PM - 6 AM)
if (hour >= SUNSET_HOUR || hour < SUNRISE_HOUR) {
strength = NIGHT_STRENGTH
}
// Full daylight (7 AM - 7 PM)
else if (hour > SUNRISE_HOUR && hour < SUNSET_HOUR - 2) {
strength = DAY_STRENGTH
}
// Sunrise transition (6 AM - 7 AM)
else if (hour === SUNRISE_HOUR) {
strength = NIGHT_STRENGTH + ((DAY_STRENGTH - NIGHT_STRENGTH) * minute) / 60
}
// Sunset transition (8 PM - 10 PM)
else if (hour >= SUNSET_HOUR - 2 && hour < SUNSET_HOUR) {
const totalMinutes = (hour - (SUNSET_HOUR - 2)) * 60 + minute
const transitionProgress = totalMinutes / 120 // 2 hours = 120 minutes
strength = DAY_STRENGTH - (DAY_STRENGTH - NIGHT_STRENGTH) * transitionProgress
}
return strength
}
// Effect updates
const updateEffects = () => { const updateEffects = () => {
const effects = zoneStore.zone?.zoneEffects || [] const effects = zoneStore.zone?.zoneEffects || []
if (effects.length > 0) {
updateZoneEffects(effects)
} else {
// Make sure we're getting the current time
const lightStrength = calculateLightStrength(gameStore.world.date)
updateLightEffect(lightStrength)
updateWeatherEffects()
}
}
const updateZoneEffects = (effects: any[]) => {
// Always update light based on time when zone effects are present
updateLightEffect(calculateLightStrength(gameStore.world.date))
effects.forEach((effect) => { effects.forEach((effect) => {
switch (effect.effect) { switch (effect.effect) {
case 'light':
updateLightEffect(effect.strength)
break
case 'rain': case 'rain':
updateRainEffect(effect.strength) updateRainEffect(effect.strength)
break break
@ -77,6 +144,11 @@ const updateEffects = () => {
}) })
} }
const updateWeatherEffects = () => {
updateRainEffect(weatherState.value.isRainEnabled ? weatherState.value.rainPercentage : 0)
updateFogEffect(weatherState.value.isFogEnabled ? weatherState.value.fogDensity * 100 : 0)
}
const updateLightEffect = (strength: number) => { const updateLightEffect = (strength: number) => {
if (!lightEffect.value) return if (!lightEffect.value) return
const darkness = 1 - strength / 100 const darkness = 1 - strength / 100
@ -100,11 +172,47 @@ const updateFogEffect = (strength: number) => {
fogSprite.value.setAlpha(strength / 100) 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 }) watch(() => zoneStore.zone?.zoneEffects, updateEffects, { deep: true })
onBeforeUnmount(() => { onMounted(() => {
if (sceneRef.value) sceneRef.value.scene.remove('effects') window.addEventListener('resize', updateEffectWindowSize)
}) })
// @TODO : Fix resize issue // Cleanup
onBeforeUnmount(() => {
window.removeEventListener('resize', updateEffectWindowSize)
if (sceneRef.value) sceneRef.value.scene.remove('effects')
gameStore.connection?.off('weather')
})
</script> </script>

View File

@ -1,11 +1,12 @@
<template> <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> <template #modalHeader>
<div class="flex gap-1.5 flex-wrap"> <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">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">Users</button>
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Chats</button> <button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Chats</button>
<button @mousedown.stop class="btn-cyan active py-1.5 px-4 min-w-24">Asset manager</button> <button @mousedown.stop class="btn-cyan active py-1.5 px-4 min-w-24">Asset manager</button>
<button class="btn-cyan py-1.5 px-4 min-w-24" type="button" @click="() => zoneEditorStore.toggleActive()">Zone manager</button>
</div> </div>
</template> </template>
<template #modalBody> <template #modalBody>
@ -21,8 +22,10 @@ import { ref } from 'vue'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import AssetManager from '@/components/gameMaster/assetManager/AssetManager.vue' import AssetManager from '@/components/gameMaster/assetManager/AssetManager.vue'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
const gameStore = useGameStore() const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore()
let toggle = ref('asset-manager') let toggle = ref('asset-manager')
</script> </script>

View File

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

View File

@ -55,6 +55,7 @@
<ObjectList v-if="selectedCategory === 'objects'" /> <ObjectList v-if="selectedCategory === 'objects'" />
<SpriteList v-if="selectedCategory === 'sprites'" /> <SpriteList v-if="selectedCategory === 'sprites'" />
<CharacterTypeList v-if="selectedCategory === 'characterTypes'" /> <CharacterTypeList v-if="selectedCategory === 'characterTypes'" />
<CharacterHairList v-if="selectedCategory === 'characterHair'" />
</div> </div>
<div class="absolute w-px bg-gray-500 h-full top-0 left-1/2"></div> <div class="absolute w-px bg-gray-500 h-full top-0 left-1/2"></div>
@ -65,6 +66,7 @@
<ObjectDetails v-if="selectedCategory === 'objects' && assetManagerStore.selectedObject" /> <ObjectDetails v-if="selectedCategory === 'objects' && assetManagerStore.selectedObject" />
<SpriteDetails v-if="selectedCategory === 'sprites' && assetManagerStore.selectedSprite" /> <SpriteDetails v-if="selectedCategory === 'sprites' && assetManagerStore.selectedSprite" />
<CharacterTypeDetails v-if="selectedCategory === 'characterTypes' && assetManagerStore.selectedCharacterType" /> <CharacterTypeDetails v-if="selectedCategory === 'characterTypes' && assetManagerStore.selectedCharacterType" />
<CharacterHairDetails v-if="selectedCategory === 'characterHair' && assetManagerStore.selectedCharacterHair" />
</div> </div>
</div> </div>
</template> </template>
@ -80,6 +82,8 @@ import SpriteList from '@/components/gameMaster/assetManager/partials/sprite/Spr
import SpriteDetails from '@/components/gameMaster/assetManager/partials/sprite/SpriteDetails.vue' import SpriteDetails from '@/components/gameMaster/assetManager/partials/sprite/SpriteDetails.vue'
import CharacterTypeList from '@/components/gameMaster/assetManager/partials/characterType/CharacterTypeList.vue' import CharacterTypeList from '@/components/gameMaster/assetManager/partials/characterType/CharacterTypeList.vue'
import CharacterTypeDetails from '@/components/gameMaster/assetManager/partials/characterType/CharacterTypeDetails.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 assetManagerStore = useAssetManagerStore()
const selectedCategory = ref('tiles') 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

@ -19,8 +19,18 @@
</select> </select>
</div> </div>
<div class="form-field-full"> <div class="form-field-full">
<label for="spriteId">Sprite ID</label> <label for="isEnabledForCharCreation">Is enabled for character creation</label>
<input v-model="characterSpriteId" class="input-field" type="text" name="spriteId" placeholder="Sprite ID" /> <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> </div>
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button> <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> <button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeCharacterType">Remove</button>
@ -30,7 +40,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { CharacterType, CharacterGender, CharacterRace } from '@/types' import type { CharacterType, CharacterGender, CharacterRace, Sprite } from '@/types'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useAssetManagerStore } from '@/stores/assetManagerStore' import { useAssetManagerStore } from '@/stores/assetManagerStore'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
@ -43,7 +53,8 @@ const selectedCharacterType = computed(() => assetManagerStore.selectedCharacter
const characterName = ref('') const characterName = ref('')
const characterGender = ref<CharacterGender>('MALE' as CharacterGender.MALE) const characterGender = ref<CharacterGender>('MALE' as CharacterGender.MALE)
const characterRace = ref<CharacterRace>('HUMAN' as CharacterRace.HUMAN) const characterRace = ref<CharacterRace>('HUMAN' as CharacterRace.HUMAN)
const characterSpriteId = ref('') const characterIsEnabledForCharCreation = ref<boolean>(false)
const characterSpriteId = ref<string | null | undefined>(null)
const genderOptions: CharacterGender[] = ['MALE' as CharacterGender.MALE, 'FEMALE' as CharacterGender.FEMALE] 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] const raceOptions: CharacterRace[] = ['HUMAN' as CharacterRace.HUMAN, 'ELF' as CharacterRace.ELF, 'DWARF' as CharacterRace.DWARF, 'ORC' as CharacterRace.ORC, 'GOBLIN' as CharacterRace.GOBLIN]
@ -56,6 +67,7 @@ if (selectedCharacterType.value) {
characterName.value = selectedCharacterType.value.name characterName.value = selectedCharacterType.value.name
characterGender.value = selectedCharacterType.value.gender characterGender.value = selectedCharacterType.value.gender
characterRace.value = selectedCharacterType.value.race characterRace.value = selectedCharacterType.value.race
characterIsEnabledForCharCreation.value = selectedCharacterType.value.isEnabledForCharCreation
characterSpriteId.value = selectedCharacterType.value.spriteId characterSpriteId.value = selectedCharacterType.value.spriteId
} }
@ -83,10 +95,11 @@ function refreshCharacterTypeList(unsetSelectedCharacterType = true) {
function saveCharacterType() { function saveCharacterType() {
const characterTypeData = { const characterTypeData = {
id: selectedCharacterType.value?.id, id: selectedCharacterType.value!.id,
name: characterName.value, name: characterName.value,
gender: characterGender.value, gender: characterGender.value,
race: characterRace.value, race: characterRace.value,
isEnabledForCharCreation: characterIsEnabledForCharCreation.value,
spriteId: characterSpriteId.value spriteId: characterSpriteId.value
} }
@ -104,11 +117,16 @@ watch(selectedCharacterType, (characterType: CharacterType | null) => {
characterName.value = characterType.name characterName.value = characterType.name
characterGender.value = characterType.gender characterGender.value = characterType.gender
characterRace.value = characterType.race characterRace.value = characterType.race
characterIsEnabledForCharCreation.value = characterType.isEnabledForCharCreation
characterSpriteId.value = characterType.spriteId characterSpriteId.value = characterType.spriteId
}) })
onMounted(() => { onMounted(() => {
if (!selectedCharacterType.value) return if (!selectedCharacterType.value) return
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
assetManagerStore.setSpriteList(response)
})
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {

View File

@ -14,7 +14,7 @@
<div v-bind="wrapperProps" ref="elementToScroll"> <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)"> <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"> <div class="flex items-center gap-2.5">
<span>{{ characterType.name }}</span> <span :class="{ 'text-white': assetManagerStore.selectedCharacterType?.id === characterType.id }">{{ characterType.name }}</span>
</div> </div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div> <div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a> </a>

View File

@ -16,7 +16,7 @@
<div class="h-7 w-16 max-w-16 flex justify-center"> <div class="h-7 w-16 max-w-16 flex justify-center">
<img class="h-7" :src="`${config.server_endpoint}/assets/objects/${object.id}.png`" alt="Object" /> <img class="h-7" :src="`${config.server_endpoint}/assets/objects/${object.id}.png`" alt="Object" />
</div> </div>
<span>{{ object.name }}</span> <span :class="{ 'text-white': assetManagerStore.selectedObject?.id === object.id }">{{ object.name }}</span>
</div> </div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div> <div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a> </a>

View File

@ -12,7 +12,7 @@
<div v-bind="wrapperProps" ref="elementToScroll"> <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)"> <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"> <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>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div> <div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a> </a>

View File

@ -16,7 +16,7 @@
<div class="h-7 w-16 max-w-16 flex justify-center"> <div class="h-7 w-16 max-w-16 flex justify-center">
<img class="h-7" :src="`${config.server_endpoint}/assets/tiles/${tile.id}.png`" alt="Tile" /> <img class="h-7" :src="`${config.server_endpoint}/assets/tiles/${tile.id}.png`" alt="Tile" />
</div> </div>
<span>{{ tile.name }}</span> <span :class="{ 'text-white': assetManagerStore.selectedTile?.id === tile.id }">{{ tile.name }}</span>
</div> </div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div> <div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
</a> </a>

View File

@ -1,9 +1,9 @@
<template> <template>
<ZoneTiles @tilemap:create="tileMap = $event" /> <ZoneTiles @tileMap:create="tileMap = $event" />
<ZoneObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" /> <ZoneObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
<ZoneEventTiles v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" /> <ZoneEventTiles v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
<Toolbar @save="save" /> <Toolbar @save="save" @clear="clear" />
<ZoneList /> <ZoneList />
<TileList /> <TileList />
@ -18,7 +18,6 @@ import { onUnmounted, ref } from 'vue'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useZoneEditorStore } from '@/stores/zoneEditorStore' import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { type Zone } from '@/types' import { type Zone } from '@/types'
// Components // Components
import Toolbar from '@/components/gameMaster/zoneEditor/partials/Toolbar.vue' import Toolbar from '@/components/gameMaster/zoneEditor/partials/Toolbar.vue'
import TileList from '@/components/gameMaster/zoneEditor/partials/TileList.vue' import TileList from '@/components/gameMaster/zoneEditor/partials/TileList.vue'
@ -26,14 +25,24 @@ import ObjectList from '@/components/gameMaster/zoneEditor/partials/ObjectList.v
import ZoneSettings from '@/components/gameMaster/zoneEditor/partials/ZoneSettings.vue' import ZoneSettings from '@/components/gameMaster/zoneEditor/partials/ZoneSettings.vue'
import ZoneList from '@/components/gameMaster/zoneEditor/partials/ZoneList.vue' import ZoneList from '@/components/gameMaster/zoneEditor/partials/ZoneList.vue'
import TeleportModal from '@/components/gameMaster/zoneEditor/partials/TeleportModal.vue' import TeleportModal from '@/components/gameMaster/zoneEditor/partials/TeleportModal.vue'
import ZoneTiles from '@/components/gameMaster/zoneEditor/ZoneTiles.vue' import ZoneTiles from '@/components/gameMaster/zoneEditor/zonePartials/ZoneTiles.vue'
import ZoneObjects from '@/components/gameMaster/zoneEditor/ZoneObjects.vue' import ZoneObjects from '@/components/gameMaster/zoneEditor/zonePartials/ZoneObjects.vue'
import ZoneEventTiles from '@/components/gameMaster/zoneEditor/ZoneEventTiles.vue' import ZoneEventTiles from '@/components/gameMaster/zoneEditor/zonePartials/ZoneEventTiles.vue'
const gameStore = useGameStore() const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore() const zoneEditorStore = useZoneEditorStore()
const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null) const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null)
function clear() {
if (!zoneEditorStore.zone) return
// Clear objects, event tiles and tiles
zoneEditorStore.zone.zoneObjects = []
zoneEditorStore.zone.zoneEventTiles = []
zoneEditorStore.triggerClearTiles()
}
function save() { function save() {
if (!zoneEditorStore.zone) return if (!zoneEditorStore.zone) return

View File

@ -1,5 +1,5 @@
<template> <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> <template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">Create new zone</h3> <h3 class="m-0 font-medium shrink-0 text-white">Create new zone</h3>
</template> </template>

View File

@ -1,5 +1,5 @@
<template> <template>
<Modal :isModalOpen="zoneEditorStore.isObjectListModalShown" :modal-width="645" :modal-height="260" @modal:close="() => (zoneEditorStore.isObjectListModalShown = false)"> <Modal :isModalOpen="zoneEditorStore.isObjectListModalShown" :modal-width="645" :modal-height="260" @modal:close="() => (zoneEditorStore.isObjectListModalShown = false)" :disable-bg-texture="true">
<template #modalHeader> <template #modalHeader>
<h3 class="text-lg text-white">Objects</h3> <h3 class="text-lg text-white">Objects</h3>
</template> </template>

View File

@ -1,5 +1,5 @@
<template> <template>
<Modal :is-modal-open="showTeleportModal" @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> <template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">Teleport settings</h3> <h3 class="m-0 font-medium shrink-0 text-white">Teleport settings</h3>
</template> </template>

View File

@ -1,5 +1,5 @@
<template> <template>
<Modal :isModalOpen="zoneEditorStore.isTileListModalShown" :modal-width="645" :modal-height="600" @modal:close="() => (zoneEditorStore.isTileListModalShown = false)"> <Modal :isModalOpen="zoneEditorStore.isTileListModalShown" :modal-width="645" :modal-height="600" @modal:close="() => (zoneEditorStore.isTileListModalShown = false)" :disable-bg-texture="true">
<template #modalHeader> <template #modalHeader>
<h3 class="text-lg text-white">Tiles</h3> <h3 class="text-lg text-white">Tiles</h3>
</template> </template>
@ -223,7 +223,7 @@ function selectTile(tile: string) {
} }
function isActiveTile(tile: Tile): boolean { function isActiveTile(tile: Tile): boolean {
return zoneEditorStore.selectedTile?.id === tile.id return zoneEditorStore.selectedTile === tile.id
} }
onMounted(async () => { onMounted(async () => {

View File

@ -1,6 +1,6 @@
<template> <template>
<CreateZone v-if="zoneEditorStore.isCreateZoneModalShown" /> <CreateZone v-if="zoneEditorStore.isCreateZoneModalShown" />
<Modal :is-modal-open="zoneEditorStore.isZoneListModalShown" @modal:close="() => zoneEditorStore.toggleZoneListModal()" :is-resizable="false" :modal-width="300" :modal-height="360"> <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> <template #modalHeader>
<h3 class="text-lg text-white">Zones</h3> <h3 class="text-lg text-white">Zones</h3>
</template> </template>
@ -15,7 +15,7 @@
<div class="flex gap-3 items-center w-full" @click="() => loadZone(zone.id)"> <div class="flex gap-3 items-center w-full" @click="() => loadZone(zone.id)">
<span>{{ zone.name }}</span> <span>{{ zone.name }}</span>
<span class="ml-auto gap-1 flex"> <span class="ml-auto gap-1 flex">
<button class="btn-red w-11 h-11 z-50" @click.stop="() => deleteZone(zone.id)">X</button> <button class="btn-red w-7 h-7 z-50 flex items-center justify-center" @click.stop="() => deleteZone(zone.id)">x</button>
</span> </span>
</div> </div>
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div> <div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>

View File

@ -1,5 +1,5 @@
<template> <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> <template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">Zone settings</h3> <h3 class="m-0 font-medium shrink-0 text-white">Zone settings</h3>
</template> </template>

View File

@ -56,7 +56,7 @@ function pencil(pointer: Phaser.Input.Pointer) {
id: uuidv4(), id: uuidv4(),
zoneId: zoneEditorStore.zone.id, zoneId: zoneEditorStore.zone.id,
zone: zoneEditorStore.zone, zone: zoneEditorStore.zone,
objectId: zoneEditorStore.selectedObject, objectId: zoneEditorStore.selectedObject.id,
object: zoneEditorStore.selectedObject, object: zoneEditorStore.selectedObject,
depth: 0, depth: 0,
isRotated: false, isRotated: false,
@ -65,7 +65,7 @@ function pencil(pointer: Phaser.Input.Pointer) {
} }
// Add new object to zoneObjects // Add new object to zoneObjects
zoneEditorStore.zone.zoneObjects = zoneEditorStore.zone.zoneObjects.concat(newObject as ZoneObject) zoneEditorStore.zone.zoneObjects = zoneEditorStore.zone.zoneObjects.concat(newObject as ZoneObjectT)
} }
function eraser(pointer: Phaser.Input.Pointer) { function eraser(pointer: Phaser.Input.Pointer) {
@ -134,7 +134,7 @@ function moveZoneObject(id: string) {
// Check if zone is set // Check if zone is set
if (!zoneEditorStore.zone) return if (!zoneEditorStore.zone) return
movingZoneObject.value = zoneEditorStore.zone.zoneObjects.find((object) => object.id === id) as ZoneObject movingZoneObject.value = zoneEditorStore.zone.zoneObjects.find((object) => object.id === id) as ZoneObjectT
function handlePointerMove(pointer: Phaser.Input.Pointer) { function handlePointerMove(pointer: Phaser.Input.Pointer) {
if (!movingZoneObject.value) return if (!movingZoneObject.value) return

View File

@ -1,31 +1,31 @@
<template> <template>
<Controls :layer="tiles" :depth="0" /> <Controls :layer="tileLayer" :depth="0" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import config from '@/config' import config from '@/config'
import { useScene } from 'phavuer' import { useScene } from 'phavuer'
import { useZoneEditorStore } from '@/stores/zoneEditorStore' import { useZoneEditorStore } from '@/stores/zoneEditorStore'
import { onMounted, onUnmounted } from 'vue' import { onMounted, onUnmounted, watch } from 'vue'
import { createTileArray, getTile, placeTile, setLayerTiles } from '@/composables/zoneComposable' import { createTileArray, getTile, placeTile, setLayerTiles } from '@/composables/zoneComposable'
import Controls from '@/components/utilities/Controls.vue' import Controls from '@/components/utilities/Controls.vue'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import type { AssetDataT } from '@/types' import type { AssetDataT } from '@/types'
const emit = defineEmits(['tilemap:create']) const emit = defineEmits(['tileMap:create'])
const scene = useScene() const scene = useScene()
const gameStore = useGameStore() const gameStore = useGameStore()
const zoneEditorStore = useZoneEditorStore() const zoneEditorStore = useZoneEditorStore()
const zoneTilemap = createTilemap() const tileMap = createTileMap()
const tiles = createTileLayer() const tileLayer = createTileLayer()
/** /**
* A Tilemap is a container for Tilemap data. * 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. * 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. * A map can have one or more tilemap layers, which are the display objects that actually render the tiles.
*/ */
function createTilemap() { function createTileMap() {
const zoneData = new Phaser.Tilemaps.MapData({ const zoneData = new Phaser.Tilemaps.MapData({
width: zoneEditorStore.zone?.width, width: zoneEditorStore.zone?.width,
height: zoneEditorStore.zone?.height, height: zoneEditorStore.zone?.height,
@ -34,9 +34,11 @@ function createTilemap() {
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC, orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
format: Phaser.Tilemaps.Formats.ARRAY_2D format: Phaser.Tilemaps.Formats.ARRAY_2D
}) })
const tilemap = new Phaser.Tilemaps.Tilemap(scene, zoneData)
emit('tilemap:create', tilemap) const newTileMap = new Phaser.Tilemaps.Tilemap(scene, zoneData)
return tilemap emit('tileMap:create', newTileMap)
return newTileMap
} }
/** /**
@ -46,12 +48,12 @@ function createTileLayer() {
const tilesArray = gameStore.getLoadedAssetsByGroup('tiles') const tilesArray = gameStore.getLoadedAssetsByGroup('tiles')
const tilesetImages = Array.from(tilesArray).map((tile: AssetDataT, index: number) => { const tilesetImages = Array.from(tilesArray).map((tile: AssetDataT, index: number) => {
return zoneTilemap.addTilesetImage(tile.key, tile.key, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y }) 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 }) as any
// Add blank tile // Add blank tile
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 })) 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 = zoneTilemap.createBlankLayer('tiles', tilesetImages, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer const layer = tileMap.createBlankLayer('tiles', tilesetImages, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
layer.setDepth(0) layer.setDepth(0)
layer.setCullPadding(2, 2) layer.setCullPadding(2, 2)
@ -79,11 +81,11 @@ function pencil(pointer: Phaser.Input.Pointer) {
if (pointer.event.shiftKey) return if (pointer.event.shiftKey) return
// Check if there is a tile // Check if there is a tile
const tile = getTile(tiles, pointer.worldX, pointer.worldY) const tile = getTile(tileLayer, pointer.worldX, pointer.worldY)
if (!tile) return if (!tile) return
// Place tile // Place tile
placeTile(zoneTilemap, tiles, tile.x, tile.y, zoneEditorStore.selectedTile) placeTile(tileMap, tileLayer, tile.x, tile.y, zoneEditorStore.selectedTile)
// Adjust zoneEditorStore.zone.tiles // Adjust zoneEditorStore.zone.tiles
zoneEditorStore.zone.tiles[tile.y][tile.x] = zoneEditorStore.selectedTile zoneEditorStore.zone.tiles[tile.y][tile.x] = zoneEditorStore.selectedTile
@ -109,11 +111,11 @@ function eraser(pointer: Phaser.Input.Pointer) {
if (pointer.event.altKey) return if (pointer.event.altKey) return
// Check if there is a tile // Check if there is a tile
const tile = getTile(tiles, pointer.worldX, pointer.worldY) const tile = getTile(tileLayer, pointer.worldX, pointer.worldY)
if (!tile) return if (!tile) return
// Place tile // Place tile
placeTile(zoneTilemap, tiles, tile.x, tile.y, 'blank_tile') placeTile(tileMap, tileLayer, tile.x, tile.y, 'blank_tile')
// Adjust zoneEditorStore.zone.tiles // Adjust zoneEditorStore.zone.tiles
zoneEditorStore.zone.tiles[tile.y][tile.x] = 'blank_tile' zoneEditorStore.zone.tiles[tile.y][tile.x] = 'blank_tile'
@ -139,10 +141,10 @@ function paint(pointer: Phaser.Input.Pointer) {
if (pointer.event.altKey) return if (pointer.event.altKey) return
// Set new tileArray with selected tile // Set new tileArray with selected tile
setLayerTiles(zoneTilemap, tiles, createTileArray(zoneTilemap.width, zoneTilemap.height, zoneEditorStore.selectedTile.id)) setLayerTiles(tileMap, tileLayer, createTileArray(tileMap.width, tileMap.height, zoneEditorStore.selectedTile))
// Adjust zoneEditorStore.zone.tiles // Adjust zoneEditorStore.zone.tiles
zoneEditorStore.zone.tiles = createTileArray(zoneTilemap.width, zoneTilemap.height, zoneEditorStore.selectedTile.id) 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 // When alt is pressed, and the pointer is down, select the tile that the pointer is over
@ -166,18 +168,45 @@ function tilePicker(pointer: Phaser.Input.Pointer) {
if (!pointer.event.altKey) return if (!pointer.event.altKey) return
// Check if there is a tile // Check if there is a tile
const tile = getTile(tiles, pointer.worldX, pointer.worldY) const tile = getTile(tileLayer, pointer.worldX, pointer.worldY)
if (!tile) return if (!tile) return
// Select the tile // Select the tile
zoneEditorStore.setSelectedTile(zoneEditorStore.zone.tiles[tile.y][tile.x]) 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(() => { onMounted(() => {
if (!zoneEditorStore.zone?.tiles) { if (!zoneEditorStore.zone?.tiles) {
return return
} }
setLayerTiles(zoneTilemap, tiles, zoneEditorStore.zone.tiles)
// 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, pencil)
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser) scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
@ -191,8 +220,8 @@ onUnmounted(() => {
scene.input.off(Phaser.Input.Events.POINTER_DOWN, paint) scene.input.off(Phaser.Input.Events.POINTER_DOWN, paint)
scene.input.off(Phaser.Input.Events.POINTER_DOWN, tilePicker) scene.input.off(Phaser.Input.Events.POINTER_DOWN, tilePicker)
zoneTilemap.destroyLayer('tiles') tileMap.destroyLayer('tiles')
zoneTilemap.removeAllLayers() tileMap.removeAllLayers()
zoneTilemap.destroy() tileMap.destroy()
}) })
</script> </script>

View File

@ -5,7 +5,7 @@
<div @mousedown="startDrag" class="cursor-move px-5 py-2.5 flex justify-between items-center relative"> <div @mousedown="startDrag" class="cursor-move px-5 py-2.5 flex justify-between items-center relative">
<span class="text-xs text-white font-thin">Character Profile [Alt+C]</span> <span class="text-xs text-white font-thin">Character Profile [Alt+C]</span>
<button @click="gameStore.uiSettings.isCharacterProfileOpen = false" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out"> <button @click="gameStore.uiSettings.isCharacterProfileOpen = false" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
<img alt="close" draggable="false" src="/assets/icons/close-button-white.svg" class="w-full h-full" /> <img alt="close" draggable="false" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" />
</button> </button>
</div> </div>
<div class="py-4 px-6 flex flex-col gap-7 relative z-10"> <div class="py-4 px-6 flex flex-col gap-7 relative z-10">
@ -15,7 +15,7 @@
<p class="text-sm m-0 font-bold text-white tracking-wide">{{ gameStore.character?.name }}</p> <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> <span class="text-xs">{{ gameStore.character?.experience }} / 18.600XP</span>
</div> </div>
<a class="hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-small.svg')] bg-no-repeat block w-8 h-8 relative mx-[3px]"> <a class="hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured-small.svg')] bg-no-repeat block w-8 h-8 relative mx-[3px]">
<img class="hover:drop-shadow-default w-3.5 h-3.5 m-[9px] object-contain" draggable="false" src="/assets/icons/plus-green-icon.svg" /> <img class="hover:drop-shadow-default w-3.5 h-3.5 m-[9px] object-contain" draggable="false" src="/assets/icons/plus-green-icon.svg" />
</a> </a>
</div> </div>

View File

@ -7,22 +7,24 @@
</div> </div>
</div> </div>
<div class="w-96 mx-auto relative"> <div class="w-96 mx-auto relative">
<img src="/assets/icons/chat-icon.svg" class="absolute top-1/2 -translate-y-1/2 left-2.5 h-4 w-4 opacity-50" /> <img src="/assets/icons/ingameUI/chat-icon.svg" class="absolute top-1/2 -translate-y-1/2 left-2.5 h-4 w-4 opacity-50" />
<input <input
class="w-[332px] h-8 rounded-sm text-xs font-default pl-8 pr-4 py-0 bg-gray-600 border-2 border-solid border-gray-500 text-gray-300 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover focus:outline-none focus:ring-0 focus:border-cyan-800" class="w-[332px] h-8 rounded-sm text-xs font-default pl-8 pr-4 py-0 bg-gray-600 border-2 border-solid border-gray-500 text-gray-300 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover focus:outline-none focus:ring-0 focus:border-cyan-800"
placeholder="Type something..." placeholder="Type something..."
v-model="message" v-model="message"
@keypress="handleKeyPress" @keypress="handleKeyPress"
@submit="handleSubmit" @submit="handleSubmit"
ref="chatInput"
/> />
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <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 { useGameStore } from '@/stores/gameStore'
import type { Character, ChatMessage } from '@/types' import type { Chat } from '@/types'
import { useZoneStore } from '@/stores/zoneStore' import { useZoneStore } from '@/stores/zoneStore'
import { useScene } from 'phavuer' import { useScene } from 'phavuer'
@ -31,12 +33,21 @@ const gameStore = useGameStore()
const zoneStore = useZoneStore() const zoneStore = useZoneStore()
const message = ref('') const message = ref('')
const chats = ref([] as ChatMessage[]) const chats = ref([] as Chat[])
const chatWindow = ref<HTMLElement | null>(null) 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 = () => { const sendMessage = () => {
if (!message.value.trim()) return 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 = '' 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) chats.value.push(data)
scrollToBottom() 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

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

View File

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

View File

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

View File

@ -6,11 +6,11 @@
<div class="absolute -bottom-3 left-1/2 -translate-x-1/2 flex gap-1"> <div class="absolute -bottom-3 left-1/2 -translate-x-1/2 flex gap-1">
<button class="w-6 h-6 relative p-0"> <button class="w-6 h-6 relative p-0">
<img class="absolute w-3 h-3 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/plus-icon.svg" /> <img class="absolute w-3 h-3 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/plus-icon.svg" />
<img class="w-full h-full" src="/assets/ui-elements/ui-border-4-corners.svg" /> <img class="w-full h-full" src="/assets/ui-elements/button-ui-box-textured.svg" />
</button> </button>
<button class="w-6 h-6 relative p-0"> <button class="w-6 h-6 relative p-0">
<img class="absolute w-3 h-3 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/minus-icon.svg" /> <img class="absolute w-3 h-3 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/minus-icon.svg" />
<img class="w-full h-full" src="/assets/ui-elements/ui-border-4-corners.svg" /> <img class="w-full h-full" src="/assets/ui-elements/button-ui-box-textured.svg" />
</button> </button>
</div> </div>
</div> </div>

View File

@ -11,7 +11,7 @@
</div> </div>
<div class="flex gap-2.5"> <div class="flex gap-2.5">
<button class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out"> <button class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
<img alt="close" draggable="false" src="/assets/icons/close-button-white.svg" class="w-full h-full" /> <img alt="close" draggable="false" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" />
</button> </button>
</div> </div>
<div class="flex sm:hidden gap-1.5 flex-wrap"> <div class="flex sm:hidden gap-1.5 flex-wrap">

View File

@ -5,7 +5,7 @@
<input class="input-field xs:min-w-[350px] min-w-64" id="username-login" v-model="username" type="text" name="username" placeholder="Username" required autofocus /> <input class="input-field xs:min-w-[350px] min-w-64" id="username-login" v-model="username" type="text" name="username" placeholder="Username" required autofocus />
<div class="relative"> <div class="relative">
<input class="input-field xs:min-w-[350px] min-w-64" id="password-login" v-model="password" :type="showPassword ? 'text' : 'password'" name="password" placeholder="Password" required /> <input class="input-field xs:min-w-[350px] min-w-64" id="password-login" v-model="password" :type="showPassword ? 'text' : 'password'" name="password" placeholder="Password" required />
<button type="button" @click.prevent="showPassword = !showPassword" :class="{ 'eye-open': showPassword }" class="bg-[url('/assets/icons/eye.svg')] p-0 absolute right-3 w-4 h-3 top-1/2 -translate-y-1/2 bg-no-repeat"></button> <button type="button" @click.prevent="showPassword = !showPassword" :class="{ 'eye-open': showPassword }" class="bg-[url('/assets/icons/eye.svg')] p-0 absolute right-3 w-5 h-4 top-1/2 -translate-y-1/2 bg-no-repeat bg-center"></button>
</div> </div>
<span v-if="loginError" class="text-red-200 text-xs absolute top-full mt-1">{{ loginError }}</span> <span v-if="loginError" class="text-red-200 text-xs absolute top-full mt-1">{{ loginError }}</span>
</div> </div>

View File

@ -59,10 +59,18 @@ async function newPasswordFunc() {
newPasswordError.value = response.error newPasswordError.value = response.error
return return
} }
window.location.href = '/'
gameStore.addNotification({
title: 'Success',
message: 'Password changed successfully'
})
window.history.replaceState(null, '', window.location.pathname)
emit('switchToLogin')
} }
function cancelNewPassword() { function cancelNewPassword() {
window.location.href = '/' window.history.replaceState(null, '', windowlocation.pathname)
emit('switchToLogin')
} }
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<Modal @modal:close="() => emit('close')" :modal-width="400" :modal-height="300" :is-resizable="false"> <Modal :isModalOpen="true" :modal-width="400" :modal-height="300" :is-resizable="false" @modal:close="() => emit('close')">
<template #modalHeader> <template #modalHeader>
<h3 class="m-0 font-medium shrink-0 text-white">Reset Password</h3> <h3 class="m-0 font-medium shrink-0 text-white">Reset Password</h3>
</template> </template>
@ -14,7 +14,13 @@
</div> </div>
<div class="grid grid-flow-col justify-stretch gap-4"> <div class="grid grid-flow-col justify-stretch gap-4">
<button class="btn-empty py-1.5 px-4 min-w-24 inline-block" @click.stop="() => emit('close')">Cancel</button> <button class="btn-empty py-1.5 px-4 min-w-24 inline-block" @click.stop="() => emit('close')">Cancel</button>
<button class="btn-cyan py-1.5 px-4 min-w-24 inline-block" type="submit">Send mail</button> <button class="btn-cyan py-1.5 px-4 min-w-24 inline-flex items-center justify-center" type="submit">
<svg v-if="isLoading" xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 animate-spin mr-2" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Send mail
</button>
</div> </div>
</form> </form>
</div> </div>
@ -23,17 +29,17 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue' import { ref } from 'vue'
import { resetPassword } from '@/services/authentication' import { resetPassword } from '@/services/authentication'
import { useGameStore } from '@/stores/gameStore'
import Modal from '@/components/utilities/Modal.vue' import Modal from '@/components/utilities/Modal.vue'
import { useGameStore } from '@/stores/gameStore'
const emit = defineEmits(['close']) const emit = defineEmits(['close'])
const gameStore = useGameStore() const gameStore = useGameStore()
const isLoading = ref(false)
const email = ref('') const email = ref('')
const resetPasswordError = ref('') const resetPasswordError = ref('')
const isPasswordResetOpen = ref(false)
async function resetPasswordFunc() { async function resetPasswordFunc() {
// check if email is valid // check if email is valid
@ -42,14 +48,24 @@ async function resetPasswordFunc() {
return return
} }
isLoading.value = true
// send reset password event to server // send reset password event to server
const response = await resetPassword(email.value) const response = await resetPassword(email.value)
if (response.success === undefined) { if (response.success === undefined) {
resetPasswordError.value = response.error resetPasswordError.value = response.error
isLoading.value = false
return return
} }
gameStore.addNotification({
title: 'Success',
message: 'Password reset email sent'
})
isLoading.value = false
emit('close') emit('close')
} }
</script> </script>

View File

@ -1,61 +1,93 @@
<template> <template>
<div class="bg-gray-900 relative"> <div class="relative max-lg:h-dvh flex flex-row-reverse">
<div class="absolute bg-[url('/assets/shapes/select-screen-bg-shape.svg')] bg-no-repeat bg-center w-full h-full"></div> <div class="lg:bg-gradient-to-l bg-gradient-to-b from-gray-900 to-transparent w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 z-10"></div>
<div class="ui-wrapper h-dvh flex flex-col justify-center items-center gap-20 px-10 sm:px-20"> <div class="bg-[url('/assets/login/login-bg.png')] opacity-20 w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 bg-no-repeat bg-cover bg-center grayscale"></div>
<div class="bg-gray-900 z-20 w-full lg:w-1/2 h-[65dvh] lg:h-dvh relative"></div>
<div class="absolute top-8 right-0 py-[18px] pr-[15px] pl-32 bg-gradient-to-r from-transparent to-cyan-900 z-20">
<h2 class="text-white">CHARACTER SELECTION</h2>
</div>
<div class="ui-wrapper h-dvh w-[calc(100%_-_80px)] sm:w-[calc(100%_-_160px)] absolute flex flex-col justify-center items-center gap-14 px-10 sm:px-20 z-30">
<div class="filler"></div> <div class="filler"></div>
<div class="flex gap-14 w-full max-h-[650px] overflow-x-auto" v-if="!isLoading"> <div class="w-2/3 max-w-[860px]" v-if="!isLoading">
<!-- CHARACTER LIST --> <div class="mb-5 flex flex-col gap-1">
<div v-for="character in characters" :key="character.id" class="group first:ml-auto last:mr-auto m-4 w-[170px] h-[275px] flex flex-col shrink-0 relative shadow-character" :class="{ active: selected_character == character.id }"> <h1 class="text-white font-bold">SELECT CHARACTER TO PLAY</h1>
<img src="/assets/ui-elements/ui-box-outer.svg" class="absolute w-full h-full" alt="UI box outer" /> <p class="m-0">Maximum of 4 characters can be created per player</p>
<img src="/assets/ui-elements/ui-box-inner.svg" class="absolute left-2 bottom-2 w-[calc(100%_-_16px)] h-[calc(100%_-_40px)]" alt="UI box inner" />
<input class="opacity-0 h-full w-full absolute m-0 z-10" type="radio" :id="character.id" name="character" :value="character.id" v-model="selected_character" />
<label class="font-bold absolute left-1/2 top-4 max-w-32 -translate-x-1/2 -translate-y-1/2 text-center text-ellipsis overflow-hidden whitespace-nowrap drop-shadow-text" :for="character.id">{{ character.name }}</label>
<button
class="delete bg-red w-8 h-8 p-[3px] rounded-full absolute -right-4 top-0 -translate-y-1/2 z-10 border-2 border-solid border-white hover:bg-red-300"
@click="
() => {
deletingCharacter = character
}
"
>
<img draggable="false" src="/assets/icons/trashcan.svg" />
</button>
<div class="sprite-container flex flex-col items-center m-auto">
<img class="drop-shadow-20" draggable="false" src="/assets/avatar/default/0.png" />
</div>
<span class="absolute bottom-6 w-full text-center translate-y-1/2 z-10">Lvl. {{ character.level }}</span>
<div class="selected-character group-[.active]:max-w-[170px] absolute max-w-0 w-4/6 h-[3px] bg-gray-500 rounded-[3px] left-1/2 -bottom-4 -translate-x-1/2 transition-all ease-in-out duration-300"></div>
</div> </div>
<div class="flex w-full h-[400px] border border-solid border-gray-500 rounded-md rounded-tl-none bg-gray">
<div class="character new-character first:ml-auto mr-auto m-4 w-[170px] h-[275px] flex flex-col shrink-0 rounded-2xl relative bg-gray-500/50 bg-no-repeat shadow-character" v-if="characters.length < 4"> <div class="w-1/3 h-full bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center border-0 border-r border-solid border-gray-500 rounded-bl-md relative">
<button class="h-full w-full py-10 flex flex-col justify-between" @click="isModalOpen = true"> <div class="absolute right-full -top-px flex gap-1 flex-col">
<div class="filler"></div> <div v-for="character in characters" :key="character.id" class="character relative rounded-l border border-solid border-gray-500 w-9 h-[50px] bg-[url('/assets/ui-texture.png')] after:absolute after:w-full after:h-px after:bg-gray-500" :class="{ active: selected_character == character.id }">
<img class="w-24 h-24 m-auto" draggable="false" src="/assets/icons/plus-icon.svg" /> <img src="/assets/avatar/default/head.png" class="w-9 h-9 object-contain absolute top-1/2 -translate-y-1/2" alt="Player head" />
<span class="self-center text-base absolute bottom-5 w-full text-center translate-y-1/2 z-10">Create new</span> <input class="h-full w-full absolute m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0" type="radio" :id="character.id" name="character" :value="character.id" v-model="selected_character" />
</button> </div>
<div class="character relative rounded-l border border-solid border-gray-500 w-9 h-[50px] bg-[url('/assets/ui-texture.png')]" :class="{ active: characters.length == 0 }" v-if="characters.length < 4">
<button class="p-0 h-full w-full flex flex-col justify-between focus-visible:outline-offset-0" @click="isModalOpen = true">
<img class="w-6 h-6 object-contain absolute top-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2" draggable="false" src="/assets/icons/plus-icon.svg" />
</button>
</div>
</div>
<div class="py-6 px-8 h-[calc(100%_-_48px)] flex flex-col items-center gap-6" v-if="selected_character">
<input class="input-field w-[158px]" type="text" name="name" :placeholder="characters.find((c) => c.id == selected_character)?.name" />
<div class="flex flex-col gap-4 items-center">
<div class="flex flex-col gap-3">
<div class="character-frame w-[190px] h-52 bg-no-repeat bg-center flex items-center justify-between">
<button class="ml-6 w-4 h-8 p-0">
<img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 m-auto" alt="Arrow left" />
</button>
<img class="w-12 object-contain mb-3.5" src="/assets/avatar/default/0.png" alt="Player avatar" />
<button class="mr-6 w-4 h-8 p-0">
<img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 -scale-x-100" alt="Arrow right" />
</button>
</div>
<div class="flex justify-between w-[190px]">
<!-- TODO: replace with color swatches -->
<button v-for="n in 9" class="w-4 h-4 rounded-sm bg-white"></button>
</div>
</div>
<!-- TODO: update gender on (selected) character -->
<div class="flex justify-between w-[190px]">
<button class="btn-empty flex gap-2" :class="{ selected: characters.find((c) => c.id == selected_character)?.characterType?.gender === 'MALE' }">
<img src="/assets/icons/male-icon.svg" class="w-4 h-4 m-auto" alt="Male symbol" />
<span class="text-white">Male</span>
</button>
<button class="btn-empty flex gap-2" :class="{ selected: characters.find((c) => c.id == selected_character)?.characterType?.gender === 'FEMALE' }">
<img src="/assets/icons/male-icon.svg" class="w-4 h-4 m-auto" alt="Male symbol" />
<span class="text-white">Female</span>
</button>
</div>
</div>
</div>
</div>
<div class="w-2/3 h-full bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center rounded-r-md">
<div class="py-6 px-8 h-[calc(100%_-_48px)] flex flex-col items-center gap-10" v-if="selected_character">
<div class="flex flex-col gap-3 w-full">
<span class="text-sm">Hairstyle</span>
<div class="flex gap-2 flex-wrap max-h-20 overflow-y-auto scrollbar">
<button class="bg-gray border border-solid border-gray-500 min-w-9 max-w-9 min-h-9 max-h-9 p-2 rounded-sm hover:bg-gray-500 hover:border-gray-400 focus-visible:outline-none focus-visible:border-gray-300 focus-visible:bg-gray-500">
<img src="/assets/icons/x-button-gray.svg" class="w-4 h-4 m-auto" alt="Male symbol" />
</button>
<!-- TODO: replace with hairstyles -->
<button v-for="n in 30" class="bg-gray border border-solid border-gray-500 min-w-9 max-w-9 min-h-9 max-h-9 p-2 rounded-sm hover:bg-gray-500 hover:border-gray-400 focus-visible:outline-none focus-visible:border-gray-300 focus-visible:bg-gray-500"></button>
</div>
</div>
<div class="flex flex-col gap-3 w-full">
<span class="text-sm">Hair color</span>
<div class="flex gap-2 flex-wrap">
<!-- TODO: replace with hairstyles -->
<button v-for="n in 10" class="bg-white w-6 h-6 rounded-sm"></button>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
<div v-else> <div v-else>
<img class="w-20 invert-80" src="/assets/icons/loading-icon1.svg" /> <img class="w-20 invert-80" src="/assets/icons/loading-icon1.svg" />
</div> </div>
<div class="button-wrapper flex gap-8" v-if="!isLoading"> <div class="button-wrapper flex self-center justify-end gap-4 max-w-[860px] w-full" v-if="!isLoading">
<button <button class="btn-empty min-w-48" @click.stop="gameStore.disconnectSocket()">Back</button>
class="btn-red py-2 pr-2.5 pl-8 min-w-24 relative rounded text-xl flex gap-4 items-center transition-all ease-in-out duration-200 hover:gap-5 disabled:bg-red/50 disabled:hover:bg-opacity-50 disabled:cursor-not-allowed disabled:hover:gap-[15px]" <button class="btn-cyan min-w-48 disabled:bg-cyan-800 disabled:cursor-not-allowed" :disabled="!selected_character" @click="select_character()">Play now</button>
@click.stop="gameStore.disconnectSocket()"
>
<img class="h-8 drop-shadow-20 rotate-180" draggable="false" src="/assets/icons/arrow.svg" alt="Logout icon" />
</button>
<button
class="btn-cyan py-2 px-2.5 pl-8 min-w-24 relative rounded text-xl flex gap-4 items-center transition-all ease-in-out duration-200 hover:gap-5 disabled:bg-cyan-800 disabled:hover:bg-opacity-50 disabled:cursor-not-allowed disabled:hover:gap-[15px]"
:disabled="!selected_character"
@click="select_character()"
>
PLAY
<img class="h-8 drop-shadow-20" draggable="false" src="/assets/icons/arrow.svg" alt="Play icon" />
</button>
</div> </div>
</div> </div>
</div> </div>
@ -126,15 +158,15 @@ const selected_character = ref(null)
function select_character() { function select_character() {
if (!selected_character.value) return if (!selected_character.value) return
deletingCharacter.value = null deletingCharacter.value = null
gameStore.connection?.emit('character:connect', { character_id: selected_character.value }) gameStore.connection?.emit('character:connect', { characterId: selected_character.value })
gameStore.connection?.on('character:connect', (data: CharacterT) => gameStore.setCharacter(data)) gameStore.connection?.on('character:connect', (data: CharacterT) => gameStore.setCharacter(data))
} }
// Delete character logics // Delete character logics
function delete_character(character_id: number) { function delete_character(characterId: number) {
if (!character_id) return if (!characterId) return
deletingCharacter.value = null deletingCharacter.value = null
gameStore.connection?.emit('character:delete', { character_id: character_id }) gameStore.connection?.emit('character:delete', { characterId: characterId })
} }
// Create character logics // Create character logics

View File

@ -5,7 +5,7 @@
<Menu /> <Menu />
<Hud /> <Hud />
<Hotkeys /> <Hotkeys />
<Minimap /> <Clock />
<Zone /> <Zone />
<Chat /> <Chat />
<ExpBar /> <ExpBar />
@ -30,7 +30,8 @@ import Hotkeys from '@/components/gui/Hotkeys.vue'
import Chat from '@/components/gui/Chat.vue' import Chat from '@/components/gui/Chat.vue'
import CharacterProfile from '@/components/gui/CharacterProfile.vue' import CharacterProfile from '@/components/gui/CharacterProfile.vue'
import Effects from '@/components/Effects.vue' import Effects from '@/components/Effects.vue'
import Minimap from '@/components/gui/Minimap.vue' // import Minimap from '@/components/gui/Minimap.vue'
import Clock from '@/components/gui/Clock.vue'
import AwaitLoaderPlugin from 'phaser3-rex-plugins/plugins/awaitloader-plugin' import AwaitLoaderPlugin from 'phaser3-rex-plugins/plugins/awaitloader-plugin'
const gameStore = useGameStore() const gameStore = useGameStore()

View File

@ -8,17 +8,17 @@
<div class="h-dvh flex items-center lg:justify-center flex-col px-8 max-lg:pt-20"> <div class="h-dvh flex items-center lg:justify-center flex-col px-8 max-lg:pt-20">
<img src="/assets/login/sq-logo-v1.svg" class="mb-10" alt="Sylvan Quest logo" /> <img src="/assets/login/sq-logo-v1.svg" class="mb-10" alt="Sylvan Quest logo" />
<div class="relative"> <div class="relative">
<img src="/assets/ui-elements/ui-box-outer.svg" class="absolute w-full h-full" alt="UI box outer" /> <img src="/assets/ui-elements/login-ui-box-outer.svg" class="absolute w-full h-full" alt="UI box outer" />
<img src="/assets/ui-elements/ui-box-inner.svg" class="absolute left-2 top-2 w-[calc(100%_-_16px)] h-[calc(100%_-_16px)] max-lg:hidden" alt="UI box inner" /> <img src="/assets/ui-elements/login-ui-box-inner.svg" class="absolute left-2 top-2 w-[calc(100%_-_16px)] h-[calc(100%_-_16px)] max-lg:hidden" alt="UI box inner" />
<!-- Login Form --> <!-- Login Form -->
<LoginForm v-if="currentForm === 'login' && !doesUrlHaveToken" @openResetPasswordModal="() => (isPasswordResetFormShown = true)" @switchToRegister="currentForm = 'register'" /> <LoginForm v-if="currentForm === 'login' && !doesUrlHaveToken" @openResetPasswordModal="() => (isPasswordResetFormShown = true)" @switchToRegister="currentForm = 'register'" />
<!-- Register Form --> <!-- Register Form -->
<RegisterForm v-if="currentForm === 'register' && !doesUrlHaveToken" @switchToLogin="currentForm = 'login'" /> <RegisterForm v-if="currentForm === 'register' && !doesUrlHaveToken" @switchToLogin="switchToLogin" />
<!-- New Password Form --> <!-- New Password Form -->
<NewPasswordForm v-if="doesUrlHaveToken" @switchToLogin="currentForm = 'login'" /> <NewPasswordForm v-if="doesUrlHaveToken" @switchToLogin="switchToLogin" />
</div> </div>
</div> </div>
</div> </div>
@ -29,17 +29,22 @@
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useCookies } from '@vueuse/integrations/useCookies' import { useCookies } from '@vueuse/integrations/useCookies'
import LoginForm from '@/components/screens/partials/login/LoginForm.vue' import LoginForm from '@/components/login/LoginForm.vue'
import RegisterForm from '@/components/screens/partials/login/RegisterForm.vue' import RegisterForm from '@/components/login/RegisterForm.vue'
import NewPasswordForm from '@/components/screens/partials/login/NewPasswordForm.vue' import NewPasswordForm from '@/components/login/NewPasswordForm.vue'
import ResetPassword from '@/components/utilities/ResetPassword.vue' import ResetPassword from '@/components/login/ResetPasswordModal.vue'
const isPasswordResetFormShown = ref(false) const isPasswordResetFormShown = ref(false)
const doesUrlHaveToken = window.location.hash.includes('#') const doesUrlHaveToken = ref(window.location.hash !== '')
const gameStore = useGameStore() const gameStore = useGameStore()
const currentForm = ref('login') const currentForm = ref('login')
function switchToLogin() {
currentForm.value = 'login'
doesUrlHaveToken.value = false
}
// automatic login because of development // automatic login because of development
onMounted(async () => { onMounted(async () => {
const token = useCookies().get('token') const token = useCookies().get('token')

View File

@ -2,7 +2,7 @@
<div class="flex justify-center items-center h-dvh relative"> <div class="flex justify-center items-center h-dvh relative">
<Game :config="gameConfig" @create="createGame"> <Game :config="gameConfig" @create="createGame">
<Scene name="main" @preload="preloadScene" @create="createScene"> <Scene name="main" @preload="preloadScene" @create="createScene">
<ZoneEditor :key="JSON.stringify(`${zoneEditorStore.zone?.id}_${zoneEditorStore.zone?.createdAt}_${zoneEditorStore.zone?.updatedAt}`)" /> <ZoneEditor :key="JSON.stringify(`${zoneEditorStore.zone?.id}_${zoneEditorStore.zone?.createdAt}_${zoneEditorStore.zone?.updatedAt ?? ''}`)" />
</Scene> </Scene>
</Game> </Game>
</div> </div>

View File

@ -6,7 +6,7 @@
</Container> </Container>
<!-- Character name and health --> <!-- Character name and health -->
<Container :depth="999" :x="currentX" :y="currentY"> <Container :depth="999" :x="currentX" :y="currentY">
<Text @create="createNicknameText" :text="character.name" /> <Text @create="createNicknameText" :text="props.zoneCharacter.character.name" />
<RoundRectangle :origin-x="0.5" :origin-y="18.5" :fillColor="0xffffff" :width="74" :height="6" :radius="5" /> <RoundRectangle :origin-x="0.5" :origin-y="18.5" :fillColor="0xffffff" :width="74" :height="6" :radius="5" />
<RoundRectangle :origin-x="0.5" :origin-y="36.4" :fillColor="0x00b3b3" :width="70" :height="3" :radius="5" /> <RoundRectangle :origin-x="0.5" :origin-y="36.4" :fillColor="0x00b3b3" :width="70" :height="3" :radius="5" />
</Container> </Container>
@ -18,7 +18,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import config from '@/config' import config from '@/config'
import { type ExtendedCharacter, type Sprite as SpriteT } from '@/types' import { type Sprite as SpriteT, type ZoneCharacter } from '@/types'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useZoneStore } from '@/stores/zoneStore' import { useZoneStore } from '@/stores/zoneStore'
import { watch, computed, ref, onMounted, onUnmounted } from 'vue' import { watch, computed, ref, onMounted, onUnmounted } from 'vue'
@ -34,7 +34,7 @@ enum Direction {
const props = defineProps<{ const props = defineProps<{
layer: Phaser.Tilemaps.TilemapLayer layer: Phaser.Tilemaps.TilemapLayer
character: ExtendedCharacter zoneCharacter: ZoneCharacter
}>() }>()
const charChatContainer = refObj<Phaser.GameObjects.Container>() const charChatContainer = refObj<Phaser.GameObjects.Container>()
@ -110,19 +110,19 @@ const calcDirection = (oldX: number, oldY: number, newX: number, newY: number):
return Direction.UNCHANGED return Direction.UNCHANGED
} }
const isFlippedX = computed(() => [6, 4].includes(props.character.rotation ?? 0)) const isFlippedX = computed(() => [6, 4].includes(props.zoneCharacter.character.rotation ?? 0))
const charTexture = computed(() => { const charTexture = computed(() => {
const { rotation, characterType, isMoving } = props.character const { rotation, characterType } = props.zoneCharacter.character
const spriteId = characterType?.sprite?.id ?? 'idle_right_down' const spriteId = characterType?.sprite?.id ?? 'idle_right_down'
const action = isMoving ? 'walk' : 'idle' const action = props.zoneCharacter.isMoving ? 'walk' : 'idle'
const direction = [0, 6].includes(rotation) ? 'left_up' : 'right_down' const direction = [0, 6].includes(rotation) ? 'left_up' : 'right_down'
return `${spriteId}-${action}_${direction}` return `${spriteId}-${action}_${direction}`
}) })
const updateSprite = () => { const updateSprite = () => {
if (props.character.isMoving) { if (props.zoneCharacter.isMoving) {
charSprite.value!.anims.play(charTexture.value, true) charSprite.value!.anims.play(charTexture.value, true)
return return
} }
@ -133,11 +133,11 @@ const updateSprite = () => {
} }
const createChatBubble = (container: Phaser.GameObjects.Container) => { const createChatBubble = (container: Phaser.GameObjects.Container) => {
container.setName(`${props.character.name}_chatBubble`) container.setName(`${props.zoneCharacter.character.name}_chatBubble`)
} }
const createChatText = (text: Phaser.GameObjects.Text) => { const createChatText = (text: Phaser.GameObjects.Text) => {
text.setName(`${props.character.name}_chatText`) text.setName(`${props.zoneCharacter.character.name}_chatText`)
text.setFontSize(13) text.setFontSize(13)
text.setFontFamily('Arial') text.setFontFamily('Arial')
text.setOrigin(0.5, 10.9) text.setOrigin(0.5, 10.9)
@ -168,7 +168,7 @@ const createNicknameText = (text: Phaser.GameObjects.Text) => {
} }
watch( watch(
() => props.character, () => props.zoneCharacter.character,
(newChar, oldChar) => { (newChar, oldChar) => {
if (!newChar) return if (!newChar) return
@ -179,10 +179,9 @@ watch(
} }
) )
watch(() => props.character.isMoving, updateSprite) watch(() => props.zoneCharacter, updateSprite)
watch(() => props.character.rotation, updateSprite)
loadSpriteTextures(scene, props.character.characterType?.sprite as SpriteT) loadSpriteTextures(scene, props.zoneCharacter.character.characterType?.sprite as SpriteT)
.then(() => { .then(() => {
charSprite.value!.setTexture(charTexture.value) charSprite.value!.setTexture(charTexture.value)
charSprite.value!.setFlipX(isFlippedX.value) charSprite.value!.setFlipX(isFlippedX.value)
@ -192,11 +191,11 @@ loadSpriteTextures(scene, props.character.characterType?.sprite as SpriteT)
}) })
onMounted(() => { onMounted(() => {
charChatContainer.value!.setName(`${props.character!.name}_chatContainer`) charChatContainer.value!.setName(`${props.zoneCharacter.character!.name}_chatContainer`)
charChatContainer.value!.setVisible(false) charChatContainer.value!.setVisible(false)
charContainer.value!.setName(props.character!.name) charContainer.value!.setName(props.zoneCharacter.character!.name)
if (props.character.id === gameStore.character!.id) { if (props.zoneCharacter.character.id === gameStore.character!.id) {
zoneStore.setCharacterLoaded(true) zoneStore.setCharacterLoaded(true)
// #146 : Set camera position to character, need to be improved still // #146 : Set camera position to character, need to be improved still
@ -204,7 +203,7 @@ onMounted(() => {
scene.cameras.main.stopFollow() scene.cameras.main.stopFollow()
} }
updatePosition(props.character.positionX, props.character.positionY, props.character.rotation) updatePosition(props.zoneCharacter.character.positionX, props.zoneCharacter.character.positionY, props.zoneCharacter.character.rotation)
}) })
onUnmounted(() => { onUnmounted(() => {

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="mb-4 flex flex-col gap-3"> <div class="mb-4 flex flex-col gap-3">
<div @click="toggle" class="p-3 bg-gray-100 bg-opacity-50 rounded hover:bg-gray-200 text-white font-default cursor-pointer"> <div @click="toggle" class="p-3 bg-gray-200 bg-opacity-50 rounded hover:bg-gray-300 text-white font-default cursor-pointer">
<slot name="header" /> <slot name="header" />
</div> </div>
<transition enter-active-class="transition-all duration-300 ease-in-out" leave-active-class="transition-all duration-300 ease-in-out" enter-from-class="opacity-0 max-h-0" enter-to-class="opacity-100 max-h-96" leave-from-class="opacity-100 max-h-96" leave-to-class="opacity-0 max-h-0"> <transition enter-active-class="transition-all duration-300 ease-in-out" leave-active-class="transition-all duration-300 ease-in-out" enter-from-class="opacity-0 max-h-0" enter-to-class="opacity-100 max-h-96" leave-from-class="opacity-100 max-h-96" leave-to-class="opacity-0 max-h-0">

View File

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

View File

@ -1,245 +1,230 @@
<template> <template>
<Teleport to="body"> <Teleport to="body">
<div v-if="isModalOpenRef" class="fixed border-solid border-2 border-gray-500 z-50 flex flex-col backdrop-blur-sm shadow-lg" :style="modalStyle"> <div v-if="isModalOpenRef" class="fixed border-solid border-2 border-gray-500 z-50 flex flex-col backdrop-blur-sm shadow-lg" :style="modalStyle">
<!-- Header -->
<div @mousedown="startDrag" class="cursor-move p-2.5 flex justify-between items-center border-solid border-0 border-b border-gray-500 relative"> <div @mousedown="startDrag" class="cursor-move p-2.5 flex justify-between items-center border-solid border-0 border-b border-gray-500 relative">
<div class="rounded-t absolute w-full h-full top-0 left-0 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-center bg-cover opacity-90"></div> <div
:class="{
'bg-[url(/assets/ui-texture.png)] bg-no-repeat bg-center bg-cover opacity-90': !disableBgTexture,
'bg-gray-700': disableBgTexture
}"
class="rounded-t absolute w-full h-full top-0 left-0"
/>
<div class="relative z-10"> <div class="relative z-10">
<slot name="modalHeader" /> <slot name="modalHeader" />
</div> </div>
<div class="flex gap-2.5"> <div class="flex gap-2.5">
<button @click="toggleFullScreen" class="w-5 h-5 m-0 p-0 relative hover:scale-110 transition-transform duration-300 ease-in-out" v-if="canFullScreen"> <button v-if="canFullScreen" @click="toggleFullScreen" class="w-5 h-5 m-0 p-0 relative hover:scale-110 transition-transform duration-300 ease-in-out">
<img :alt="isFullScreen ? 'exit full-screen' : 'full-screen'" draggable="false" :src="isFullScreen ? '/assets/icons/minimize.svg' : '/assets/icons/increase-size-option.svg'" class="w-3.5 h-3.5 invert" /> <img :alt="isFullScreen ? 'exit full-screen' : 'full-screen'" :src="isFullScreen ? '/assets/icons/modal/minimize.svg' : '/assets/icons/modal/increase-size-option.svg'" class="w-3.5 h-3.5 invert" draggable="false" />
</button> </button>
<button @click="close" v-if="closable" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out"> <button v-if="closable" @click="emit('modal:close')" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
<img alt="close" draggable="false" src="/assets/icons/close-button-white.svg" class="w-full h-full" /> <img alt="close" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" draggable="false" />
</button> </button>
</div> </div>
</div> </div>
<!-- Body -->
<div class="overflow-hidden grow relative"> <div class="overflow-hidden grow relative">
<div class="rounded-b absolute w-full h-full top-0 left-0 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center opacity-90"></div> <div
:class="{
'bg-[url(/assets/ui-texture.png)] bg-no-repeat bg-center bg-cover opacity-90': !disableBgTexture,
'bg-gray-700': disableBgTexture
}"
class="rounded-b absolute w-full h-full top-0 left-0"
/>
<div class="relative z-10 h-full"> <div class="relative z-10 h-full">
<slot name="modalBody" /> <slot name="modalBody" />
</div> </div>
<img v-if="isResizable && !isFullScreen" src="/assets/icons/resize-icon.svg" alt="resize" class="absolute z-10 bottom-0 right-0 w-5 h-5 cursor-nwse-resize" @mousedown="startResize" /> <img v-if="isResizable && !isFullScreen" src="/assets/icons/modal/resize-icon.svg" alt="resize" class="absolute z-10 bottom-0 right-0 w-5 h-5 cursor-nwse-resize" @mousedown="startResize" />
</div> </div>
</div> </div>
</Teleport> </Teleport>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineEmits, onMounted, onUnmounted, ref, watch, computed } from 'vue' import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
const props = defineProps({ interface ModalProps {
isModalOpen: { isModalOpen: boolean
type: Boolean, closable?: boolean
default: false isResizable?: boolean
}, canFullScreen?: boolean
closable: { modalPositionX?: number
type: Boolean, modalPositionY?: number
default: true modalWidth?: number
}, modalHeight?: number
isResizable: { disableBgTexture?: boolean
type: Boolean, }
default: true
}, interface Position {
canFullScreen: { x: number
type: Boolean, y: number
default: false width: number
}, height: number
modalPositionX: { }
type: Number,
default: 0 const props = withDefaults(defineProps<ModalProps>(), {
}, isModalOpen: false,
modalPositionY: { closable: true,
type: Number, isResizable: true,
default: 0 canFullScreen: false,
}, modalPositionX: 0,
modalWidth: { modalPositionY: 0,
type: Number, modalWidth: 500,
default: 500 modalHeight: 280,
}, disableBgTexture: false
modalHeight: {
type: Number,
default: 280
}
}) })
const isModalOpenRef = ref(props.isModalOpen) const emit = defineEmits<{
const emit = defineEmits(['modal:close', 'character:create']) 'modal:close': []
'character:create': []
}>()
const isModalOpenRef = ref(props.isModalOpen)
const width = ref(props.modalWidth) const width = ref(props.modalWidth)
const height = ref(props.modalHeight) const height = ref(props.modalHeight)
const x = ref(0) const x = ref(0)
const y = ref(0) const y = ref(0)
const minWidth = ref(200)
const minHeight = ref(100)
const isResizing = ref(false) const isResizing = ref(false)
const isDragging = ref(false) const isDragging = ref(false)
const isFullScreen = ref(false) const isFullScreen = ref(false)
let startX = 0 const minDimensions = {
let startY = 0 width: 200,
let initialX = 0 height: 100
let initialY = 0 }
let startWidth = 0
let startHeight = 0 let dragState = {
let preFullScreenState = { x: 0, y: 0, width: 0, height: 0 } startX: 0,
startY: 0,
initialX: 0,
initialY: 0,
startWidth: 0,
startHeight: 0
}
let preFullScreenState: Position = { x: 0, y: 0, width: 0, height: 0 }
const modalStyle = computed(() => ({ const modalStyle = computed(() => ({
borderRadius: isFullScreen.value ? '0' : '6px', borderRadius: isFullScreen.value ? '0' : '6px',
top: isFullScreen.value ? '0' : `${y.value}px`, top: isFullScreen.value ? '0' : `${y.value}px`,
left: isFullScreen.value ? '0' : `${x.value}px`, left: isFullScreen.value ? '0' : `${x.value}px`,
width: isFullScreen.value ? '100vw' : `${width.value}px`, width: isFullScreen.value ? '100vw' : `${width.value}px`,
height: isFullScreen.value ? '100vh' : `${height.value}px`, height: isFullScreen.value ? '100vh' : `${height.value}px`
maxWidth: '100vw',
maxHeight: '100vh'
})) }))
function close() {
emit('modal:close')
}
function startResize(event: MouseEvent) { function startResize(event: MouseEvent) {
if (isFullScreen.value) return if (isFullScreen.value) return
isResizing.value = true isResizing.value = true
startWidth = width.value - event.clientX dragState.startWidth = width.value - event.clientX
startHeight = height.value - event.clientY dragState.startHeight = height.value - event.clientY
event.preventDefault() event.preventDefault()
} }
function resizeModal(event: MouseEvent) { function resizeModal(event: MouseEvent) {
if (!isResizing.value || isFullScreen.value) return if (!isResizing.value || isFullScreen.value) return
const newWidth = Math.min(startWidth + event.clientX, window.innerWidth) width.value = Math.max(dragState.startWidth + event.clientX, minDimensions.width)
const newHeight = Math.min(startHeight + event.clientY, window.innerHeight) height.value = Math.max(dragState.startHeight + event.clientY, minDimensions.height)
width.value = Math.max(newWidth, minWidth.value)
height.value = Math.max(newHeight, minHeight.value)
adjustPosition()
}
function stopResize() {
isResizing.value = false
} }
function startDrag(event: MouseEvent) { function startDrag(event: MouseEvent) {
if (isFullScreen.value) return if (isFullScreen.value) return
isDragging.value = true isDragging.value = true
startX = event.clientX dragState = {
startY = event.clientY startX: event.clientX,
initialX = x.value startY: event.clientY,
initialY = y.value initialX: x.value,
initialY: y.value,
startWidth: width.value,
startHeight: height.value
}
event.preventDefault() event.preventDefault()
} }
function drag(event: MouseEvent) { function drag(event: MouseEvent) {
if (!isDragging.value || isFullScreen.value) return if (!isDragging.value || isFullScreen.value) return
const dx = event.clientX - startX x.value = dragState.initialX + (event.clientX - dragState.startX)
const dy = event.clientY - startY y.value = dragState.initialY + (event.clientY - dragState.startY)
x.value = initialX + dx
y.value = initialY + dy
adjustPosition()
}
function stopDrag() {
isDragging.value = false
}
function adjustPosition() {
if (isFullScreen.value) return
x.value = Math.min(x.value, window.innerWidth - width.value)
y.value = Math.min(y.value, window.innerHeight - height.value)
}
function handleResize() {
if (isFullScreen.value) return
width.value = Math.min(width.value, window.innerWidth)
height.value = Math.min(height.value, window.innerHeight)
adjustPosition()
}
function initializePosition() {
width.value = Math.min(props.modalWidth, window.innerWidth)
height.value = Math.min(props.modalHeight, window.innerHeight)
if (props.modalPositionX !== 0 && props.modalPositionY !== 0) {
x.value = props.modalPositionX
y.value = props.modalPositionY
} else {
x.value = (window.innerWidth - width.value) / 2
y.value = (window.innerHeight - height.value) / 2
}
} }
function toggleFullScreen() { function toggleFullScreen() {
if (isFullScreen.value) { if (isFullScreen.value) {
// Exit full-screen Object.assign({ x, y, width, height }, preFullScreenState)
x.value = preFullScreenState.x
y.value = preFullScreenState.y
width.value = preFullScreenState.width
height.value = preFullScreenState.height
isFullScreen.value = false
} else { } else {
// Enter full-screen
preFullScreenState = { x: x.value, y: y.value, width: width.value, height: height.value } preFullScreenState = { x: x.value, y: y.value, width: width.value, height: height.value }
isFullScreen.value = true
} }
isFullScreen.value = !isFullScreen.value
} }
function initializePosition() {
width.value = props.modalWidth
height.value = props.modalHeight
x.value = props.modalPositionX || (window.innerWidth - width.value) / 2
y.value = props.modalPositionY || (window.innerHeight - height.value) / 2
}
// Watchers
watch( watch(
() => props.isModalOpen, () => props.isModalOpen,
(value) => { (value) => {
isModalOpenRef.value = value isModalOpenRef.value = value
if (value) { if (value) initializePosition()
initializePosition()
}
} }
) )
watch( watch(
() => props.modalWidth, () => props.modalWidth,
(value) => { (value) => (width.value = value)
width.value = Math.min(value, window.innerWidth)
}
) )
watch( watch(
() => props.modalHeight, () => props.modalHeight,
(value) => { (value) => (height.value = value)
height.value = Math.min(value, window.innerHeight)
}
) )
watch( watch(
() => props.modalPositionX, () => props.modalPositionX,
(value) => { (value) => (x.value = value)
x.value = value
}
) )
watch( watch(
() => props.modalPositionY, () => props.modalPositionY,
(value) => { (value) => (y.value = value)
y.value = value
}
) )
// Lifecycle hooks
onMounted(() => { onMounted(() => {
addEventListener('mousemove', drag) const handlers: Record<string, EventListener[]> = {
addEventListener('mouseup', stopDrag) mousemove: [(e: Event) => drag(e as MouseEvent), (e: Event) => resizeModal(e as MouseEvent)],
addEventListener('mousemove', resizeModal) mouseup: [
addEventListener('mouseup', stopResize) () => {
if (props.modalPositionX !== 0 && props.modalPositionY !== 0) { isDragging.value = false
addEventListener('resize', handleResize) },
() => {
isResizing.value = false
}
]
} }
Object.entries(handlers).forEach(([event, fns]) => {
fns.forEach((fn) => window.addEventListener(event, fn))
})
initializePosition() initializePosition()
}) })
onUnmounted(() => { onUnmounted(() => {
removeEventListener('mousemove', drag) const handlers: Record<string, EventListener[]> = {
removeEventListener('mouseup', stopDrag) mousemove: [(e: Event) => drag(e as MouseEvent), (e: Event) => resizeModal(e as MouseEvent)],
removeEventListener('mousemove', resizeModal) mouseup: [
removeEventListener('mouseup', stopResize) () => {
if (props.modalPositionX !== 0 && props.modalPositionY !== 0) { isDragging.value = false
removeEventListener('resize', handleResize) },
() => {
isResizing.value = false
}
]
} }
Object.entries(handlers).forEach(([event, fns]) => {
fns.forEach((fn) => window.removeEventListener(event, fn))
})
}) })
</script> </script>

View File

@ -1,11 +1,10 @@
<template> <template>
<Character v-for="item in zoneStore.characters" :key="item.id" :layer="tilemap" :character="item" /> <Character v-for="item in zoneStore.characters" :key="item.character.id" :layer="tilemap" :zoneCharacter="item" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Character from '@/components/sprites/Character.vue' import Character from '@/components/sprites/Character.vue'
import { useZoneStore } from '@/stores/zoneStore' import { useZoneStore } from '@/stores/zoneStore'
import { calculateIsometricDepth } from '@/composables/zoneComposable'
const zoneStore = useZoneStore() const zoneStore = useZoneStore()

View File

@ -1,5 +1,5 @@
<template> <template>
<ZoneTiles :key="zoneStore.zone?.id ?? 0" @tilemap:create="tileMap = $event" /> <ZoneTiles :key="zoneStore.zone?.id ?? 0" @tileMap:create="tileMap = $event" />
<ZoneObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" /> <ZoneObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
<Characters v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" /> <Characters v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
</template> </template>
@ -10,7 +10,7 @@ import { useScene } from 'phavuer'
import { useGameStore } from '@/stores/gameStore' import { useGameStore } from '@/stores/gameStore'
import { useZoneStore } from '@/stores/zoneStore' import { useZoneStore } from '@/stores/zoneStore'
import { loadZoneTilesIntoScene } from '@/composables/zoneComposable' import { loadZoneTilesIntoScene } from '@/composables/zoneComposable'
import type { Character as CharacterT, Zone as ZoneT, ExtendedCharacter as ExtendedCharacterT } from '@/types' import type { Zone as ZoneT, ZoneCharacter } from '@/types'
import ZoneTiles from '@/components/zone/ZoneTiles.vue' import ZoneTiles from '@/components/zone/ZoneTiles.vue'
import ZoneObjects from '@/components/zone/ZoneObjects.vue' import ZoneObjects from '@/components/zone/ZoneObjects.vue'
import Characters from '@/components/zone/Characters.vue' import Characters from '@/components/zone/Characters.vue'
@ -23,7 +23,7 @@ const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null)
type zoneLoadData = { type zoneLoadData = {
zone: ZoneT zone: ZoneT
characters: CharacterT[] characters: ZoneCharacter[]
} }
// Event listeners // Event listeners
@ -41,17 +41,17 @@ gameStore.connection!.on('zone:character:teleport', async (data: zoneLoadData) =
zoneStore.setCharacters(data.characters) zoneStore.setCharacters(data.characters)
}) })
gameStore.connection!.on('zone:character:join', async (data: ExtendedCharacterT) => { gameStore.connection!.on('zone:character:join', async (data: ZoneCharacter) => {
// If data is from the current user, don't add it to the store // If data is from the current user, don't add it to the store
if (data.id === gameStore.character?.id) return if (data.character.id === gameStore.character?.id) return
zoneStore.addCharacter(data) zoneStore.addCharacter(data)
}) })
gameStore.connection!.on('zone:character:leave', (character_id: number) => { gameStore.connection!.on('zone:character:leave', (characterId: number) => {
zoneStore.removeCharacter(character_id) zoneStore.removeCharacter(characterId)
}) })
gameStore.connection!.on('character:move', (data: ExtendedCharacterT) => { gameStore.connection!.on('character:move', (data: ZoneCharacter) => {
zoneStore.updateCharacter(data) zoneStore.updateCharacter(data)
}) })

View File

@ -1,5 +1,5 @@
<template> <template>
<Controls :layer="tiles" :depth="0" /> <Controls :layer="tileLayer" :depth="0" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -11,19 +11,19 @@ import { FlattenZoneArray, setLayerTiles } from '@/composables/zoneComposable'
import Controls from '@/components/utilities/Controls.vue' import Controls from '@/components/utilities/Controls.vue'
import { unduplicateArray } from '@/utilities' import { unduplicateArray } from '@/utilities'
const emit = defineEmits(['tilemap:create']) const emit = defineEmits(['tileMap:create'])
const scene = useScene() const scene = useScene()
const zoneStore = useZoneStore() const zoneStore = useZoneStore()
const zoneTilemap = createTilemap() const tileMap = createTileMap()
const tiles = createTileLayer() const tileLayer = createTileLayer()
/** /**
* A Tilemap is a container for Tilemap data. * 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. * 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. * A map can have one or more tilemap layers, which are the display objects that actually render the tiles.
*/ */
function createTilemap() { function createTileMap() {
const zoneData = new Phaser.Tilemaps.MapData({ const zoneData = new Phaser.Tilemaps.MapData({
width: zoneStore.zone?.width, width: zoneStore.zone?.width,
height: zoneStore.zone?.height, height: zoneStore.zone?.height,
@ -32,9 +32,11 @@ function createTilemap() {
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC, orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
format: Phaser.Tilemaps.Formats.ARRAY_2D format: Phaser.Tilemaps.Formats.ARRAY_2D
}) })
const tilemap = new Phaser.Tilemaps.Tilemap(scene, zoneData)
emit('tilemap:create', tilemap) const newTileMap = new Phaser.Tilemaps.Tilemap(scene, zoneData)
return tilemap emit('tileMap:create', newTileMap)
return newTileMap
} }
/** /**
@ -44,12 +46,12 @@ function createTileLayer() {
const tilesArray = unduplicateArray(FlattenZoneArray(zoneStore.zone?.tiles ?? [])) const tilesArray = unduplicateArray(FlattenZoneArray(zoneStore.zone?.tiles ?? []))
const tilesetImages = Array.from(tilesArray).map((tile: any, index: number) => { const tilesetImages = Array.from(tilesArray).map((tile: any, index: number) => {
return zoneTilemap.addTilesetImage(tile, tile, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y }) return tileMap.addTilesetImage(tile, tile, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y })
}) as any }) as any
// Add blank tile // Add blank tile
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 })) 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 = zoneTilemap.createBlankLayer('tiles', tilesetImages, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer const layer = tileMap.createBlankLayer('tiles', tilesetImages, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
layer.setDepth(0) layer.setDepth(0)
layer.setCullPadding(2, 2) layer.setCullPadding(2, 2)
@ -57,11 +59,11 @@ function createTileLayer() {
return layer return layer
} }
setLayerTiles(zoneTilemap, tiles, zoneStore.zone?.tiles) setLayerTiles(tileMap, tileLayer, zoneStore.zone?.tiles)
onBeforeUnmount(() => { onBeforeUnmount(() => {
zoneTilemap.destroyLayer('tiles') tileMap.destroyLayer('tiles')
zoneTilemap.removeAllLayers() tileMap.removeAllLayers()
zoneTilemap.destroy() tileMap.destroy()
}) })
</script> </script>

View File

@ -72,6 +72,9 @@ export async function loadSpriteTextures(scene: Phaser.Scene, sprite: Sprite) {
// If the sprite is not animated, skip // If the sprite is not animated, skip
if (!sprite_action.isAnimated) continue if (!sprite_action.isAnimated) continue
// Check if animation already exists
if (scene.anims.get(sprite_action.key)) continue
// Add the animation to the scene // Add the animation to the scene
const anim = scene.textures.get(sprite_action.key) const anim = scene.textures.get(sprite_action.key)
scene.textures.addSpriteSheet(sprite_action.key, anim, { frameWidth: sprite_action.frameWidth ?? 0, frameHeight: sprite_action.frameHeight ?? 0 }) scene.textures.addSpriteSheet(sprite_action.key, anim, { frameWidth: sprite_action.frameWidth ?? 0, frameHeight: sprite_action.frameHeight ?? 0 })

View File

@ -48,7 +48,7 @@ export function useGamePointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilema
if (distance <= dragThreshold) { if (distance <= dragThreshold) {
const pointerTile = getTile(layer, pointer.worldX, pointer.worldY) const pointerTile = getTile(layer, pointer.worldX, pointer.worldY)
if (pointerTile) { if (pointerTile) {
gameStore.connection?.emit('character:initMove', { gameStore.connection?.emit('character:move', {
positionX: pointerTile.x, positionX: pointerTile.x,
positionY: pointerTile.y positionY: pointerTile.y
}) })

View File

@ -9,7 +9,7 @@ export async function register(username: string, email: string, password: string
useCookies().set('token', response.data.token as string) useCookies().set('token', response.data.token as string)
return { success: true, token: response.data.token } return { success: true, token: response.data.token }
} catch (error: any) { } catch (error: any) {
if (typeof error.response.data === 'undefined') { if (typeof error.response?.data === 'undefined') {
return { error: 'Could not connect to server' } return { error: 'Could not connect to server' }
} }
return { error: error.response.data.message } return { error: error.response.data.message }
@ -24,6 +24,9 @@ export async function login(username: string, password: string) {
}) })
return { success: true, token: response.data.token } return { success: true, token: response.data.token }
} catch (error: any) { } catch (error: any) {
if (typeof error.response?.data === 'undefined') {
return { error: 'Could not connect to server' }
}
return { error: error.response.data.message } return { error: error.response.data.message }
} }
} }
@ -33,7 +36,7 @@ export async function resetPassword(email: string) {
const response = await axios.post(`${config.server_endpoint}/reset-password`, { email }) const response = await axios.post(`${config.server_endpoint}/reset-password`, { email })
return { success: true, token: response.data.token } return { success: true, token: response.data.token }
} catch (error: any) { } catch (error: any) {
if (typeof error.response.data === 'undefined') { if (typeof error.response?.data === 'undefined') {
return { error: 'Could not connect to server' } return { error: 'Could not connect to server' }
} }
return { error: error.response.data.message } return { error: error.response.data.message }
@ -45,7 +48,7 @@ export async function newPassword(urlToken: string, password: string) {
const response = await axios.post(`${config.server_endpoint}/new-password`, { urlToken, password }) const response = await axios.post(`${config.server_endpoint}/new-password`, { urlToken, password })
return { success: true, token: response.data.token } return { success: true, token: response.data.token }
} catch (error: any) { } catch (error: any) {
if (typeof error.response.data === 'undefined') { if (typeof error.response?.data === 'undefined') {
return { error: 'Could not connect to server' } return { error: 'Could not connect to server' }
} }
return { error: error.response.data.message } return { error: error.response.data.message }

View File

@ -1,6 +1,6 @@
import { ref } from 'vue' import { ref } from 'vue'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import type { Tile, Object, Sprite, CharacterType } from '@/types' import type { Tile, Object, Sprite, CharacterType, CharacterHair } from '@/types'
export const useAssetManagerStore = defineStore('assetManager', () => { export const useAssetManagerStore = defineStore('assetManager', () => {
const tileList = ref<Tile[]>([]) const tileList = ref<Tile[]>([])
@ -15,6 +15,9 @@ export const useAssetManagerStore = defineStore('assetManager', () => {
const characterTypeList = ref<CharacterType[]>([]) const characterTypeList = ref<CharacterType[]>([])
const selectedCharacterType = ref<CharacterType | null>(null) const selectedCharacterType = ref<CharacterType | null>(null)
const characterHairList = ref<CharacterHair[]>([])
const selectedCharacterHair = ref<CharacterHair | null>(null)
function setTileList(tiles: Tile[]) { function setTileList(tiles: Tile[]) {
tileList.value = tiles tileList.value = tiles
} }
@ -47,6 +50,14 @@ export const useAssetManagerStore = defineStore('assetManager', () => {
selectedCharacterType.value = characterType selectedCharacterType.value = characterType
} }
function setCharacterHairList(characterHair: CharacterHair[]) {
characterHairList.value = characterHair
}
function setSelectedCharacterHair(characterHair: CharacterHair | null) {
selectedCharacterHair.value = characterHair
}
return { return {
tileList, tileList,
selectedTile, selectedTile,
@ -56,6 +67,8 @@ export const useAssetManagerStore = defineStore('assetManager', () => {
selectedSprite, selectedSprite,
characterTypeList, characterTypeList,
selectedCharacterType, selectedCharacterType,
characterHairList,
selectedCharacterHair,
setTileList, setTileList,
setSelectedTile, setSelectedTile,
setObjectList, setObjectList,
@ -63,6 +76,8 @@ export const useAssetManagerStore = defineStore('assetManager', () => {
setSelectedObject, setSelectedObject,
setSpriteList, setSpriteList,
setSelectedSprite, setSelectedSprite,
setSelectedCharacterType setSelectedCharacterType,
setCharacterHairList,
setSelectedCharacterHair
} }
}) })

View File

@ -27,6 +27,7 @@ export const useZoneEditorStore = defineStore('zoneEditor', {
isZoneListModalShown: false, isZoneListModalShown: false,
isCreateZoneModalShown: false, isCreateZoneModalShown: false,
isSettingsModalShown: false, isSettingsModalShown: false,
shouldClearTiles: false,
zoneSettings: { zoneSettings: {
name: '', name: '',
width: 0, width: 0,
@ -106,6 +107,13 @@ export const useZoneEditorStore = defineStore('zoneEditor', {
setTeleportSettings(teleportSettings: TeleportSettings) { setTeleportSettings(teleportSettings: TeleportSettings) {
this.teleportSettings = teleportSettings this.teleportSettings = teleportSettings
}, },
triggerClearTiles() {
this.shouldClearTiles = true
},
resetClearTilesFlag() {
this.shouldClearTiles = false
},
reset(resetZone = false) { reset(resetZone = false) {
if (resetZone) this.zone = null if (resetZone) this.zone = null
this.zoneList = [] this.zoneList = []
@ -115,9 +123,12 @@ export const useZoneEditorStore = defineStore('zoneEditor', {
this.drawMode = 'tile' this.drawMode = 'tile'
this.selectedTile = '' this.selectedTile = ''
this.selectedObject = null this.selectedObject = null
this.isTileListModalShown = false
this.isObjectListModalShown = false
this.isSettingsModalShown = false this.isSettingsModalShown = false
this.isZoneListModalShown = false this.isZoneListModalShown = false
this.isCreateZoneModalShown = false this.isCreateZoneModalShown = false
this.shouldClearTiles = false
} }
} }
}) })

View File

@ -1,17 +1,17 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import type { ExtendedCharacter, Zone } from '@/types' import type { ZoneCharacter, Zone } from '@/types'
export const useZoneStore = defineStore('zone', { export const useZoneStore = defineStore('zone', {
state: () => { state: () => {
return { return {
zone: null as Zone | null, zone: null as Zone | null,
characters: [] as ExtendedCharacter[], characters: [] as ZoneCharacter[],
characterLoaded: false characterLoaded: false
} }
}, },
getters: { getters: {
getCharacterById: (state) => { getCharacterById: (state) => {
return (id: number) => state.characters.find((char) => char.id === id) return (id: number) => state.characters.find((char) => char.character.id === id)
}, },
getCharacterCount: (state) => { getCharacterCount: (state) => {
return state.characters.length return state.characters.length
@ -24,20 +24,18 @@ export const useZoneStore = defineStore('zone', {
setZone(zone: Zone | null) { setZone(zone: Zone | null) {
this.zone = zone this.zone = zone
}, },
setCharacters(characters: ExtendedCharacter[]) { setCharacters(characters: ZoneCharacter[]) {
this.characters = characters this.characters = characters
}, },
addCharacter(character: ExtendedCharacter) { addCharacter(character: ZoneCharacter) {
this.characters.push(character) this.characters.push(character)
}, },
updateCharacter(updatedCharacter: ExtendedCharacter) { updateCharacter(updatedCharacter: ZoneCharacter) {
const index = this.characters.findIndex((char) => char.id === updatedCharacter.id) const index = this.characters.findIndex((char) => char.character.id === updatedCharacter.character.id)
if (index !== -1) { if (index !== -1) this.characters[index] = updatedCharacter
this.characters[index] = { ...this.characters[index], ...updatedCharacter }
}
}, },
removeCharacter(character_id: number) { removeCharacter(characterId: number) {
this.characters = this.characters.filter((char) => char.id !== character_id) this.characters = this.characters.filter((char) => char.character.id !== characterId)
}, },
setCharacterLoaded(loaded: boolean) { setCharacterLoaded(loaded: boolean) {
this.characterLoaded = loaded this.characterLoaded = loaded

View File

@ -137,6 +137,7 @@ export type CharacterType = {
name: string name: string
gender: CharacterGender gender: CharacterGender
race: CharacterRace race: CharacterRace
isEnabledForCharCreation: boolean
characters: Character[] characters: Character[]
spriteId?: string spriteId?: string
sprite?: Sprite sprite?: Sprite
@ -144,6 +145,16 @@ export type CharacterType = {
updatedAt: Date updatedAt: Date
} }
export type CharacterHair = {
id: number
name: string
spriteId: string
sprite: Sprite
gender: CharacterGender
isEnabledForCharCreation: boolean
// @TODO: Do we need addedAt and updatedAt?
}
export type Character = { export type Character = {
id: number id: number
userId: number userId: number
@ -162,11 +173,14 @@ export type Character = {
zone: Zone zone: Zone
characterTypeId: number | null characterTypeId: number | null
characterType: CharacterType | null characterType: CharacterType | null
hairId: number | null
hair: CharacterHair | null
chats: Chat[] chats: Chat[]
items: CharacterItem[] items: CharacterItem[]
} }
export type ExtendedCharacter = Character & { export type ZoneCharacter = {
character: Character
isMoving?: boolean isMoving?: boolean
} }
@ -213,14 +227,16 @@ export type Chat = {
createdAt: Date createdAt: Date
} }
export type ChatMessage = {
character: Character
message: string
}
export type WorldSettings = { export type WorldSettings = {
date: Date date: Date
isRainEnabled: boolean isRainEnabled: boolean
isFogEnabled: boolean isFogEnabled: boolean
fogDensity: number fogDensity: number
} }
export type WeatherState = {
isRainEnabled: boolean
rainPercentage: number
isFogEnabled: boolean
fogDensity: number
}