Compare commits
153 Commits
feature/ch
...
feature/#2
Author | SHA1 | Date | |
---|---|---|---|
90bdf43b64 | |||
e9dfcf7870 | |||
d0c08c25fd | |||
7bb7af9476 | |||
e4186a1bf5 | |||
8c664d7774 | |||
744df2e2dc | |||
b4f9b11143 | |||
18b07d2f46 | |||
9d0f810ab3 | |||
cf3f17dfef | |||
6be1134c8c | |||
6dad7bc9dd | |||
231f19a30f | |||
9c105d6df6 | |||
179ceb0ca0 | |||
680661f07c | |||
c54d2a2da8 | |||
85f0fca2ae | |||
420e63b724 | |||
5d9b4fd19a | |||
b3d68ef562 | |||
baae737d6b | |||
03f8b327c5 | |||
b9a1ce5ab5 | |||
1b650bd733 | |||
b867250580 | |||
2c7a1e27be | |||
0e455f8ffc | |||
8005bc1318 | |||
11e978121f | |||
727ca99b73 | |||
97080d7380 | |||
1a3a53a229 | |||
a926de8466 | |||
5eabb39ec8 | |||
03313cb092 | |||
d58cfa668d | |||
e3e40dd083 | |||
facdd2d1b4 | |||
7d6bd39f29 | |||
608932300f | |||
b5c1c92b04 | |||
c68b129da8 | |||
963c593a1f | |||
a299e22f88 | |||
2007bfd7c5 | |||
4cae045d0d | |||
1fa8b8f06e | |||
4095184b27 | |||
857d56a878 | |||
8087f754b0 | |||
1479d96162 | |||
606e220a9f | |||
6988565484 | |||
fbc4a3dcdb | |||
924d5bdd13 | |||
25a2fd24f3 | |||
64f5ac45dd | |||
937ce939d1 | |||
7d89364104 | |||
f7b8c235d8 | |||
89d83efca4 | |||
ab97e27f27 | |||
ee3e1b55cb | |||
5e109e2a39 | |||
a8e50c993a | |||
ba8af589a7 | |||
301340327a | |||
1e4c58c79e | |||
f87cd063ee | |||
9593298389 | |||
ad4651844d | |||
3748c459f8 | |||
50ea3ecdab | |||
8910390f7b | |||
d820490b2b | |||
2c96caee4f | |||
84939a7d32 | |||
1e3fc2b0f8 | |||
7c8b5f3e82 | |||
570d315bf5 | |||
7871b34c60 | |||
85d64f23eb | |||
bdb6dd0d54 | |||
faf887163a | |||
dd5baa530d | |||
d9947e29cf | |||
1888521762 | |||
48fef2313b | |||
0a99d2c430 | |||
ed6f592606 | |||
46ebfaec01 | |||
1384f50406 | |||
d71f4e7b59 | |||
58929290ab | |||
63146106c0 | |||
7c5602f204 | |||
e711e124ce | |||
e1b39c42ec | |||
d81c889426 | |||
afb0edacf6 | |||
6d7d568746 | |||
8df5b6eb76 | |||
270d12821a | |||
9c244e980c | |||
25ba54c8ac | |||
9c4bef864b | |||
bdc566e30f | |||
a653b61b51 | |||
7b61f71fa9 | |||
42539cc73d | |||
864369860c | |||
89938b7f93 | |||
f076661d4a | |||
0b018f53a7 | |||
32a13f0f3c | |||
f203bf9534 | |||
67b6339ffc | |||
6233da0044 | |||
56f455a08e | |||
9d541cc46a | |||
adf86d369b | |||
bbcb84ed03 | |||
6f40c774ea | |||
e3b70df46f | |||
584262a59b | |||
7e4e613405 | |||
e2c60bfd5a | |||
e38c402266 | |||
a299d5dc55 | |||
43c0f0ab1e | |||
ed17e7f16e | |||
7d723530e6 | |||
a3532a5940 | |||
74cbf3f2c8 | |||
d402744955 | |||
39b65b3884 | |||
c62ff2efc1 | |||
08f55c9680 | |||
1afc50ea6a | |||
7c259f455c | |||
be854a79b8 | |||
a71890ab68 | |||
dc2b6b9851 | |||
d091aabeb3 | |||
c261937cf5 | |||
4aa1309797 | |||
4f8517a50c | |||
446e049e6e | |||
7db2ba322c | |||
70fb732051 | |||
5128aa83f9 |
@ -1,4 +1,4 @@
|
|||||||
VITE_NAME=New Quest
|
VITE_NAME=Noxious
|
||||||
VITE_DEVELOPMENT=true
|
VITE_DEVELOPMENT=true
|
||||||
VITE_SERVER_ENDPOINT=http://localhost:4000
|
VITE_SERVER_ENDPOINT=http://localhost:4000
|
||||||
VITE_TILE_SIZE_X=64
|
VITE_TILE_SIZE_X=64
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/favicon.ico">
|
<link rel="icon" href="/favicon.ico">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<title>Sylvan Quest - Play</title>
|
<title>Noxious - Play</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
1637
package-lock.json
generated
@ -50,6 +50,7 @@
|
|||||||
"tailwindcss": "^3.4.13",
|
"tailwindcss": "^3.4.13",
|
||||||
"typescript": "~5.6.2",
|
"typescript": "~5.6.2",
|
||||||
"vite": "^5.4.9",
|
"vite": "^5.4.9",
|
||||||
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vite-plugin-vue-devtools": "^7.5.2",
|
"vite-plugin-vue-devtools": "^7.5.2",
|
||||||
"vitest": "^2.0.3",
|
"vitest": "^2.0.3",
|
||||||
"vue-tsc": "^1.6.5"
|
"vue-tsc": "^1.6.5"
|
||||||
|
Before Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 162 KiB |
@ -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 |
Before Width: | Height: | Size: 597 KiB After Width: | Height: | Size: 597 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 600 KiB After Width: | Height: | Size: 600 KiB |
Before Width: | Height: | Size: 600 KiB After Width: | Height: | Size: 600 KiB |
Before Width: | Height: | Size: 599 KiB After Width: | Height: | Size: 599 KiB |
10
public/assets/icons/male-icon.svg
Normal 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 |
@ -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 |
Before Width: | Height: | Size: 768 B After Width: | Height: | Size: 768 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 375 B After Width: | Height: | Size: 375 B |
Before Width: | Height: | Size: 652 B After Width: | Height: | Size: 652 B |
Before Width: | Height: | Size: 346 B After Width: | Height: | Size: 346 B |
@ -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 |
Before Width: | Height: | Size: 2.1 KiB |
3
public/assets/icons/triangle-icon.svg
Normal 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 |
3
public/assets/icons/x-button-gray.svg
Normal 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 |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 274 B |
@ -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("#fill-color-gradient-render-1-0");"/></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 |
@ -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 |
@ -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 |
@ -1 +0,0 @@
|
|||||||
<svg width="1508.086" xmlns="http://www.w3.org/2000/svg" height="1511.251" id="screenshot-0d120e2a-8725-8061-8004-79728483f7ea" viewBox="-201.784 -208.012 1508.086 1511.251" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-0d120e2a-8725-8061-8004-79728483f7ea" width="800px" height="800px" rx="0" ry="0" style="opacity: 0.3; fill: rgb(0, 0, 0);"><g id="shape-0d120e2a-8725-8061-8004-79728484b3fe"><g class="fills" id="fills-0d120e2a-8725-8061-8004-79728484b3fe"><path d="M1190.359,745.690L1043.367,575.945L1133.504,630.603C1099.180,585.722,1047.978,532.622,975.722,469.519L780.783,401.898L896.468,404.538C851.234,371.382,797.068,339.090,738.130,311.073L601.350,337.338L689.349,289.039C627.425,263.088,562.143,241.922,497.713,229.160C430.172,215.674,363.453,211.534,303.512,221.641L314.753,151.382C271.664,177.012,239.130,209.992,214.226,251.017L204.390,177.710C166.181,212.950,148.095,250.172,143.131,301.343C69.092,307.974,-2.300,327.925,-73.861,347.005L-63.628,384.898C361.675,238.903,753.109,407.667,987.467,615.054L960.305,643.506C749.259,458.743,490.332,358.712,193.406,380.541C209.110,415.226,228.858,447.126,251.288,474.785L371.998,430.671L277.417,504.449C294.635,521.771,312.591,536.670,331.111,548.765L396.081,470.998L358.366,564.253C377.625,573.924,397.183,580.217,416.674,582.837C534.164,599.232,652.310,618.566,782.785,703.535L773.618,601.955L831.163,737.792C852.261,754.489,876.370,771.191,897.168,782.701L861.169,684.190L960.409,811.519C976.589,817.512,992.991,822.477,1008.953,826.486C1066.083,840.287,1120.015,842.594,1157.256,819.002C1175.975,807.393,1189.205,786.963,1190.791,762.853C1191.080,756.933,1190.907,750.762,1190.359,745.690ZZ" style="fill: rgb(13 109 105);"/></g></g></g></svg>
|
|
Before Width: | Height: | Size: 1.7 KiB |
BIN
public/assets/tlogo.png
Normal file
After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 470 KiB After Width: | Height: | Size: 470 KiB |
Before Width: | Height: | Size: 471 KiB After Width: | Height: | Size: 471 KiB |
Before Width: | Height: | Size: 598 KiB After Width: | Height: | Size: 598 KiB |
25
public/assets/ui-elements/character-select-ui-shape.svg
Normal 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 |
Before Width: | Height: | Size: 470 KiB After Width: | Height: | Size: 470 KiB |
23
public/assets/ui-elements/login-ui-box-inner.svg
Normal file
After Width: | Height: | Size: 302 KiB |
23
public/assets/ui-elements/login-ui-box-outer.svg
Normal file
After Width: | Height: | Size: 400 KiB |
Before Width: | Height: | Size: 301 KiB |
Before Width: | Height: | Size: 400 KiB |
Before Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 2.1 MiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 1.1 MiB |
46
src/App.vue
@ -1,8 +1,7 @@
|
|||||||
<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>
|
||||||
|
|
||||||
@ -10,24 +9,51 @@
|
|||||||
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 '@/screens/Login.vue'
|
import Login from '@/components/screens/Login.vue'
|
||||||
import Characters from '@/screens/Characters.vue'
|
import Characters from '@/components/screens/Characters.vue'
|
||||||
import Game from '@/screens/Game.vue'
|
import Game from '@/components/screens/Game.vue'
|
||||||
// import Loading from '@/screens/Loading.vue'
|
import ZoneEditor from '@/components/screens/ZoneEditor.vue'
|
||||||
import ZoneEditor from '@/screens/ZoneEditor.vue'
|
import { computed, watch } from 'vue'
|
||||||
import { computed } from 'vue'
|
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
|
|
||||||
const currentScreen = computed(() => {
|
const currentScreen = computed(() => {
|
||||||
// if (!gameStore.isAssetsLoaded) return Loading
|
|
||||||
if (!gameStore.connection) return Login
|
if (!gameStore.connection) return Login
|
||||||
if (!gameStore.token) return Login
|
if (!gameStore.token) return Login
|
||||||
if (!gameStore.character) return Characters
|
if (!gameStore.character) return Characters
|
||||||
if (zoneEditorStore.active) return ZoneEditor
|
if (zoneEditorStore.active) return ZoneEditor
|
||||||
return Game
|
return Game
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Watch zoneEditorStore.active and empty gameStore.game.loadedAssets
|
||||||
|
watch(
|
||||||
|
() => zoneEditorStore.active,
|
||||||
|
() => {
|
||||||
|
gameStore.game.loadedAssets = []
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// #209: Play sound when a button is pressed
|
||||||
|
addEventListener('click', (event) => {
|
||||||
|
if (!(event.target instanceof HTMLButtonElement)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const audio = new Audio('/assets/music/click-btn.mp3')
|
||||||
|
audio.play()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Watch for "G" key press and toggle the gm panel
|
||||||
|
addEventListener('keydown', (event) => {
|
||||||
|
if (gameStore.character?.role !== 'gm') return // Only allow toggling the gm panel if the character is a gm
|
||||||
|
|
||||||
|
// Check if no input is active or focus is on an input
|
||||||
|
if (event.repeat || event.isComposing || event.defaultPrevented || document.activeElement?.tagName.toUpperCase() === 'INPUT' || document.activeElement?.tagName.toUpperCase() === 'TEXTAREA') return
|
||||||
|
|
||||||
|
if (event.key === 'G') {
|
||||||
|
gameStore.toggleGmPanel()
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -4,14 +4,24 @@ export type Notification = {
|
|||||||
message?: string
|
message?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AssetT = {
|
export type HttpResponse<T> = {
|
||||||
|
success: boolean
|
||||||
|
message?: string
|
||||||
|
data?: T
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AssetDataT = {
|
||||||
key: string
|
key: string
|
||||||
url: string
|
data: string
|
||||||
group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
|
group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
frameCount?: number
|
originX?: number
|
||||||
|
originY?: number
|
||||||
|
isAnimated?: boolean
|
||||||
|
frameRate?: number
|
||||||
frameWidth?: number
|
frameWidth?: number
|
||||||
frameHeight?: number
|
frameHeight?: number
|
||||||
|
frameCount?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Tile = {
|
export type Tile = {
|
||||||
@ -29,7 +39,7 @@ export type Object = {
|
|||||||
originX: number
|
originX: number
|
||||||
originY: number
|
originY: number
|
||||||
isAnimated: boolean
|
isAnimated: boolean
|
||||||
frameSpeed: number
|
frameRate: number
|
||||||
frameWidth: number
|
frameWidth: number
|
||||||
frameHeight: number
|
frameHeight: number
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
@ -41,12 +51,18 @@ export type Item = {
|
|||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
description: string | null
|
description: string | null
|
||||||
|
itemType: ItemType
|
||||||
stackable: boolean
|
stackable: boolean
|
||||||
|
rarity: ItemRarity
|
||||||
|
spriteId: string | null
|
||||||
|
sprite?: Sprite
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
characters: CharacterItem[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ItemType = 'WEAPON' | 'HELMET' | 'CHEST' | 'LEGS' | 'BOOTS' | 'GLOVES' | 'RING' | 'NECKLACE'
|
||||||
|
export type ItemRarity = 'COMMON' | 'UNCOMMON' | 'RARE' | 'EPIC' | 'LEGENDARY'
|
||||||
|
|
||||||
export type Zone = {
|
export type Zone = {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
@ -136,6 +152,7 @@ export type CharacterType = {
|
|||||||
name: string
|
name: string
|
||||||
gender: CharacterGender
|
gender: CharacterGender
|
||||||
race: CharacterRace
|
race: CharacterRace
|
||||||
|
isSelectable: boolean
|
||||||
characters: Character[]
|
characters: Character[]
|
||||||
spriteId?: string
|
spriteId?: string
|
||||||
sprite?: Sprite
|
sprite?: Sprite
|
||||||
@ -143,6 +160,14 @@ export type CharacterType = {
|
|||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CharacterHair = {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
sprite: Sprite
|
||||||
|
gender: CharacterGender
|
||||||
|
isSelectable: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export type Character = {
|
export type Character = {
|
||||||
id: number
|
id: number
|
||||||
userId: number
|
userId: number
|
||||||
@ -157,15 +182,19 @@ export type Character = {
|
|||||||
positionX: number
|
positionX: number
|
||||||
positionY: number
|
positionY: number
|
||||||
rotation: number
|
rotation: number
|
||||||
zoneId: number
|
|
||||||
zone: Zone
|
|
||||||
characterTypeId: number | null
|
characterTypeId: number | null
|
||||||
characterType: CharacterType | null
|
characterType: CharacterType | null
|
||||||
|
characterHairId: number | null
|
||||||
|
characterHair: CharacterHair | null
|
||||||
|
zoneId: number
|
||||||
|
zone: Zone
|
||||||
chats: Chat[]
|
chats: Chat[]
|
||||||
items: CharacterItem[]
|
items: CharacterItem[]
|
||||||
|
equipment: CharacterEquipment[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ExtendedCharacter = Character & {
|
export type ZoneCharacter = {
|
||||||
|
character: Character
|
||||||
isMoving?: boolean
|
isMoving?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,6 +207,22 @@ export type CharacterItem = {
|
|||||||
quantity: number
|
quantity: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CharacterEquipment = {
|
||||||
|
id: number
|
||||||
|
slot: CharacterEquipmentSlotType
|
||||||
|
characterItemId: number
|
||||||
|
characterItem: CharacterItem
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CharacterEquipmentSlotType {
|
||||||
|
HEAD = 'HEAD',
|
||||||
|
BODY = 'BODY',
|
||||||
|
ARMS = 'ARMS',
|
||||||
|
LEGS = 'LEGS',
|
||||||
|
NECK = 'NECK',
|
||||||
|
RING = 'RING'
|
||||||
|
}
|
||||||
|
|
||||||
export type Sprite = {
|
export type Sprite = {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
@ -199,7 +244,7 @@ export type SpriteAction = {
|
|||||||
isLooping: boolean
|
isLooping: boolean
|
||||||
frameWidth: number
|
frameWidth: number
|
||||||
frameHeight: number
|
frameHeight: number
|
||||||
frameSpeed: number
|
frameRate: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Chat = {
|
export type Chat = {
|
||||||
@ -212,14 +257,21 @@ 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
|
||||||
|
}
|
||||||
|
|
||||||
|
export type zoneLoadData = {
|
||||||
|
zone: ZoneT
|
||||||
|
characters: ZoneCharacter[]
|
||||||
|
}
|
25
src/application/utilities.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
export function uuidv4() {
|
||||||
|
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) => (+c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))).toString(16))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unduplicateArray(array: any[]) {
|
||||||
|
return [...new Set(array.flat())]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDomain() {
|
||||||
|
// Check if not localhost
|
||||||
|
if (window.location.hostname !== 'localhost') {
|
||||||
|
return window.location.hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if not IP address
|
||||||
|
if (window.location.hostname.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) {
|
||||||
|
return window.location.hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.location.hostname.split('.').length < 3) {
|
||||||
|
return window.location.hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.location.hostname.split('.').slice(-2).join('.')
|
||||||
|
}
|
@ -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,
|
||||||
@ -45,7 +53,7 @@ label {
|
|||||||
|
|
||||||
button,
|
button,
|
||||||
a {
|
a {
|
||||||
@apply font-medium drop-shadow-20;
|
@apply font-medium;
|
||||||
}
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
@ -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 {
|
||||||
@ -99,11 +114,11 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.btn-red {
|
&.btn-red {
|
||||||
@apply bg-red text-gray-50 text-base leading-5 rounded py-2.5;
|
@apply bg-red-300 text-gray-50 text-base leading-5 rounded py-2.5;
|
||||||
|
|
||||||
&.active,
|
&.active,
|
||||||
&:hover {
|
&:hover {
|
||||||
@apply bg-red-300;
|
@apply bg-red-400;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,8 +126,9 @@ button {
|
|||||||
@apply text-gray-50 border-2 border-solid border-gray-500 text-base leading-5 rounded py-2.5;
|
@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 border-gray;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,6 +139,28 @@ 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 bg-gray bg-none
|
||||||
|
}
|
||||||
|
|
||||||
|
.hair-deselect:has(:checked) {
|
||||||
|
img {
|
||||||
|
@apply brightness-200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.default-border {
|
||||||
|
@apply border border-solid border-gray-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-element {
|
||||||
|
@apply absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-pixel {
|
.text-pixel {
|
||||||
@ -133,6 +171,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;
|
||||||
|
@ -1,110 +1,179 @@
|
|||||||
<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 '@/application/types'
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const LIGHT_CONFIG = {
|
||||||
|
SUNRISE_HOUR: 6,
|
||||||
|
SUNSET_HOUR: 20,
|
||||||
|
DAY_STRENGTH: 100,
|
||||||
|
NIGHT_STRENGTH: 30
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stores and refs
|
||||||
|
const gameStore = useGameStore()
|
||||||
const zoneStore = useZoneStore()
|
const zoneStore = useZoneStore()
|
||||||
|
|
||||||
const sceneRef = ref<Phaser.Scene | null>(null)
|
const sceneRef = ref<Phaser.Scene | null>(null)
|
||||||
|
const zoneEffectsReady = ref(false)
|
||||||
|
|
||||||
// Effect-related refs
|
// Effect objects
|
||||||
const lightEffect = ref<Phaser.GameObjects.Graphics | null>(null)
|
const effects = {
|
||||||
const rainEmitter = ref<Phaser.GameObjects.Particles.ParticleEmitter | null>(null)
|
light: ref<Phaser.GameObjects.Graphics | null>(null),
|
||||||
const fogSprite = ref<Phaser.GameObjects.Sprite | null>(null)
|
rain: ref<Phaser.GameObjects.Particles.ParticleEmitter | null>(null),
|
||||||
|
fog: ref<Phaser.GameObjects.Sprite | null>(null)
|
||||||
|
}
|
||||||
|
|
||||||
const preloadScene = async (scene: Phaser.Scene) => {
|
// Weather state
|
||||||
|
const weatherState = ref<WeatherState>({
|
||||||
|
isRainEnabled: false,
|
||||||
|
rainPercentage: 0,
|
||||||
|
isFogEnabled: false,
|
||||||
|
fogDensity: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// Scene setup
|
||||||
|
const preloadScene = (scene: Phaser.Scene) => {
|
||||||
scene.load.image('raindrop', 'assets/raindrop.png')
|
scene.load.image('raindrop', 'assets/raindrop.png')
|
||||||
scene.load.image('fog', 'assets/fog.png')
|
scene.load.image('fog', 'assets/fog.png')
|
||||||
}
|
}
|
||||||
|
|
||||||
const createScene = async (scene: Phaser.Scene) => {
|
const createScene = (scene: Phaser.Scene) => {
|
||||||
sceneRef.value = scene
|
sceneRef.value = scene
|
||||||
createLightEffect(scene)
|
initializeEffects(scene)
|
||||||
createRainEffect(scene)
|
setupSocketListeners()
|
||||||
createFogEffect(scene)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const initializeEffects = (scene: Phaser.Scene) => {
|
||||||
|
// Light
|
||||||
|
effects.light.value = scene.add.graphics().setDepth(1000)
|
||||||
|
|
||||||
|
// Rain
|
||||||
|
effects.rain.value = scene.add
|
||||||
|
.particles(0, 0, 'raindrop', {
|
||||||
|
x: { min: 0, max: window.innerWidth },
|
||||||
|
y: -50,
|
||||||
|
quantity: 5,
|
||||||
|
lifespan: 2000,
|
||||||
|
speedY: { min: 300, max: 500 },
|
||||||
|
scale: { start: 0.005, end: 0.005 },
|
||||||
|
alpha: { start: 0.5, end: 0 },
|
||||||
|
blendMode: 'ADD'
|
||||||
|
})
|
||||||
|
.setDepth(900)
|
||||||
|
effects.rain.value.stop()
|
||||||
|
|
||||||
|
// Fog
|
||||||
|
effects.fog.value = scene.add
|
||||||
|
.sprite(window.innerWidth / 2, window.innerHeight / 2, 'fog')
|
||||||
|
.setScale(2)
|
||||||
|
.setAlpha(0)
|
||||||
|
.setDepth(950)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Effect updates
|
||||||
const updateScene = () => {
|
const updateScene = () => {
|
||||||
updateEffects()
|
const timeBasedLight = calculateLightStrength(gameStore.world.date)
|
||||||
}
|
const zoneEffects = zoneStore.zone?.zoneEffects?.reduce(
|
||||||
|
(acc, curr) => ({
|
||||||
|
...acc,
|
||||||
|
[curr.effect]: curr.strength
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
) as { [key: string]: number }
|
||||||
|
|
||||||
const createLightEffect = (scene: Phaser.Scene) => {
|
// Only update effects once zoneEffects are loaded
|
||||||
lightEffect.value = scene.add.graphics()
|
if (!zoneEffectsReady.value) {
|
||||||
lightEffect.value.setDepth(1000)
|
if (zoneEffects && Object.keys(zoneEffects).length) {
|
||||||
}
|
zoneEffectsReady.value = true
|
||||||
|
} else {
|
||||||
const createRainEffect = (scene: Phaser.Scene) => {
|
return
|
||||||
rainEmitter.value = scene.add.particles(0, 0, 'raindrop', {
|
|
||||||
x: { min: 0, max: window.innerWidth },
|
|
||||||
y: -50,
|
|
||||||
quantity: 5,
|
|
||||||
lifespan: 2000,
|
|
||||||
speedY: { min: 300, max: 500 },
|
|
||||||
scale: { start: 0.005, end: 0.005 },
|
|
||||||
alpha: { start: 0.5, end: 0 },
|
|
||||||
blendMode: 'ADD'
|
|
||||||
})
|
|
||||||
rainEmitter.value.setDepth(900)
|
|
||||||
rainEmitter.value.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
const createFogEffect = (scene: Phaser.Scene) => {
|
|
||||||
fogSprite.value = scene.add.sprite(window.innerWidth / 2, window.innerHeight / 2, 'fog')
|
|
||||||
fogSprite.value.setScale(2)
|
|
||||||
fogSprite.value.setAlpha(0)
|
|
||||||
fogSprite.value.setDepth(950)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateEffects = () => {
|
|
||||||
const effects = zoneStore.zone?.zoneEffects || []
|
|
||||||
|
|
||||||
effects.forEach((effect) => {
|
|
||||||
switch (effect.effect) {
|
|
||||||
case 'light':
|
|
||||||
updateLightEffect(effect.strength)
|
|
||||||
break
|
|
||||||
case 'rain':
|
|
||||||
updateRainEffect(effect.strength)
|
|
||||||
break
|
|
||||||
case 'fog':
|
|
||||||
updateFogEffect(effect.strength)
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
|
const finalEffects =
|
||||||
|
zoneEffects && Object.keys(zoneEffects).length
|
||||||
|
? zoneEffects
|
||||||
|
: {
|
||||||
|
light: timeBasedLight,
|
||||||
|
rain: weatherState.value.isRainEnabled ? weatherState.value.rainPercentage : 0,
|
||||||
|
fog: weatherState.value.isFogEnabled ? weatherState.value.fogDensity * 100 : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
applyEffects(finalEffects)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateLightEffect = (strength: number) => {
|
const applyEffects = (effectValues: any) => {
|
||||||
if (!lightEffect.value) return
|
if (effects.light.value) {
|
||||||
const darkness = 1 - strength / 100
|
const darkness = 1 - (effectValues.light ?? 100) / 100
|
||||||
lightEffect.value.clear()
|
effects.light.value.clear().fillStyle(0x000000, darkness).fillRect(0, 0, window.innerWidth, window.innerHeight)
|
||||||
lightEffect.value.fillStyle(0x000000, darkness)
|
}
|
||||||
lightEffect.value.fillRect(0, 0, window.innerWidth, window.innerHeight)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateRainEffect = (strength: number) => {
|
if (effects.rain.value) {
|
||||||
if (!rainEmitter.value) return
|
const strength = effectValues.rain ?? 0
|
||||||
if (strength > 0) {
|
strength > 0 ? effects.rain.value.start().setQuantity(Math.floor((strength / 100) * 10)) : effects.rain.value.stop()
|
||||||
rainEmitter.value.start()
|
}
|
||||||
rainEmitter.value.setQuantity(Math.floor((strength / 100) * 10))
|
|
||||||
} else {
|
if (effects.fog.value) {
|
||||||
rainEmitter.value.stop()
|
effects.fog.value.setAlpha((effectValues.fog ?? 0) / 100)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateFogEffect = (strength: number) => {
|
const calculateLightStrength = (time: Date): number => {
|
||||||
if (!fogSprite.value) return
|
const hour = time.getHours()
|
||||||
fogSprite.value.setAlpha(strength / 100)
|
const minute = time.getMinutes()
|
||||||
|
|
||||||
|
if (hour >= LIGHT_CONFIG.SUNSET_HOUR || hour < LIGHT_CONFIG.SUNRISE_HOUR) return LIGHT_CONFIG.NIGHT_STRENGTH
|
||||||
|
|
||||||
|
if (hour > LIGHT_CONFIG.SUNRISE_HOUR && hour < LIGHT_CONFIG.SUNSET_HOUR - 2) return LIGHT_CONFIG.DAY_STRENGTH
|
||||||
|
|
||||||
|
if (hour === LIGHT_CONFIG.SUNRISE_HOUR) return LIGHT_CONFIG.NIGHT_STRENGTH + ((LIGHT_CONFIG.DAY_STRENGTH - LIGHT_CONFIG.NIGHT_STRENGTH) * minute) / 60
|
||||||
|
|
||||||
|
const totalMinutes = (hour - (LIGHT_CONFIG.SUNSET_HOUR - 2)) * 60 + minute
|
||||||
|
return LIGHT_CONFIG.DAY_STRENGTH - (LIGHT_CONFIG.DAY_STRENGTH - LIGHT_CONFIG.NIGHT_STRENGTH) * (totalMinutes / 120)
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => zoneStore.zone?.zoneEffects, updateEffects, { deep: true })
|
// Socket and window handlers
|
||||||
|
const setupSocketListeners = () => {
|
||||||
|
gameStore.connection?.emit('weather', (response: WeatherState) => {
|
||||||
|
weatherState.value = response
|
||||||
|
updateScene()
|
||||||
|
})
|
||||||
|
|
||||||
|
gameStore.connection?.on('weather', (data: WeatherState) => {
|
||||||
|
weatherState.value = data
|
||||||
|
updateScene()
|
||||||
|
})
|
||||||
|
|
||||||
|
gameStore.connection?.on('date', updateScene)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
if (effects.rain.value) effects.rain.value.updateConfig({ x: { min: 0, max: window.innerWidth } })
|
||||||
|
if (effects.fog.value) effects.fog.value.setPosition(window.innerWidth / 2, window.innerHeight / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
watch(
|
||||||
|
() => zoneStore.zone,
|
||||||
|
() => {
|
||||||
|
zoneEffectsReady.value = false
|
||||||
|
updateScene()
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(() => window.addEventListener('resize', handleResize))
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
if (sceneRef.value) sceneRef.value.scene.remove('effects')
|
if (sceneRef.value) sceneRef.value.scene.remove('effects')
|
||||||
|
gameStore.connection?.off('weather')
|
||||||
})
|
})
|
||||||
|
|
||||||
// @TODO : Fix resize issue
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,29 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- Chat bubble -->
|
<ChatBubble :zoneCharacter="props.zoneCharacter" :currentX="currentX" :currentY="currentY" />
|
||||||
<Container ref="charChatContainer" :depth="999" :x="currentX" :y="currentY">
|
<Healthbar :zoneCharacter="props.zoneCharacter" :currentX="currentX" :currentY="currentY" />
|
||||||
<RoundRectangle @create="createChatBubble" :origin-x="0.5" :origin-y="7.5" :fillColor="0xffffff" :width="194" :height="21" :radius="20" />
|
|
||||||
<Text @create="createChatText" :style="{ fontSize: 13, fontFamily: 'Arial', color: '#000' }" />
|
|
||||||
</Container>
|
|
||||||
<!-- Character name and health -->
|
|
||||||
<Container :depth="999" :x="currentX" :y="currentY">
|
|
||||||
<Text @create="createNicknameText" :text="character.name" />
|
|
||||||
<RoundRectangle :origin-x="0.5" :origin-y="18.5" :fillColor="0xffffff" :width="74" :height="6" :radius="5" />
|
|
||||||
<RoundRectangle :origin-x="0.5" :origin-y="36.4" :fillColor="0x00b3b3" :width="70" :height="3" :radius="5" />
|
|
||||||
</Container>
|
|
||||||
<!-- Character sprite -->
|
|
||||||
<Container ref="charContainer" :depth="isometricDepth" :x="currentX" :y="currentY">
|
<Container ref="charContainer" :depth="isometricDepth" :x="currentX" :y="currentY">
|
||||||
<Sprite ref="charSprite" :origin-y="1" :flipX="isFlippedX" :flipY="false" />
|
<CharacterHair :zoneCharacter="props.zoneCharacter" :currentX="currentX" :currentY="currentY" />
|
||||||
|
<!-- <CharacterChest :zoneCharacter="props.zoneCharacter" :currentX="currentX" :currentY="currentY" />-->
|
||||||
|
<Sprite ref="charSprite" :origin-y="1" :flipX="isFlippedX" />
|
||||||
</Container>
|
</Container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import config from '@/config'
|
import config from '@/application/config'
|
||||||
import { type ExtendedCharacter } from '@/types'
|
import { type Sprite as SpriteT, type ZoneCharacter } from '@/application/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'
|
||||||
import { Container, refObj, RoundRectangle, Sprite, Text, useGame, useScene } from 'phavuer'
|
import { Container, refObj, Sprite, useScene } from 'phavuer'
|
||||||
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||||
|
import { loadSpriteTextures } from '@/composables/gameComposable'
|
||||||
|
import ChatBubble from '@/components/game/character/partials/ChatBubble.vue'
|
||||||
|
import Healthbar from '@/components/game/character/partials/Healthbar.vue'
|
||||||
|
import CharacterHair from '@/components/game/character/partials/CharacterHair.vue'
|
||||||
|
// import CharacterChest from '@/components/game/character/partials/CharacterChest.vue'
|
||||||
|
|
||||||
enum Direction {
|
enum Direction {
|
||||||
POSITIVE,
|
POSITIVE,
|
||||||
@ -33,14 +30,12 @@ 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 charContainer = refObj<Phaser.GameObjects.Container>()
|
const charContainer = refObj<Phaser.GameObjects.Container>()
|
||||||
const charSprite = refObj<Phaser.GameObjects.Sprite>()
|
const charSprite = refObj<Phaser.GameObjects.Sprite>()
|
||||||
|
|
||||||
const game = useGame()
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const zoneStore = useZoneStore()
|
const zoneStore = useZoneStore()
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
@ -78,7 +73,7 @@ const updatePosition = (x: number, y: number, direction: Direction) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const duration = distance * 6
|
const duration = distance * 5.7
|
||||||
|
|
||||||
tween.value = props.layer.scene.tweens.add({
|
tween.value = props.layer.scene.tweens.add({
|
||||||
targets: { x: currentX.value, y: currentY.value },
|
targets: { x: currentX.value, y: currentY.value },
|
||||||
@ -109,19 +104,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
|
||||||
}
|
}
|
||||||
@ -131,43 +126,8 @@ const updateSprite = () => {
|
|||||||
charSprite.value!.setTexture(charTexture.value)
|
charSprite.value!.setTexture(charTexture.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const createChatBubble = (container: Phaser.GameObjects.Container) => {
|
|
||||||
container.setName(`${props.character.name}_chatBubble`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const createChatText = (text: Phaser.GameObjects.Text) => {
|
|
||||||
text.setName(`${props.character.name}_chatText`)
|
|
||||||
text.setFontSize(13)
|
|
||||||
text.setFontFamily('Arial')
|
|
||||||
text.setOrigin(0.5, 10.9)
|
|
||||||
|
|
||||||
// Fix text alignment on Windows and Android
|
|
||||||
if (game.device.os.windows || game.device.os.android) {
|
|
||||||
text.setOrigin(0.5, 9.75)
|
|
||||||
|
|
||||||
if (game.device.browser.firefox) {
|
|
||||||
text.setOrigin(0.5, 10.9)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const createNicknameText = (text: Phaser.GameObjects.Text) => {
|
|
||||||
text.setFontSize(13)
|
|
||||||
text.setFontFamily('Arial')
|
|
||||||
text.setOrigin(0.5, 9)
|
|
||||||
|
|
||||||
// Fix text alignment on Windows and Android
|
|
||||||
if (game.device.os.windows || game.device.os.android) {
|
|
||||||
text.setOrigin(0.5, 8)
|
|
||||||
|
|
||||||
if (game.device.browser.firefox) {
|
|
||||||
text.setOrigin(0.5, 9)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.character,
|
() => props.zoneCharacter.character,
|
||||||
(newChar, oldChar) => {
|
(newChar, oldChar) => {
|
||||||
if (!newChar) return
|
if (!newChar) return
|
||||||
|
|
||||||
@ -178,27 +138,29 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(() => props.character.isMoving, updateSprite)
|
watch(() => props.zoneCharacter, updateSprite)
|
||||||
watch(() => props.character.rotation, updateSprite)
|
|
||||||
|
loadSpriteTextures(scene, props.zoneCharacter.character.characterType?.sprite as SpriteT)
|
||||||
|
.then(() => {
|
||||||
|
charSprite.value!.setTexture(charTexture.value)
|
||||||
|
charSprite.value!.setFlipX(isFlippedX.value)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error loading texture:', error)
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
charChatContainer.value!.setName(`${props.character!.name}_chatContainer`)
|
charContainer.value!.setName(props.zoneCharacter.character!.name)
|
||||||
charChatContainer.value!.setVisible(false)
|
|
||||||
charContainer.value!.setName(props.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
|
||||||
scene.cameras.main.startFollow(charContainer.value as Phaser.GameObjects.Container)
|
// scene.cameras.main.startFollow(charContainer.value as Phaser.GameObjects.Container)
|
||||||
scene.cameras.main.stopFollow()
|
// scene.cameras.main.stopFollow()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set sprite
|
updatePosition(props.zoneCharacter.character.positionX, props.zoneCharacter.character.positionY, props.zoneCharacter.character.rotation)
|
||||||
charSprite.value!.setTexture(charTexture.value)
|
|
||||||
charSprite.value!.setFlipX(isFlippedX.value)
|
|
||||||
|
|
||||||
updatePosition(props.character.positionX, props.character.positionY, props.character.rotation)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
51
src/components/game/character/partials/CharacterChest.vue
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<Image v-bind="imageProps" v-if="gameStore.getLoadedAsset(texture)" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { Image, useScene } from 'phavuer'
|
||||||
|
import type { Sprite as SpriteT, ZoneCharacter } from '@/application/types'
|
||||||
|
import { loadSpriteTextures } from '@/composables/gameComposable'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
zoneCharacter: ZoneCharacter
|
||||||
|
currentX: number
|
||||||
|
currentY: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const scene = useScene()
|
||||||
|
|
||||||
|
const texture = computed(() => {
|
||||||
|
const { rotation, characterHair } = props.zoneCharacter.character
|
||||||
|
const spriteId = characterHair?.sprite?.id
|
||||||
|
const direction = [0, 6].includes(rotation) ? 'back' : 'front'
|
||||||
|
|
||||||
|
return `${spriteId}-${direction}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const isFlippedX = computed(() => [6, 4].includes(props.zoneCharacter.character.rotation ?? 0))
|
||||||
|
|
||||||
|
const imageProps = computed(() => {
|
||||||
|
// Get the current sprite action based on direction
|
||||||
|
const direction = [0, 6].includes(props.zoneCharacter.character.rotation ?? 0) ? 'back' : 'front'
|
||||||
|
const spriteAction = props.zoneCharacter.character.characterHair?.sprite?.spriteActions?.find((spriteAction) => spriteAction.action === direction)
|
||||||
|
|
||||||
|
return {
|
||||||
|
depth: 1,
|
||||||
|
originX: Number(spriteAction?.originX) ?? 0,
|
||||||
|
originY: Number(spriteAction?.originY) ?? 0,
|
||||||
|
flipX: isFlippedX.value,
|
||||||
|
texture: texture.value
|
||||||
|
// y: props.zoneCharacter.isMoving ? Math.floor(Date.now() / 250) % 2 : 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
loadSpriteTextures(scene, props.zoneCharacter.character.characterHair?.sprite as SpriteT)
|
||||||
|
.then(() => {})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error loading texture:', error)
|
||||||
|
})
|
||||||
|
</script>
|
51
src/components/game/character/partials/CharacterHair.vue
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<Image v-bind="imageProps" v-if="gameStore.getLoadedAsset(texture)" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { Image, useScene } from 'phavuer'
|
||||||
|
import type { Sprite as SpriteT, ZoneCharacter } from '@/application/types'
|
||||||
|
import { loadSpriteTextures } from '@/composables/gameComposable'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
zoneCharacter: ZoneCharacter
|
||||||
|
currentX: number
|
||||||
|
currentY: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const scene = useScene()
|
||||||
|
|
||||||
|
const texture = computed(() => {
|
||||||
|
const { rotation, characterHair } = props.zoneCharacter.character
|
||||||
|
const spriteId = characterHair?.sprite?.id
|
||||||
|
const direction = [0, 6].includes(rotation) ? 'back' : 'front'
|
||||||
|
|
||||||
|
return `${spriteId}-${direction}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const isFlippedX = computed(() => [6, 4].includes(props.zoneCharacter.character.rotation ?? 0))
|
||||||
|
|
||||||
|
const imageProps = computed(() => {
|
||||||
|
// Get the current sprite action based on direction
|
||||||
|
const direction = [0, 6].includes(props.zoneCharacter.character.rotation ?? 0) ? 'back' : 'front'
|
||||||
|
const spriteAction = props.zoneCharacter.character.characterHair?.sprite?.spriteActions?.find((spriteAction) => spriteAction.action === direction)
|
||||||
|
|
||||||
|
return {
|
||||||
|
depth: 1,
|
||||||
|
originX: Number(spriteAction?.originX) ?? 0,
|
||||||
|
originY: Number(spriteAction?.originY) ?? 0,
|
||||||
|
flipX: isFlippedX.value,
|
||||||
|
texture: texture.value,
|
||||||
|
y: props.zoneCharacter.isMoving ? Math.floor(Date.now() / 250) % 2 : 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
loadSpriteTextures(scene, props.zoneCharacter.character.characterHair?.sprite as SpriteT)
|
||||||
|
.then(() => {})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error loading texture:', error)
|
||||||
|
})
|
||||||
|
</script>
|
46
src/components/game/character/partials/ChatBubble.vue
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<Container ref="charChatContainer" :depth="999" :x="currentX" :y="currentY">
|
||||||
|
<RoundRectangle @create="createChatBubble" :origin-x="0.5" :origin-y="7.5" :fillColor="0xffffff" :width="194" :height="21" :radius="20" />
|
||||||
|
<Text @create="createChatText" :style="{ fontSize: 13, fontFamily: 'Arial', color: '#000' }" />
|
||||||
|
</Container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Container, refObj, RoundRectangle, Text, useGame } from 'phavuer'
|
||||||
|
import type { ZoneCharacter } from '@/application/types'
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
zoneCharacter: ZoneCharacter
|
||||||
|
currentX: number
|
||||||
|
currentY: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const game = useGame()
|
||||||
|
const charChatContainer = refObj<Phaser.GameObjects.Container>()
|
||||||
|
|
||||||
|
const createChatBubble = (container: Phaser.GameObjects.Container) => {
|
||||||
|
container.setName(`${props.zoneCharacter.character.name}_chatBubble`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const createChatText = (text: Phaser.GameObjects.Text) => {
|
||||||
|
text.setName(`${props.zoneCharacter.character.name}_chatText`)
|
||||||
|
text.setFontSize(13)
|
||||||
|
text.setFontFamily('Arial')
|
||||||
|
text.setOrigin(0.5, 10.9)
|
||||||
|
|
||||||
|
// Fix text alignment on Windows and Android
|
||||||
|
if (game.device.os.windows || game.device.os.android) {
|
||||||
|
text.setOrigin(0.5, 9.75)
|
||||||
|
|
||||||
|
if (game.device.browser.firefox) {
|
||||||
|
text.setOrigin(0.5, 10.9)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
charChatContainer.value!.setName(`${props.zoneCharacter.character!.name}_chatContainer`)
|
||||||
|
charChatContainer.value!.setVisible(false)
|
||||||
|
})
|
||||||
|
</script>
|
35
src/components/game/character/partials/Healthbar.vue
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<Container :depth="999" :x="currentX" :y="currentY">
|
||||||
|
<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="36.4" :fillColor="0x00b3b3" :width="70" :height="3" :radius="5" />
|
||||||
|
</Container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Container, RoundRectangle, Text, useGame } from 'phavuer'
|
||||||
|
import type { ZoneCharacter } from '@/application/types'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
zoneCharacter: ZoneCharacter
|
||||||
|
currentX: number
|
||||||
|
currentY: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const game = useGame()
|
||||||
|
|
||||||
|
const createNicknameText = (text: Phaser.GameObjects.Text) => {
|
||||||
|
text.setFontSize(13)
|
||||||
|
text.setFontFamily('Arial')
|
||||||
|
text.setOrigin(0.5, 9)
|
||||||
|
|
||||||
|
// Fix text alignment on Windows and Android
|
||||||
|
if (game.device.os.windows || game.device.os.android) {
|
||||||
|
text.setOrigin(0.5, 8)
|
||||||
|
|
||||||
|
if (game.device.browser.firefox) {
|
||||||
|
text.setOrigin(0.5, 9)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
14
src/components/game/zone/Characters.vue
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<Character v-for="item in zoneStore.characters" :key="item.character.id" :layer="tilemap" :zoneCharacter="item" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Character from '@/components/game/character/Character.vue'
|
||||||
|
import { useZoneStore } from '@/stores/zoneStore'
|
||||||
|
|
||||||
|
const zoneStore = useZoneStore()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
tilemap: Phaser.Tilemaps.Tilemap
|
||||||
|
}>()
|
||||||
|
</script>
|
50
src/components/game/zone/Zone.vue
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<ZoneTiles :key="zoneStore.zone?.id ?? 0" @tileMap:create="tileMap = $event" />
|
||||||
|
<ZoneObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
||||||
|
<Characters v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onUnmounted, onMounted, onBeforeMount } from 'vue'
|
||||||
|
import { useScene } from 'phavuer'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useZoneStore } from '@/stores/zoneStore'
|
||||||
|
import { loadZoneTilesIntoScene } from '@/composables/zoneComposable'
|
||||||
|
import type { Zone as ZoneT, ZoneCharacter, zoneLoadData } from '@/application/types'
|
||||||
|
import ZoneTiles from '@/components/game/zone/ZoneTiles.vue'
|
||||||
|
import ZoneObjects from '@/components/game/zone/ZoneObjects.vue'
|
||||||
|
import Characters from '@/components/game/zone/Characters.vue'
|
||||||
|
|
||||||
|
const scene = useScene()
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const zoneStore = useZoneStore()
|
||||||
|
|
||||||
|
const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null)
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
zoneStore.reset()
|
||||||
|
gameStore.connection!.off('zone:character:teleport')
|
||||||
|
gameStore.connection!.off('zone:character:join')
|
||||||
|
gameStore.connection!.off('zone:character:leave')
|
||||||
|
gameStore.connection!.off('zone:character:move')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
gameStore.connection!.on('zone:character:teleport', async (data: zoneLoadData) => {
|
||||||
|
await loadZoneTilesIntoScene(data.zone, scene)
|
||||||
|
zoneStore.setZone(data.zone)
|
||||||
|
zoneStore.setCharacters(data.characters)
|
||||||
|
})
|
||||||
|
|
||||||
|
gameStore.connection!.on('zone:character:join', async (data: ZoneCharacter) => {
|
||||||
|
zoneStore.addCharacter(data)
|
||||||
|
})
|
||||||
|
|
||||||
|
gameStore.connection!.on('zone:character:leave', (characterId: number) => {
|
||||||
|
zoneStore.removeCharacter(characterId)
|
||||||
|
})
|
||||||
|
|
||||||
|
gameStore.connection!.on('zone:character:move', (data: { id: number; positionX: number; positionY: number; rotation: number; isMoving: boolean }) => {
|
||||||
|
zoneStore.updateCharacterPosition(data)
|
||||||
|
})
|
||||||
|
</script>
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useZoneStore } from '@/stores/zoneStore'
|
import { useZoneStore } from '@/stores/zoneStore'
|
||||||
import ZoneObject from '@/components/zone/partials/ZoneObject.vue'
|
import ZoneObject from '@/components/game/zone/partials/ZoneObject.vue'
|
||||||
|
|
||||||
const zoneStore = useZoneStore()
|
const zoneStore = useZoneStore()
|
||||||
|
|
69
src/components/game/zone/ZoneTiles.vue
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<template>
|
||||||
|
<Controls :layer="tileLayer" :depth="0" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import config from '@/application/config'
|
||||||
|
import { useScene } from 'phavuer'
|
||||||
|
import { useZoneStore } from '@/stores/zoneStore'
|
||||||
|
import { onBeforeUnmount } from 'vue'
|
||||||
|
import { FlattenZoneArray, setLayerTiles } from '@/composables/zoneComposable'
|
||||||
|
import Controls from '@/components/utilities/Controls.vue'
|
||||||
|
import { unduplicateArray } from '@/application/utilities'
|
||||||
|
|
||||||
|
const emit = defineEmits(['tileMap:create'])
|
||||||
|
|
||||||
|
const scene = useScene()
|
||||||
|
const zoneStore = useZoneStore()
|
||||||
|
const tileMap = createTileMap()
|
||||||
|
const tileLayer = createTileLayer()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Tilemap is a container for Tilemap data.
|
||||||
|
* This isn't a display object, rather, it holds data about the map and allows you to add tilesets and tilemap layers to it.
|
||||||
|
* A map can have one or more tilemap layers, which are the display objects that actually render the tiles.
|
||||||
|
*/
|
||||||
|
function createTileMap() {
|
||||||
|
const zoneData = new Phaser.Tilemaps.MapData({
|
||||||
|
width: zoneStore.zone?.width,
|
||||||
|
height: zoneStore.zone?.height,
|
||||||
|
tileWidth: config.tile_size.x,
|
||||||
|
tileHeight: config.tile_size.y,
|
||||||
|
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
|
||||||
|
format: Phaser.Tilemaps.Formats.ARRAY_2D
|
||||||
|
})
|
||||||
|
|
||||||
|
const newTileMap = new Phaser.Tilemaps.Tilemap(scene, zoneData)
|
||||||
|
emit('tileMap:create', newTileMap)
|
||||||
|
|
||||||
|
return newTileMap
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Tileset is a combination of a single image containing the tiles and a container for data about each tile.
|
||||||
|
*/
|
||||||
|
function createTileLayer() {
|
||||||
|
const tilesArray = unduplicateArray(FlattenZoneArray(zoneStore.zone?.tiles ?? []))
|
||||||
|
|
||||||
|
const tilesetImages = Array.from(tilesArray).map((tile: any, index: number) => {
|
||||||
|
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
|
||||||
|
|
||||||
|
// Add blank tile
|
||||||
|
tilesetImages.push(tileMap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 1, 2, 0, { x: 0, y: -config.tile_size.y }))
|
||||||
|
const layer = tileMap.createBlankLayer('tiles', tilesetImages, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
|
||||||
|
|
||||||
|
layer.setDepth(0)
|
||||||
|
layer.setCullPadding(2, 2)
|
||||||
|
|
||||||
|
return layer
|
||||||
|
}
|
||||||
|
|
||||||
|
setLayerTiles(tileMap, tileLayer, zoneStore.zone?.tiles)
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
tileMap.destroyLayer('tiles')
|
||||||
|
tileMap.removeAllLayers()
|
||||||
|
tileMap.destroy()
|
||||||
|
})
|
||||||
|
</script>
|
41
src/components/game/zone/partials/ZoneObject.vue
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<template>
|
||||||
|
<Image v-if="gameStore.getLoadedAsset(props.zoneObject.object.id)" v-bind="imageProps" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { Image, useScene } from 'phavuer'
|
||||||
|
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||||
|
import { loadTexture } from '@/composables/gameComposable'
|
||||||
|
import type { AssetDataT, ZoneObject } from '@/application/types'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
tilemap: Phaser.Tilemaps.Tilemap
|
||||||
|
zoneObject: ZoneObject
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const scene = useScene()
|
||||||
|
|
||||||
|
const imageProps = computed(() => ({
|
||||||
|
depth: calculateIsometricDepth(props.zoneObject.positionX, props.zoneObject.positionY, props.zoneObject.object.frameWidth, props.zoneObject.object.frameHeight),
|
||||||
|
x: tileToWorldX(props.tilemap, props.zoneObject.positionX, props.zoneObject.positionY),
|
||||||
|
y: tileToWorldY(props.tilemap, props.zoneObject.positionX, props.zoneObject.positionY),
|
||||||
|
flipX: props.zoneObject.isRotated,
|
||||||
|
texture: props.zoneObject.object.id,
|
||||||
|
originY: Number(props.zoneObject.object.originX),
|
||||||
|
originX: Number(props.zoneObject.object.originY)
|
||||||
|
}))
|
||||||
|
|
||||||
|
loadTexture(scene, {
|
||||||
|
key: props.zoneObject.object.id,
|
||||||
|
data: '/assets/objects/' + props.zoneObject.object.id + '.png',
|
||||||
|
group: 'objects',
|
||||||
|
updatedAt: props.zoneObject.object.updatedAt,
|
||||||
|
frameWidth: props.zoneObject.object.frameWidth,
|
||||||
|
frameHeight: props.zoneObject.object.frameHeight
|
||||||
|
} as AssetDataT).catch((error) => {
|
||||||
|
console.error('Error loading texture:', error)
|
||||||
|
})
|
||||||
|
</script>
|
@ -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" :is-full-screen="true" :bg-style="'dark'">
|
||||||
<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()">Map editor</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>
|
||||||
|
@ -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>
|
|
@ -1,70 +1,60 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full w-full relative">
|
<div class="flex gap-4 h-[calc(100%_-_32px)] w-[calc(100%_-_32px)] relative m-4">
|
||||||
<div class="w-2/12 flex flex-col relative overflow-auto">
|
<div class="w-2/12 flex flex-col relative overflow-auto rounded-md default-border bg-gray p-2.5">
|
||||||
<!-- Asset Categories -->
|
<!-- Asset Categories -->
|
||||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'tiles' }" @click="() => (selectedCategory = 'tiles')">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'tiles' }" @click="() => (selectedCategory = 'tiles')">
|
||||||
<span :class="{ 'text-white': selectedCategory === 'tiles' }">Tiles</span>
|
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'tiles' }">Tiles</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'objects' }" @click="() => (selectedCategory = 'objects')">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'objects' }" @click="() => (selectedCategory = 'objects')">
|
||||||
<span :class="{ 'text-white': selectedCategory === 'objects' }">Objects</span>
|
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'objects' }">Objects</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'sprites' }" @click="() => (selectedCategory = 'sprites')">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'sprites' }" @click="() => (selectedCategory = 'sprites')">
|
||||||
<span :class="{ 'text-white': selectedCategory === 'sprites' }">Sprites</span>
|
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'sprites' }">Sprites</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'items' }" @click="() => (selectedCategory = 'items')">
|
||||||
<span>Items</span>
|
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'items' }">Items</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group">
|
||||||
<span>NPC's</span>
|
<span class="group-hover:text-white">NPC's</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'shops' }" @click="() => (selectedCategory = 'shops')">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan/80': selectedCategory === 'shops' }" @click="() => (selectedCategory = 'shops')">
|
||||||
<span :class="{ 'text-white': selectedCategory === 'shops' }">Shops</span>
|
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'shops' }">Shops</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'characterTypes' }" @click="() => (selectedCategory = 'characterTypes')">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan/80': selectedCategory === 'characterTypes' }" @click="() => (selectedCategory = 'characterTypes')">
|
||||||
<span :class="{ 'text-white': selectedCategory === 'characterTypes' }">Character types</span>
|
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'characterTypes' }">Character types</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'characterHair' }" @click="() => (selectedCategory = 'characterHair')">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan/80': selectedCategory === 'characterHair' }" @click="() => (selectedCategory = 'characterHair')">
|
||||||
<span :class="{ 'text-white': selectedCategory === 'characterHair' }">Character hair</span>
|
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'characterHair' }">Character hair</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group">
|
||||||
<span>Mounts</span>
|
<span class="group-hover:text-white">Mounts</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group">
|
||||||
<span>Pets</span>
|
<span class="group-hover:text-white">Pets</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group">
|
||||||
<span>Emoticons</span>
|
<span class="group-hover:text-white">Emoticons</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute w-px bg-gray-500 h-full top-0 left-1/6"></div>
|
|
||||||
|
|
||||||
<!-- Assets list -->
|
<!-- Assets list -->
|
||||||
<div class="overflow-auto h-full w-4/12 flex flex-col relative">
|
<div class="overflow-auto h-full w-4/12 flex flex-col relative">
|
||||||
<TileList v-if="selectedCategory === 'tiles'" />
|
<TileList v-if="selectedCategory === 'tiles'" />
|
||||||
<ObjectList v-if="selectedCategory === 'objects'" />
|
<ObjectList v-if="selectedCategory === 'objects'" />
|
||||||
<SpriteList v-if="selectedCategory === 'sprites'" />
|
<SpriteList v-if="selectedCategory === 'sprites'" />
|
||||||
|
<ItemList v-if="selectedCategory === 'items'" />
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- Asset details -->
|
<!-- Asset details -->
|
||||||
<div class="flex w-1/2 after:hidden flex-col relative overflow-auto">
|
<div class="flex w-7/12 after:hidden flex-col relative overflow-auto">
|
||||||
<TileDetails v-if="selectedCategory === 'tiles' && assetManagerStore.selectedTile" />
|
<TileDetails v-if="selectedCategory === 'tiles' && assetManagerStore.selectedTile" />
|
||||||
<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" />
|
||||||
|
<ItemDetails v-if="selectedCategory === 'items' && assetManagerStore.selectedItem" />
|
||||||
<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 +70,10 @@ 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'
|
||||||
|
import ItemList from '@/components/gameMaster/assetManager/partials/item/itemList.vue'
|
||||||
|
import ItemDetails from '@/components/gameMaster/assetManager/partials/item/itemDetails.vue'
|
||||||
|
|
||||||
const assetManagerStore = useAssetManagerStore()
|
const assetManagerStore = useAssetManagerStore()
|
||||||
const selectedCategory = ref('tiles')
|
const selectedCategory = ref('tiles')
|
||||||
|
@ -0,0 +1,124 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-full overflow-auto">
|
||||||
|
<div class="p-2.5 block rounded-md default-border bg-gray">
|
||||||
|
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveCharacterHair">
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input v-model="characterName" class="input-field" type="text" name="name" placeholder="Character Type Name" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="gender">Gender</label>
|
||||||
|
<select v-model="characterGender" class="input-field" name="gender">
|
||||||
|
<option v-for="gender in genderOptions" :key="gender" :value="gender">{{ gender }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="isSelectable">Is selectable</label>
|
||||||
|
<select v-model="characterIsSelectable" class="input-field" name="isSelectable">
|
||||||
|
<option :value="false">No</option>
|
||||||
|
<option :value="true">Yes</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="spriteId">Sprite</label>
|
||||||
|
<select v-model="characterSpriteId" class="input-field" name="spriteId">
|
||||||
|
<option disabled selected value="">Select sprite</option>
|
||||||
|
<option v-for="sprite in assetManagerStore.spriteList" :key="sprite.id" :value="sprite.id">{{ sprite.name }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
||||||
|
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeCharacterHair">Remove</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { CharacterHair, CharacterGender, Sprite } from '@/application/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 characterIsSelectable = ref<boolean>(false)
|
||||||
|
const characterSpriteId = ref<string | null | undefined>(null)
|
||||||
|
|
||||||
|
const genderOptions: CharacterGender[] = ['MALE' as CharacterGender.MALE, 'FEMALE' as CharacterGender.FEMALE]
|
||||||
|
|
||||||
|
if (!selectedCharacterHair.value) {
|
||||||
|
console.error('No character hair selected')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedCharacterHair.value) {
|
||||||
|
characterName.value = selectedCharacterHair.value.name
|
||||||
|
characterGender.value = selectedCharacterHair.value.gender
|
||||||
|
characterIsSelectable.value = selectedCharacterHair.value.isSelectable
|
||||||
|
characterSpriteId.value = selectedCharacterHair.value.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,
|
||||||
|
isSelectable: characterIsSelectable.value,
|
||||||
|
spriteId: characterSpriteId.value
|
||||||
|
}
|
||||||
|
|
||||||
|
gameStore.connection?.emit('gm:characterHair:update', characterHairData, (response: boolean) => {
|
||||||
|
if (!response) {
|
||||||
|
console.error('Failed to save character type')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
refreshCharacterHairList(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(selectedCharacterHair, (characterHair: CharacterHair | null) => {
|
||||||
|
if (!characterHair) return
|
||||||
|
characterName.value = characterHair.name
|
||||||
|
characterGender.value = characterHair.gender
|
||||||
|
characterIsSelectable.value = characterHair.isSelectable
|
||||||
|
characterSpriteId.value = characterHair.spriteId
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!selectedCharacterHair.value) return
|
||||||
|
|
||||||
|
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
||||||
|
assetManagerStore.setSpriteList(response)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
assetManagerStore.setSelectedCharacterHair(null)
|
||||||
|
})
|
||||||
|
</script>
|
@ -0,0 +1,99 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative mb-5 flex items-center gap-x-2.5">
|
||||||
|
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
|
||||||
|
<label for="create-character" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2.5 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
|
||||||
|
<button class="p-0 h-5" id="create-character" @click="createNewCharacterHair">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="white">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @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 rounded hover:bg-cyan group"
|
||||||
|
:class="{ 'bg-cyan': assetManagerStore.selectedCharacterHair?.id === characterHair.id }"
|
||||||
|
@click="assetManagerStore.setSelectedCharacterHair(characterHair as CharacterHair)"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2.5">
|
||||||
|
<span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedCharacterHair?.id === characterHair.id }">{{ characterHair.name }}</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
||||||
|
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
||||||
|
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</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 '@/application/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>
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full overflow-auto">
|
<div class="h-full overflow-auto">
|
||||||
<div class="m-2.5 p-2.5 block">
|
<div class="p-2.5 block rounded-md default-border bg-gray">
|
||||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveCharacterType">
|
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveCharacterType">
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="name">Name</label>
|
<label for="name">Name</label>
|
||||||
@ -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="isSelectable">Is selectable</label>
|
||||||
<input v-model="characterSpriteId" class="input-field" type="text" name="spriteId" placeholder="Sprite ID" />
|
<select v-model="characterIsSelectable" class="input-field" name="isSelectable">
|
||||||
|
<option :value="false">No</option>
|
||||||
|
<option :value="true">Yes</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="spriteId">Sprite</label>
|
||||||
|
<select v-model="characterSpriteId" class="input-field" name="spriteId">
|
||||||
|
<option disabled selected value="">Select sprite</option>
|
||||||
|
<option v-for="sprite in assetManagerStore.spriteList" :key="sprite.id" :value="sprite.id">{{ sprite.name }}</option>
|
||||||
|
</select>
|
||||||
</div>
|
</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 '@/application/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 characterIsSelectable = 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
|
||||||
|
characterIsSelectable.value = selectedCharacterType.value.isSelectable
|
||||||
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,
|
||||||
|
isSelectable: characterIsSelectable.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
|
||||||
|
characterIsSelectable.value = characterType.isSelectable
|
||||||
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(() => {
|
||||||
|
@ -1,27 +1,33 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative p-2.5 flex items-center gap-x-2.5">
|
<div class="relative mb-5 flex items-center gap-x-2.5">
|
||||||
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
|
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
|
||||||
<label for="create-character" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
|
<label for="create-character" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2.5 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
|
||||||
<button class="p-0 h-5" id="create-character" @click="createNewCharacterType">
|
<button class="p-0 h-5" id="create-character" @click="createNewCharacterType">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="white">
|
<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" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</label>
|
</label>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-bind="containerProps" class="overflow-y-auto relative" @scroll="onScroll">
|
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||||
<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 rounded hover:bg-cyan group"
|
||||||
|
:class="{ 'bg-cyan': assetManagerStore.selectedCharacterType?.id === characterType.id }"
|
||||||
|
@click="assetManagerStore.setSelectedCharacterType(characterType as CharacterType)"
|
||||||
|
>
|
||||||
<div class="flex items-center gap-2.5">
|
<div class="flex items-center gap-2.5">
|
||||||
<span>{{ characterType.name }}</span>
|
<span class="group-hover:text-white" :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>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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">
|
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
||||||
<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 class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
||||||
</button>
|
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -29,7 +35,7 @@
|
|||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { onMounted, ref, computed } from 'vue'
|
import { onMounted, ref, computed } from 'vue'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import type { CharacterType } from '@/types'
|
import type { CharacterType } from '@/application/types'
|
||||||
import { useVirtualList } from '@vueuse/core'
|
import { useVirtualList } from '@vueuse/core'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
@ -0,0 +1,143 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-full overflow-auto">
|
||||||
|
<div class="p-2.5 block rounded-md default-border bg-gray">
|
||||||
|
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveItem">
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input v-model="itemName" class="input-field" type="text" name="name" placeholder="Item Name" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<input v-model="itemDescription" class="input-field" type="text" name="description" placeholder="Item Description" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="itemType">Type</label>
|
||||||
|
<select v-model="itemType" class="input-field" name="itemType">
|
||||||
|
<option v-for="type in itemTypeOptions" :key="type" :value="type">{{ type }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="rarity">Rarity</label>
|
||||||
|
<select v-model="itemRarity" class="input-field" name="rarity">
|
||||||
|
<option v-for="rarity in rarityOptions" :key="rarity" :value="rarity">{{ rarity }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="stackable">Stackable</label>
|
||||||
|
<select v-model="itemStackable" class="input-field" name="stackable">
|
||||||
|
<option :value="false">No</option>
|
||||||
|
<option :value="true">Yes</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="spriteId">Sprite</label>
|
||||||
|
<select v-model="itemSpriteId" class="input-field" name="spriteId">
|
||||||
|
<option disabled selected value="">Select sprite</option>
|
||||||
|
<option v-for="sprite in assetManagerStore.spriteList" :key="sprite.id" :value="sprite.id">{{ sprite.name }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
||||||
|
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeItem">Remove</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Item, ItemType, ItemRarity } from '@/application/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 selectedItem = computed(() => assetManagerStore.selectedItem)
|
||||||
|
|
||||||
|
const itemName = ref('')
|
||||||
|
const itemDescription = ref('')
|
||||||
|
const itemType = ref<ItemType>('WEAPON' as ItemType)
|
||||||
|
const itemRarity = ref<ItemRarity>('COMMON' as ItemRarity)
|
||||||
|
const itemStackable = ref<boolean>(false)
|
||||||
|
const itemSpriteId = ref<string | null | undefined>(null)
|
||||||
|
|
||||||
|
const itemTypeOptions: ItemType[] = ['WEAPON', 'HELMET', 'CHEST', 'LEGS', 'BOOTS', 'GLOVES', 'RING', 'NECKLACE']
|
||||||
|
const rarityOptions: ItemRarity[] = ['COMMON', 'UNCOMMON', 'RARE', 'EPIC', 'LEGENDARY']
|
||||||
|
|
||||||
|
if (!selectedItem.value) {
|
||||||
|
console.error('No item selected')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedItem.value) {
|
||||||
|
itemName.value = selectedItem.value.name
|
||||||
|
itemDescription.value = selectedItem.value.description || ''
|
||||||
|
itemType.value = selectedItem.value.itemType
|
||||||
|
itemRarity.value = selectedItem.value.rarity
|
||||||
|
itemStackable.value = selectedItem.value.stackable
|
||||||
|
itemSpriteId.value = selectedItem.value.spriteId
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeItem() {
|
||||||
|
if (!selectedItem.value) return
|
||||||
|
|
||||||
|
gameStore.connection?.emit('gm:item:remove', { id: selectedItem.value.id }, (response: boolean) => {
|
||||||
|
if (!response) {
|
||||||
|
console.error('Failed to remove item')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
refreshItemList()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshItemList(unsetSelectedItem = true) {
|
||||||
|
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => {
|
||||||
|
assetManagerStore.setItemList(response)
|
||||||
|
|
||||||
|
if (unsetSelectedItem) {
|
||||||
|
assetManagerStore.setSelectedItem(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveItem() {
|
||||||
|
const itemData = {
|
||||||
|
id: selectedItem.value!.id,
|
||||||
|
name: itemName.value,
|
||||||
|
description: itemDescription.value,
|
||||||
|
itemType: itemType.value,
|
||||||
|
rarity: itemRarity.value,
|
||||||
|
stackable: itemStackable.value,
|
||||||
|
spriteId: itemSpriteId.value
|
||||||
|
}
|
||||||
|
|
||||||
|
gameStore.connection?.emit('gm:item:update', itemData, (response: boolean) => {
|
||||||
|
if (!response) {
|
||||||
|
console.error('Failed to save item')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
refreshItemList(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(selectedItem, (item: Item | null) => {
|
||||||
|
if (!item) return
|
||||||
|
itemName.value = item.name
|
||||||
|
itemDescription.value = item.description || ''
|
||||||
|
itemType.value = item.itemType
|
||||||
|
itemRarity.value = item.rarity
|
||||||
|
itemStackable.value = item.stackable
|
||||||
|
itemSpriteId.value = item.spriteId
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!selectedItem.value) return
|
||||||
|
|
||||||
|
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
||||||
|
assetManagerStore.setSpriteList(response)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
assetManagerStore.setSelectedItem(null)
|
||||||
|
})
|
||||||
|
</script>
|
@ -0,0 +1,95 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative mb-5 flex items-center gap-x-2.5">
|
||||||
|
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
|
||||||
|
<label for="create-item" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2.5 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
|
||||||
|
<button class="p-0 h-5" id="create-item" @click="createNewItem">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="white">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||||
|
<div v-bind="wrapperProps" ref="elementToScroll">
|
||||||
|
<a v-for="{ data: item } in list" :key="item.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedItem?.id === item.id }" @click="assetManagerStore.setSelectedItem(item as Item)">
|
||||||
|
<div class="flex items-center gap-2.5">
|
||||||
|
<span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedItem?.id === item.id }">
|
||||||
|
{{ item.name }}
|
||||||
|
<small class="text-gray-400">({{ item.itemType }})</small>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
||||||
|
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
||||||
|
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { onMounted, ref, computed } from 'vue'
|
||||||
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
|
import type { Item } from '@/application/types'
|
||||||
|
import { useVirtualList } from '@vueuse/core'
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const assetManagerStore = useAssetManagerStore()
|
||||||
|
|
||||||
|
const searchQuery = ref('')
|
||||||
|
|
||||||
|
const hasScrolled = ref(false)
|
||||||
|
const elementToScroll = ref()
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
virtualList.value?.scrollTo(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const createNewItem = () => {
|
||||||
|
gameStore.connection?.emit('gm:item:create', {}, (response: boolean) => {
|
||||||
|
if (!response) {
|
||||||
|
console.error('Failed to create new item')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => {
|
||||||
|
assetManagerStore.setItemList(response)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredItems = computed(() => {
|
||||||
|
if (!searchQuery.value) {
|
||||||
|
return assetManagerStore.itemList
|
||||||
|
}
|
||||||
|
return assetManagerStore.itemList.filter((item) => item.name.toLowerCase().includes(searchQuery.value.toLowerCase()) || item.itemType.toLowerCase().includes(searchQuery.value.toLowerCase()))
|
||||||
|
})
|
||||||
|
|
||||||
|
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(filteredItems, {
|
||||||
|
itemHeight: 48
|
||||||
|
})
|
||||||
|
|
||||||
|
const virtualList = ref({ scrollTo })
|
||||||
|
|
||||||
|
const onScroll = () => {
|
||||||
|
let scrollTop = elementToScroll.value.style.marginTop.replace('px', '')
|
||||||
|
|
||||||
|
if (scrollTop > 80) {
|
||||||
|
hasScrolled.value = true
|
||||||
|
} else if (scrollTop <= 80) {
|
||||||
|
hasScrolled.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toTop() {
|
||||||
|
virtualList.value?.scrollTo(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => {
|
||||||
|
assetManagerStore.setItemList(response)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
@ -1,12 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full overflow-auto">
|
<div class="h-full overflow-auto">
|
||||||
<div class="relative p-2.5 flex flex-col items-center justify-between h-72">
|
<div class="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray">
|
||||||
<div class="filler"></div>
|
|
||||||
<img class="max-h-56" :src="`${config.server_endpoint}/assets/objects/${selectedObject?.id}.png`" :alt="'Object ' + selectedObject?.id" />
|
<img class="max-h-56" :src="`${config.server_endpoint}/assets/objects/${selectedObject?.id}.png`" :alt="'Object ' + selectedObject?.id" />
|
||||||
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeObject">Remove</button>
|
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="m-2.5 p-2.5 block">
|
<div class="mt-5 block">
|
||||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject">
|
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject">
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="name">Name</label>
|
<label for="name">Name</label>
|
||||||
@ -21,19 +18,19 @@
|
|||||||
<input v-model="objectOriginY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
|
<input v-model="objectOriginY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="origin-x">Tags</label>
|
<label for="tags">Tags</label>
|
||||||
<ChipsInput v-model="objectTags" @update:modelValue="objectTags = $event" />
|
<ChipsInput v-model="objectTags" @update:modelValue="objectTags = $event" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="origin-x">Is animated</label>
|
<label for="is-animated">Is animated</label>
|
||||||
<select v-model="objectIsAnimated" class="input-field" name="is-animated">
|
<select v-model="objectIsAnimated" class="input-field" name="is-animated">
|
||||||
<option :value="false">No</option>
|
<option :value="false">No</option>
|
||||||
<option :value="true">Yes</option>
|
<option :value="true">Yes</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="frame-speed">Frame speed</label>
|
<label for="frame-speed">Frame rate</label>
|
||||||
<input v-model="objectFrameSpeed" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame speed" />
|
<input v-model="objectFrameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-half">
|
<div class="form-field-half">
|
||||||
<label for="frame-width">Frame width</label>
|
<label for="frame-width">Frame width</label>
|
||||||
@ -43,19 +40,22 @@
|
|||||||
<label for="frame-height">Frame height</label>
|
<label for="frame-height">Frame height</label>
|
||||||
<input v-model="objectFrameHeight" class="input-field" type="number" step="any" name="frame-height" placeholder="Frame height" />
|
<input v-model="objectFrameHeight" class="input-field" type="number" step="any" name="frame-height" placeholder="Frame height" />
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
<div class="flex gap-4">
|
||||||
|
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
||||||
|
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeObject">Delete</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Object } from '@/types'
|
import type { Object } from '@/application/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 { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import config from '@/config'
|
import config from '@/application/config'
|
||||||
import ChipsInput from '@/components/forms/ChipsInput.vue'
|
import ChipsInput from '@/components/forms/ChipsInput.vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
@ -69,7 +69,7 @@ const objectTags = ref<string[]>([])
|
|||||||
const objectOriginX = ref(0)
|
const objectOriginX = ref(0)
|
||||||
const objectOriginY = ref(0)
|
const objectOriginY = ref(0)
|
||||||
const objectIsAnimated = ref(false)
|
const objectIsAnimated = ref(false)
|
||||||
const objectFrameSpeed = ref(0)
|
const objectFrameRate = ref(0)
|
||||||
const objectFrameWidth = ref(0)
|
const objectFrameWidth = ref(0)
|
||||||
const objectFrameHeight = ref(0)
|
const objectFrameHeight = ref(0)
|
||||||
|
|
||||||
@ -83,7 +83,7 @@ if (selectedObject.value) {
|
|||||||
objectOriginX.value = selectedObject.value.originX
|
objectOriginX.value = selectedObject.value.originX
|
||||||
objectOriginY.value = selectedObject.value.originY
|
objectOriginY.value = selectedObject.value.originY
|
||||||
objectIsAnimated.value = selectedObject.value.isAnimated
|
objectIsAnimated.value = selectedObject.value.isAnimated
|
||||||
objectFrameSpeed.value = selectedObject.value.frameSpeed
|
objectFrameRate.value = selectedObject.value.frameRate
|
||||||
objectFrameWidth.value = selectedObject.value.frameWidth
|
objectFrameWidth.value = selectedObject.value.frameWidth
|
||||||
objectFrameHeight.value = selectedObject.value.frameHeight
|
objectFrameHeight.value = selectedObject.value.frameHeight
|
||||||
}
|
}
|
||||||
@ -127,7 +127,7 @@ function saveObject() {
|
|||||||
originX: objectOriginX.value,
|
originX: objectOriginX.value,
|
||||||
originY: objectOriginY.value,
|
originY: objectOriginY.value,
|
||||||
isAnimated: objectIsAnimated.value,
|
isAnimated: objectIsAnimated.value,
|
||||||
frameSpeed: objectFrameSpeed.value,
|
frameRate: objectFrameRate.value,
|
||||||
frameWidth: objectFrameWidth.value,
|
frameWidth: objectFrameWidth.value,
|
||||||
frameHeight: objectFrameHeight.value
|
frameHeight: objectFrameHeight.value
|
||||||
},
|
},
|
||||||
@ -148,7 +148,7 @@ watch(selectedObject, (object: Object | null) => {
|
|||||||
objectOriginX.value = object.originX
|
objectOriginX.value = object.originX
|
||||||
objectOriginY.value = object.originY
|
objectOriginY.value = object.originY
|
||||||
objectIsAnimated.value = object.isAnimated
|
objectIsAnimated.value = object.isAnimated
|
||||||
objectFrameSpeed.value = object.frameSpeed
|
objectFrameRate.value = object.frameRate
|
||||||
objectFrameWidth.value = object.frameWidth
|
objectFrameWidth.value = object.frameWidth
|
||||||
objectFrameHeight.value = object.frameHeight
|
objectFrameHeight.value = object.frameHeight
|
||||||
})
|
})
|
||||||
|
@ -1,38 +1,38 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative p-2.5 flex items-center gap-x-2.5">
|
<div class="relative mb-5 flex items-center gap-x-2.5">
|
||||||
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
|
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
|
||||||
<label for="upload-asset" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
|
<label for="upload-asset" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2.5 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
|
||||||
<input class="hidden" id="upload-asset" ref="objectUploadField" type="file" accept="image/png" multiple @change="handleFileUpload" />
|
<input class="hidden" id="upload-asset" ref="objectUploadField" type="file" accept="image/png" multiple @change="handleFileUpload" />
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||||
</svg>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-bind="containerProps" class="overflow-y-auto relative" @scroll="onScroll">
|
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||||
<div v-bind="wrapperProps" ref="elementToScroll">
|
<div v-bind="wrapperProps" ref="elementToScroll">
|
||||||
<a v-for="{ data: object } in list" :key="object.id" class="relative p-2.5 cursor-pointer block" :class="{ 'bg-cyan/80': assetManagerStore.selectedObject?.id === object.id }" @click="assetManagerStore.setSelectedObject(object as Object)">
|
<a v-for="{ data: object } in list" :key="object.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedObject?.id === object.id }" @click="assetManagerStore.setSelectedObject(object as Object)">
|
||||||
<div class="flex items-center gap-2.5">
|
<div class="flex items-center gap-2.5">
|
||||||
<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>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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">
|
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
||||||
<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 class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
||||||
</button>
|
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/config'
|
import config from '@/application/config'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { onMounted, ref, computed } from 'vue'
|
import { onMounted, ref, computed } from 'vue'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import type { Object } from '@/types'
|
import type { Object } from '@/application/types'
|
||||||
import { useVirtualList } from '@vueuse/core'
|
import { useVirtualList } from '@vueuse/core'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full overflow-auto">
|
<div class="h-full overflow-auto">
|
||||||
<div class="relative p-4 flex flex-col">
|
<div class="relative flex flex-col">
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2 p-2.5 rounded-md default-border bg-gray">
|
||||||
<div class="w-full flex flex-col">
|
<div class="w-full flex flex-col">
|
||||||
<label class="mb-1.5 font-titles" for="name">Name</label>
|
<label class="mb-1.5 font-titles" for="name">Name</label>
|
||||||
<input v-model="spriteName" class="input-field" type="text" name="name" placeholder="New sprite" />
|
<input v-model="spriteName" class="input-field" type="text" name="name" placeholder="New sprite" />
|
||||||
@ -10,7 +10,11 @@
|
|||||||
<div class="w-full flex gap-2 mt-2 pb-4 relative">
|
<div class="w-full flex gap-2 mt-2 pb-4 relative">
|
||||||
<button class="btn-cyan px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="saveSprite">Save</button>
|
<button class="btn-cyan px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="saveSprite">Save</button>
|
||||||
<button class="btn-red px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="deleteSprite">Delete</button>
|
<button class="btn-red px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="deleteSprite">Delete</button>
|
||||||
<div class="w-[calc(100%_+_32px)] absolute left-[-15px] bottom-0 h-px bg-gray-500"></div>
|
<button class="btn bg-indigo-500 hover:bg-indigo-600 rounded text-white px-4 py-2 flex-1 sm:flex-none" type="button" @click.prevent="copySprite">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -51,8 +55,8 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-full" v-if="action.isAnimated">
|
<div class="form-field-full" v-if="action.isAnimated">
|
||||||
<label for="frame-speed">Frame speed</label>
|
<label for="frame-speed">Frame rate</label>
|
||||||
<input v-model.number="action.frameSpeed" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame speed" />
|
<input v-model.number="action.frameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<SpriteActionsInput v-model="action.sprites" />
|
<SpriteActionsInput v-model="action.sprites" />
|
||||||
@ -65,13 +69,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Sprite, SpriteAction } from '@/types'
|
import type { Sprite, SpriteAction } from '@/application/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'
|
||||||
import Accordion from '@/components/utilities/Accordion.vue'
|
import Accordion from '@/components/utilities/Accordion.vue'
|
||||||
import SpriteActionsInput from '@/components/gameMaster/assetManager/partials/sprite/partials/SpriteImagesInput.vue'
|
import SpriteActionsInput from '@/components/gameMaster/assetManager/partials/sprite/partials/SpriteImagesInput.vue'
|
||||||
import { uuidv4 } from '@/utilities'
|
import { uuidv4 } from '@/application/utilities'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const assetManagerStore = useAssetManagerStore()
|
const assetManagerStore = useAssetManagerStore()
|
||||||
@ -87,7 +91,7 @@ if (!selectedSprite.value) {
|
|||||||
|
|
||||||
if (selectedSprite.value) {
|
if (selectedSprite.value) {
|
||||||
spriteName.value = selectedSprite.value.name
|
spriteName.value = selectedSprite.value.name
|
||||||
spriteActions.value = selectedSprite.value.spriteActions
|
spriteActions.value = sortSpriteActions(selectedSprite.value.spriteActions)
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteSprite() {
|
function deleteSprite() {
|
||||||
@ -100,6 +104,16 @@ function deleteSprite() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function copySprite() {
|
||||||
|
gameStore.connection?.emit('gm:sprite:copy', { id: selectedSprite.value?.id }, (response: boolean) => {
|
||||||
|
if (!response) {
|
||||||
|
console.error('Failed to copy sprite')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
refreshSpriteList(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function refreshSpriteList(unsetSelectedSprite = true) {
|
function refreshSpriteList(unsetSelectedSprite = true) {
|
||||||
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
||||||
assetManagerStore.setSpriteList(response)
|
assetManagerStore.setSpriteList(response)
|
||||||
@ -128,7 +142,7 @@ function saveSprite() {
|
|||||||
originY: action.originY,
|
originY: action.originY,
|
||||||
isAnimated: action.isAnimated,
|
isAnimated: action.isAnimated,
|
||||||
isLooping: action.isLooping,
|
isLooping: action.isLooping,
|
||||||
frameSpeed: action.frameSpeed,
|
frameRate: action.frameRate,
|
||||||
frameWidth: action.frameWidth,
|
frameWidth: action.frameWidth,
|
||||||
frameHeight: action.frameHeight
|
frameHeight: action.frameHeight
|
||||||
}
|
}
|
||||||
@ -148,7 +162,7 @@ function addNewImage() {
|
|||||||
if (!selectedSprite.value) return
|
if (!selectedSprite.value) return
|
||||||
|
|
||||||
const newImage: SpriteAction = {
|
const newImage: SpriteAction = {
|
||||||
id: uuidv4(), // Temporary ID, should be replaced by server-generated ID
|
id: uuidv4(),
|
||||||
spriteId: selectedSprite.value.id,
|
spriteId: selectedSprite.value.id,
|
||||||
sprite: selectedSprite.value,
|
sprite: selectedSprite.value,
|
||||||
action: 'new_action',
|
action: 'new_action',
|
||||||
@ -157,7 +171,7 @@ function addNewImage() {
|
|||||||
originY: 0,
|
originY: 0,
|
||||||
isAnimated: false,
|
isAnimated: false,
|
||||||
isLooping: false,
|
isLooping: false,
|
||||||
frameSpeed: 0,
|
frameRate: 0,
|
||||||
frameWidth: 0,
|
frameWidth: 0,
|
||||||
frameHeight: 0
|
frameHeight: 0
|
||||||
}
|
}
|
||||||
@ -166,13 +180,17 @@ function addNewImage() {
|
|||||||
spriteActions.value = []
|
spriteActions.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
spriteActions.value.push(newImage)
|
spriteActions.value = sortSpriteActions([...spriteActions.value, newImage])
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortSpriteActions(actions: SpriteAction[]): SpriteAction[] {
|
||||||
|
return [...actions].sort((a, b) => a.action.localeCompare(b.action))
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(selectedSprite, (sprite: Sprite | null) => {
|
watch(selectedSprite, (sprite: Sprite | null) => {
|
||||||
if (!sprite) return
|
if (!sprite) return
|
||||||
spriteName.value = sprite.name
|
spriteName.value = sprite.name
|
||||||
spriteActions.value = sprite.spriteActions
|
spriteActions.value = sortSpriteActions(sprite.spriteActions)
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
@ -1,35 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative p-2.5 flex items-center gap-x-2.5">
|
<div class="relative mb-5 flex items-center gap-x-2.5">
|
||||||
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
|
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
|
||||||
<button @click.prevent="newButtonClickHandler" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
|
<button @click.prevent="newButtonClickHandler" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2.5 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-bind="containerProps" class="overflow-y-auto relative" @scroll="onScroll">
|
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||||
<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 rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedSprite?.id === sprite.id }" @click="assetManagerStore.setSelectedSprite(sprite as Sprite)">
|
||||||
<div class="flex items-center gap-2.5">
|
<div class="flex items-center gap-2.5">
|
||||||
<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>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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">
|
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
||||||
<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 class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
||||||
</button>
|
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/config'
|
import config from '@/application/config'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { onMounted, ref, computed } from 'vue'
|
import { onMounted, ref, computed } from 'vue'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useVirtualList } from '@vueuse/core'
|
import { useVirtualList } from '@vueuse/core'
|
||||||
import type { Sprite } from '@/types'
|
import type { Sprite } from '@/application/types'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const assetManagerStore = useAssetManagerStore()
|
const assetManagerStore = useAssetManagerStore()
|
||||||
|
@ -1,14 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-wrap gap-3">
|
<div class="flex flex-wrap gap-3">
|
||||||
<div v-for="(image, index) in modelValue" :key="index" class="h-20 w-20 p-4 bg-gray-50 bg-opacity-50 rounded text-center relative group cursor-move" draggable="true" @dragstart="dragStart($event, index)" @dragover.prevent @dragenter.prevent @drop="drop($event, index)">
|
<div v-for="(image, index) in modelValue" :key="index" class="h-20 w-20 p-4 bg-gray-300 bg-opacity-50 rounded text-center relative group cursor-move" draggable="true" @dragstart="dragStart($event, index)" @dragover.prevent @dragenter.prevent @drop="drop($event, index)">
|
||||||
<img :src="image" class="max-w-full max-h-full object-contain pointer-events-none" alt="Uploaded image" />
|
<img :src="image" class="max-w-full max-h-full object-contain pointer-events-none" alt="Uploaded image" />
|
||||||
<button @click.stop="deleteImage(index)" class="absolute top-1 right-1 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" aria-label="Delete image">
|
<div class="absolute top-1 left-1 flex-row space-y-1">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<button @click.stop="deleteImage(index)" class="bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" aria-label="Delete image">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
</svg>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
</button>
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="bg-blue-500 text-white rounded-full w-6 h-6 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" aria-label="Scope image">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-20 w-20 p-4 bg-gray-100 bg-opacity-50 rounded justify-center items-center flex hover:cursor-pointer" @click="triggerFileInput" @drop.prevent="onDrop" @dragover.prevent>
|
<div class="h-20 w-20 p-4 bg-gray-200 bg-opacity-50 rounded justify-center items-center flex hover:cursor-pointer" @click="triggerFileInput" @drop.prevent="onDrop" @dragover.prevent>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 invert" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 invert" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||||
</svg>
|
</svg>
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full overflow-auto">
|
<div class="h-full overflow-auto">
|
||||||
<div class="relative p-2.5 flex flex-col items-center justify-between h-72">
|
<div class="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray">
|
||||||
<div class="filler"></div>
|
|
||||||
<img class="max-h-72" :src="`${config.server_endpoint}/assets/tiles/${selectedTile?.id}.png`" :alt="'Tile ' + selectedTile?.id" />
|
<img class="max-h-72" :src="`${config.server_endpoint}/assets/tiles/${selectedTile?.id}.png`" :alt="'Tile ' + selectedTile?.id" />
|
||||||
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="deleteTile">Delete</button>
|
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="m-2.5 p-2.5 block">
|
<div class="mt-5 block">
|
||||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveTile">
|
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveTile">
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="name">Name</label>
|
<label for="name">Name</label>
|
||||||
@ -16,19 +13,22 @@
|
|||||||
<label for="origin-x">Tags</label>
|
<label for="origin-x">Tags</label>
|
||||||
<ChipsInput v-model="tileTags" @update:modelValue="tileTags = $event" />
|
<ChipsInput v-model="tileTags" @update:modelValue="tileTags = $event" />
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
<div class="flex gap-4">
|
||||||
|
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
||||||
|
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="deleteTile">Delete</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Tile } from '@/types'
|
import type { Tile } from '@/application/types'
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import config from '@/config'
|
import config from '@/application/config'
|
||||||
import ChipsInput from '@/components/forms/ChipsInput.vue'
|
import ChipsInput from '@/components/forms/ChipsInput.vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
@ -1,38 +1,38 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative p-2.5 flex items-center gap-x-2.5">
|
<div class="relative mb-5 flex items-center gap-x-2.5">
|
||||||
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
|
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
|
||||||
<label for="upload-asset" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
|
<label for="upload-asset" class="bg-cyan text-white border border-solid border-white/25 rounded p-2.5 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
|
||||||
<input class="hidden" id="upload-asset" ref="tileUploadField" type="file" accept="image/png" multiple @change="handleFileUpload" />
|
<input class="hidden" id="upload-asset" ref="tileUploadField" type="file" accept="image/png" multiple @change="handleFileUpload" />
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||||
</svg>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-bind="containerProps" class="overflow-y-auto relative" @scroll="onScroll">
|
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||||
<div v-bind="wrapperProps" ref="elementToScroll">
|
<div v-bind="wrapperProps" ref="elementToScroll">
|
||||||
<a v-for="{ data: tile } in list" :key="tile.id" class="relative p-2.5 cursor-pointer block" :class="{ 'bg-cyan/80': assetManagerStore.selectedTile?.id === tile.id }" @click="assetManagerStore.setSelectedTile(tile)">
|
<a v-for="{ data: tile } in list" :key="tile.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedTile?.id === tile.id }" @click="assetManagerStore.setSelectedTile(tile)">
|
||||||
<div class="flex items-center gap-2.5">
|
<div class="flex items-center gap-2.5">
|
||||||
<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="group-hover:text-white" :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>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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">
|
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
||||||
<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 class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
||||||
</button>
|
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/config'
|
import config from '@/application/config'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { onMounted, ref, computed } from 'vue'
|
import { onMounted, ref, computed } from 'vue'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import type { Tile } from '@/types'
|
import type { Tile } from '@/application/types'
|
||||||
import { useVirtualList } from '@vueuse/core'
|
import { useVirtualList } from '@vueuse/core'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
@ -1,146 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Controls :layer="tiles" :depth="0" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import config from '@/config'
|
|
||||||
import { useScene } from 'phavuer'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
|
||||||
import { onBeforeMount, onBeforeUnmount } from 'vue'
|
|
||||||
import { createTileArray, getTile, placeTile, setLayerTiles } from '@/composables/zoneComposable'
|
|
||||||
import Controls from '@/components/utilities/Controls.vue'
|
|
||||||
|
|
||||||
const emit = defineEmits(['tilemap:create'])
|
|
||||||
|
|
||||||
const scene = useScene()
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
|
||||||
|
|
||||||
const zoneTilemap = createTilemap()
|
|
||||||
const tiles = createTileLayer()
|
|
||||||
|
|
||||||
function createTilemap() {
|
|
||||||
const zoneData = new Phaser.Tilemaps.MapData({
|
|
||||||
width: zoneEditorStore.zone?.width,
|
|
||||||
height: zoneEditorStore.zone?.height,
|
|
||||||
tileWidth: config.tile_size.x,
|
|
||||||
tileHeight: config.tile_size.y,
|
|
||||||
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
|
|
||||||
format: Phaser.Tilemaps.Formats.ARRAY_2D
|
|
||||||
})
|
|
||||||
const tilemap = new Phaser.Tilemaps.Tilemap(scene, zoneData)
|
|
||||||
emit('tilemap:create', tilemap)
|
|
||||||
return tilemap
|
|
||||||
}
|
|
||||||
|
|
||||||
function createTileLayer() {
|
|
||||||
const tilesetImages = gameStore.assets.filter((asset) => asset.group === 'tiles').map((asset, index) => zoneTilemap.addTilesetImage(asset.key, asset.key, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y }))
|
|
||||||
tilesetImages.push(zoneTilemap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 1, 2, 0, { x: 0, y: -config.tile_size.y }))
|
|
||||||
|
|
||||||
const layer = zoneTilemap.createBlankLayer('tiles', tilesetImages as any, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
|
|
||||||
|
|
||||||
layer.setDepth(0)
|
|
||||||
layer.setCullPadding(2, 2)
|
|
||||||
|
|
||||||
return layer
|
|
||||||
}
|
|
||||||
|
|
||||||
function pencil(pointer: Phaser.Input.Pointer) {
|
|
||||||
// Check if zone is set
|
|
||||||
if (!zoneEditorStore.zone) return
|
|
||||||
|
|
||||||
// Check if tool is pencil
|
|
||||||
if (zoneEditorStore.tool !== 'pencil') return
|
|
||||||
|
|
||||||
// Check if draw mode is tile
|
|
||||||
if (zoneEditorStore.drawMode !== 'tile') return
|
|
||||||
|
|
||||||
// Check if there is a selected tile
|
|
||||||
if (!zoneEditorStore.selectedTile) return
|
|
||||||
|
|
||||||
// Check if left mouse button is pressed
|
|
||||||
if (!pointer.isDown) return
|
|
||||||
|
|
||||||
// Check if shift is not pressed, this means we are moving the camera
|
|
||||||
if (pointer.event.shiftKey) return
|
|
||||||
|
|
||||||
// Check if there is a tile
|
|
||||||
const tile = getTile(tiles, pointer.worldX, pointer.worldY)
|
|
||||||
if (!tile) return
|
|
||||||
|
|
||||||
// Place tile
|
|
||||||
placeTile(zoneTilemap, tiles, tile.x, tile.y, zoneEditorStore.selectedTile.id)
|
|
||||||
|
|
||||||
// Adjust zoneEditorStore.zone.tiles
|
|
||||||
zoneEditorStore.zone.tiles[tile.y][tile.x] = zoneEditorStore.selectedTile.id
|
|
||||||
}
|
|
||||||
|
|
||||||
function eraser(pointer: Phaser.Input.Pointer) {
|
|
||||||
// Check if zone is set
|
|
||||||
if (!zoneEditorStore.zone) return
|
|
||||||
|
|
||||||
// Check if tool is pencil
|
|
||||||
if (zoneEditorStore.tool !== 'eraser') return
|
|
||||||
|
|
||||||
// Check if draw mode is tile
|
|
||||||
if (zoneEditorStore.eraserMode !== 'tile') return
|
|
||||||
|
|
||||||
// Check if left mouse button is pressed
|
|
||||||
if (!pointer.isDown) return
|
|
||||||
|
|
||||||
// Check if shift is not pressed, this means we are moving the camera
|
|
||||||
if (pointer.event.shiftKey) return
|
|
||||||
|
|
||||||
// Check if there is a tile
|
|
||||||
const tile = getTile(tiles, pointer.worldX, pointer.worldY)
|
|
||||||
if (!tile) return
|
|
||||||
|
|
||||||
// Place tile
|
|
||||||
placeTile(zoneTilemap, tiles, tile.x, tile.y, 'blank_tile')
|
|
||||||
|
|
||||||
// Adjust zoneEditorStore.zone.tiles
|
|
||||||
zoneEditorStore.zone.tiles[tile.y][tile.x] = 'blank_tile'
|
|
||||||
}
|
|
||||||
|
|
||||||
function paint(pointer: Phaser.Input.Pointer) {
|
|
||||||
// Check if zone is set
|
|
||||||
if (!zoneEditorStore.zone) return
|
|
||||||
|
|
||||||
// Check if tool is pencil
|
|
||||||
if (zoneEditorStore.tool !== 'paint') return
|
|
||||||
|
|
||||||
// Check if there is a selected tile
|
|
||||||
if (!zoneEditorStore.selectedTile) return
|
|
||||||
|
|
||||||
// Check if left mouse button is pressed
|
|
||||||
if (!pointer.isDown) return
|
|
||||||
|
|
||||||
// Set new tileArray with selected tile
|
|
||||||
setLayerTiles(zoneTilemap, tiles, createTileArray(zoneTilemap.width, zoneTilemap.height, zoneEditorStore.selectedTile.id))
|
|
||||||
|
|
||||||
// Adjust zoneEditorStore.zone.tiles
|
|
||||||
zoneEditorStore.zone.tiles = createTileArray(zoneTilemap.width, zoneTilemap.height, zoneEditorStore.selectedTile.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
onBeforeMount(() => {
|
|
||||||
if (!zoneEditorStore.zone?.tiles) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setLayerTiles(zoneTilemap, tiles, zoneEditorStore.zone.tiles)
|
|
||||||
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_DOWN, paint)
|
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
|
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
|
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_DOWN, paint)
|
|
||||||
|
|
||||||
zoneTilemap.destroyLayer('tiles')
|
|
||||||
zoneTilemap.removeAllLayers()
|
|
||||||
zoneTilemap.destroy()
|
|
||||||
})
|
|
||||||
</script>
|
|
@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<Tiles @tilemap:create="tileMap = $event" />
|
<ZoneTiles @tileMap:create="tileMap = $event" />
|
||||||
<Objects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
<ZoneObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
||||||
<EventTiles 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 />
|
||||||
@ -17,8 +17,7 @@
|
|||||||
import { onUnmounted, ref } from 'vue'
|
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 '@/application/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,15 +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 Tiles from '@/components/gameMaster/zoneEditor/Tiles.vue'
|
import ZoneTiles from '@/components/gameMaster/zoneEditor/zonePartials/ZoneTiles.vue'
|
||||||
import Objects from '@/components/gameMaster/zoneEditor/Objects.vue'
|
import ZoneObjects from '@/components/gameMaster/zoneEditor/zonePartials/ZoneObjects.vue'
|
||||||
import EventTiles from '@/components/gameMaster/zoneEditor/EventTiles.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
|
||||||
|
|
||||||
|
@ -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" :bg-style="'none'">
|
||||||
<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>
|
||||||
@ -40,7 +40,7 @@ import { ref } from 'vue'
|
|||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
import type { Zone } from '@/types'
|
import type { Zone } from '@/application/types'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
|
@ -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)" :bg-style="'none'">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="text-lg text-white">Objects</h3>
|
<h3 class="text-lg text-white">Objects</h3>
|
||||||
</template>
|
</template>
|
||||||
@ -41,24 +41,19 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/config'
|
import config from '@/application/config'
|
||||||
import { ref, onMounted, computed, watch } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import type { Object } from '@/types'
|
import type { Object, ZoneObject } from '@/application/types'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const isModalOpen = ref(false)
|
const isModalOpen = ref(false)
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
// const objectDepth = ref(0)
|
|
||||||
const selectedTags = ref<string[]>([])
|
const selectedTags = ref<string[]>([])
|
||||||
|
|
||||||
// watch(objectDepth, (depth) => {
|
|
||||||
// zoneEditorStore.setObjectDepth(depth)
|
|
||||||
// })
|
|
||||||
|
|
||||||
const uniqueTags = computed(() => {
|
const uniqueTags = computed(() => {
|
||||||
const allTags = zoneEditorStore.objectList.flatMap((obj) => obj.tags || [])
|
const allTags = zoneEditorStore.objectList.flatMap((obj) => obj.tags || [])
|
||||||
return Array.from(new Set(allTags))
|
return Array.from(new Set(allTags))
|
||||||
@ -81,8 +76,6 @@ const toggleTag = (tag: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
zoneEditorStore.setObjectDepth(0)
|
|
||||||
|
|
||||||
isModalOpen.value = true
|
isModalOpen.value = true
|
||||||
gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => {
|
gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => {
|
||||||
zoneEditorStore.setObjectList(response)
|
zoneEditorStore.setObjectList(response)
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ZoneObject } from '@/types'
|
import type { ZoneObject } from '@/application/types'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
zoneObject: ZoneObject
|
zoneObject: ZoneObject
|
||||||
|
@ -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" :bg-style="'none'">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="m-0 font-medium shrink-0 text-white">Teleport settings</h3>
|
<h3 class="m-0 font-medium shrink-0 text-white">Teleport settings</h3>
|
||||||
</template>
|
</template>
|
||||||
@ -43,7 +43,7 @@ import { computed, onMounted, ref, watch } from 'vue'
|
|||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import type { Zone } from '@/types'
|
import type { Zone } from '@/application/types'
|
||||||
|
|
||||||
const showTeleportModal = computed(() => zoneEditorStore.tool === 'pencil' && zoneEditorStore.drawMode === 'teleport')
|
const showTeleportModal = computed(() => zoneEditorStore.tool === 'pencil' && zoneEditorStore.drawMode === 'teleport')
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
|
@ -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)" :bg-style="'none'">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="text-lg text-white">Tiles</h3>
|
<h3 class="text-lg text-white">Tiles</h3>
|
||||||
</template>
|
</template>
|
||||||
@ -52,7 +52,7 @@
|
|||||||
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
|
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
|
||||||
:src="`${config.server_endpoint}/assets/tiles/${selectedGroup.parent.id}.png`"
|
:src="`${config.server_endpoint}/assets/tiles/${selectedGroup.parent.id}.png`"
|
||||||
:alt="selectedGroup.parent.name"
|
:alt="selectedGroup.parent.name"
|
||||||
@click="selectTile(selectedGroup.parent)"
|
@click="selectTile(selectedGroup.parent.id)"
|
||||||
:class="{
|
:class="{
|
||||||
'border-cyan shadow-lg scale-105': isActiveTile(selectedGroup.parent),
|
'border-cyan shadow-lg scale-105': isActiveTile(selectedGroup.parent),
|
||||||
'border-transparent hover:border-gray-300': !isActiveTile(selectedGroup.parent)
|
'border-transparent hover:border-gray-300': !isActiveTile(selectedGroup.parent)
|
||||||
@ -65,7 +65,7 @@
|
|||||||
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
|
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
|
||||||
:src="`${config.server_endpoint}/assets/tiles/${childTile.id}.png`"
|
:src="`${config.server_endpoint}/assets/tiles/${childTile.id}.png`"
|
||||||
:alt="childTile.name"
|
:alt="childTile.name"
|
||||||
@click="selectTile(childTile)"
|
@click="selectTile(childTile.id)"
|
||||||
:class="{
|
:class="{
|
||||||
'border-cyan shadow-lg scale-105': isActiveTile(childTile),
|
'border-cyan shadow-lg scale-105': isActiveTile(childTile),
|
||||||
'border-transparent hover:border-gray-300': !isActiveTile(childTile)
|
'border-transparent hover:border-gray-300': !isActiveTile(childTile)
|
||||||
@ -81,12 +81,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/config'
|
import config from '@/application/config'
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import type { Tile } from '@/types'
|
import type { Tile } from '@/application/types'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const isModalOpen = ref(false)
|
const isModalOpen = ref(false)
|
||||||
@ -218,12 +218,12 @@ function closeGroup() {
|
|||||||
selectedGroup.value = null
|
selectedGroup.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectTile(tile: Tile) {
|
function selectTile(tile: string) {
|
||||||
zoneEditorStore.setSelectedTile(tile)
|
zoneEditorStore.setSelectedTile(tile)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 () => {
|
||||||
|
@ -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" :bg-style="'none'">
|
||||||
<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>
|
||||||
@ -29,7 +29,7 @@
|
|||||||
import { onMounted } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import type { Zone } from '@/types'
|
import type { Zone } from '@/application/types'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
import CreateZone from '@/components/gameMaster/zoneEditor/partials/CreateZone.vue'
|
import CreateZone from '@/components/gameMaster/zoneEditor/partials/CreateZone.vue'
|
||||||
|
|
||||||
|
@ -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="430" :bg-style="'none'">
|
||||||
<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>
|
||||||
|
@ -3,12 +3,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { type ZoneEventTile, ZoneEventTileType } from '@/types'
|
import { type ZoneEventTile, ZoneEventTileType } from '@/application/types'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
import { Image, useScene } from 'phavuer'
|
import { Image, useScene } from 'phavuer'
|
||||||
import { getTile, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
import { getTile, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||||
import { uuidv4 } from '@/utilities'
|
import { uuidv4 } from '@/application/utilities'
|
||||||
import { onBeforeMount, onBeforeUnmount } from 'vue'
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
|
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
@ -102,14 +102,14 @@ function eraser(pointer: Phaser.Input.Pointer) {
|
|||||||
zoneEditorStore.zone.zoneEventTiles = zoneEditorStore.zone.zoneEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id)
|
zoneEditorStore.zone.zoneEventTiles = zoneEditorStore.zone.zoneEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onMounted(() => {
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_DOWN, pencil)
|
scene.input.on(Phaser.Input.Events.POINTER_DOWN, pencil)
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
|
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_DOWN, eraser)
|
scene.input.on(Phaser.Input.Events.POINTER_DOWN, eraser)
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
|
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onUnmounted(() => {
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_DOWN, pencil)
|
scene.input.off(Phaser.Input.Events.POINTER_DOWN, pencil)
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
|
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_DOWN, eraser)
|
scene.input.off(Phaser.Input.Events.POINTER_DOWN, eraser)
|
@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<Image v-if="gameStore.getLoadedAsset(props.zoneObject.object.id)" v-bind="imageProps" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { Image, useScene } from 'phavuer'
|
||||||
|
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||||
|
import { loadTexture } from '@/composables/gameComposable'
|
||||||
|
import type { AssetDataT, ZoneObject } from '@/application/types'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
tilemap: Phaser.Tilemaps.Tilemap
|
||||||
|
zoneObject: ZoneObject
|
||||||
|
selectedZoneObject: ZoneObject | null
|
||||||
|
movingZoneObject: ZoneObject | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const scene = useScene()
|
||||||
|
|
||||||
|
const imageProps = computed(() => ({
|
||||||
|
alpha: props.movingZoneObject?.id === props.zoneObject.id ? 0.5 : 1,
|
||||||
|
tint: props.selectedZoneObject?.id === props.zoneObject.id ? 0x00ff00 : 0xffffff,
|
||||||
|
depth: calculateIsometricDepth(props.zoneObject.positionX, props.zoneObject.positionY, props.zoneObject.object.frameWidth, props.zoneObject.object.frameHeight),
|
||||||
|
x: tileToWorldX(props.tilemap, props.zoneObject.positionX, props.zoneObject.positionY),
|
||||||
|
y: tileToWorldY(props.tilemap, props.zoneObject.positionX, props.zoneObject.positionY),
|
||||||
|
flipX: props.zoneObject.isRotated,
|
||||||
|
texture: props.zoneObject.object.id,
|
||||||
|
originY: Number(props.zoneObject.object.originX),
|
||||||
|
originX: Number(props.zoneObject.object.originY)
|
||||||
|
}))
|
||||||
|
|
||||||
|
loadTexture(scene, {
|
||||||
|
key: props.zoneObject.object.id,
|
||||||
|
data: '/assets/objects/' + props.zoneObject.object.id + '.png',
|
||||||
|
group: 'objects',
|
||||||
|
updatedAt: props.zoneObject.object.updatedAt,
|
||||||
|
frameWidth: props.zoneObject.object.frameWidth,
|
||||||
|
frameHeight: props.zoneObject.object.frameHeight
|
||||||
|
} as AssetDataT).catch((error) => {
|
||||||
|
console.error('Error loading texture:', error)
|
||||||
|
})
|
||||||
|
</script>
|
@ -1,40 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<SelectedZoneObject v-if="selectedZoneObject" :zoneObject="selectedZoneObject" @move="moveZoneObject" @rotate="rotateZoneObject" @delete="deleteZoneObject" />
|
<SelectedZoneObject v-if="selectedZoneObject" :zoneObject="selectedZoneObject" :movingZoneObject="movingZoneObject" @move="moveZoneObject" @rotate="rotateZoneObject" @delete="deleteZoneObject" />
|
||||||
<Image v-for="object in zoneEditorStore.zone?.zoneObjects" v-bind="getImageProps(object)" @pointerup="() => (selectedZoneObject = object)" />
|
<ZoneObject v-for="zoneObject in zoneEditorStore.zone?.zoneObjects" :tilemap="tilemap" :zoneObject :selectedZoneObject :movingZoneObject @pointerup="clickZoneObject(zoneObject)" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { uuidv4 } from '@/utilities'
|
import { uuidv4 } from '@/application/utilities'
|
||||||
import { calculateIsometricDepth, getTile, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
import { getTile } from '@/composables/zoneComposable'
|
||||||
import { Image, useScene } from 'phavuer'
|
import { useScene } from 'phavuer'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
import type { ZoneObject } from '@/types'
|
|
||||||
import SelectedZoneObject from '@/components/gameMaster/zoneEditor/partials/SelectedZoneObject.vue'
|
import SelectedZoneObject from '@/components/gameMaster/zoneEditor/partials/SelectedZoneObject.vue'
|
||||||
import { onBeforeMount, onBeforeUnmount, ref, watch } from 'vue'
|
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
|
import ZoneObject from '@/components/gameMaster/zoneEditor/zonePartials/ZoneObject.vue'
|
||||||
|
import type { ZoneObject as ZoneObjectT } from '@/application/types'
|
||||||
|
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
const selectedZoneObject = ref<ZoneObject | null>(null)
|
const selectedZoneObject = ref<ZoneObjectT | null>(null)
|
||||||
const movingZoneObject = ref<ZoneObject | null>(null)
|
const movingZoneObject = ref<ZoneObjectT | null>(null)
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
tilemap: Phaser.Tilemaps.Tilemap
|
tilemap: Phaser.Tilemaps.Tilemap
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
function getImageProps(zoneObject: ZoneObject) {
|
|
||||||
return {
|
|
||||||
alpha: zoneObject.id === movingZoneObject.value?.id ? 0.5 : 1,
|
|
||||||
depth: calculateIsometricDepth(zoneObject.positionX, zoneObject.positionY, zoneObject.object.frameWidth, zoneObject.object.frameHeight),
|
|
||||||
tint: selectedZoneObject.value?.id === zoneObject.id ? 0x00ff00 : 0xffffff,
|
|
||||||
x: tileToWorldX(props.tilemap, zoneObject.positionX, zoneObject.positionY),
|
|
||||||
y: tileToWorldY(props.tilemap, zoneObject.positionX, zoneObject.positionY),
|
|
||||||
flipX: zoneObject.isRotated,
|
|
||||||
texture: zoneObject.object.id,
|
|
||||||
originY: Number(zoneObject.object.originX),
|
|
||||||
originX: Number(zoneObject.object.originY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function pencil(pointer: Phaser.Input.Pointer) {
|
function pencil(pointer: Phaser.Input.Pointer) {
|
||||||
// Check if zone is set
|
// Check if zone is set
|
||||||
if (!zoneEditorStore.zone) return
|
if (!zoneEditorStore.zone) return
|
||||||
@ -54,6 +41,9 @@ function pencil(pointer: Phaser.Input.Pointer) {
|
|||||||
// Check if shift is not pressed, this means we are moving the camera
|
// Check if shift is not pressed, this means we are moving the camera
|
||||||
if (pointer.event.shiftKey) return
|
if (pointer.event.shiftKey) return
|
||||||
|
|
||||||
|
// Check if alt is pressed, this means we are selecting the object
|
||||||
|
if (pointer.event.altKey) return
|
||||||
|
|
||||||
// Check if there is a tile
|
// Check if there is a tile
|
||||||
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
|
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
|
||||||
if (!tile) return
|
if (!tile) return
|
||||||
@ -75,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) {
|
||||||
@ -94,6 +84,9 @@ function eraser(pointer: Phaser.Input.Pointer) {
|
|||||||
// Check if shift is not pressed, this means we are moving the camera
|
// Check if shift is not pressed, this means we are moving the camera
|
||||||
if (pointer.event.shiftKey) return
|
if (pointer.event.shiftKey) return
|
||||||
|
|
||||||
|
// Check if alt is pressed, this means we are selecting the object
|
||||||
|
if (pointer.event.altKey) return
|
||||||
|
|
||||||
// Check if there is a tile
|
// Check if there is a tile
|
||||||
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
|
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
|
||||||
if (!tile) return
|
if (!tile) return
|
||||||
@ -106,11 +99,42 @@ function eraser(pointer: Phaser.Input.Pointer) {
|
|||||||
zoneEditorStore.zone.zoneObjects = zoneEditorStore.zone.zoneObjects.filter((object) => object.id !== existingObject.id)
|
zoneEditorStore.zone.zoneObjects = zoneEditorStore.zone.zoneObjects.filter((object) => object.id !== existingObject.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function objectPicker(pointer: Phaser.Input.Pointer) {
|
||||||
|
// Check if zone is set
|
||||||
|
if (!zoneEditorStore.zone) return
|
||||||
|
|
||||||
|
// Check if tool is pencil
|
||||||
|
if (zoneEditorStore.tool !== 'pencil') return
|
||||||
|
|
||||||
|
// Check if draw mode is object
|
||||||
|
if (zoneEditorStore.drawMode !== 'object') return
|
||||||
|
|
||||||
|
// Check if left mouse button is pressed
|
||||||
|
if (!pointer.isDown) return
|
||||||
|
|
||||||
|
// Check if shift is not pressed, this means we are moving the camera
|
||||||
|
if (pointer.event.shiftKey) return
|
||||||
|
|
||||||
|
// If alt is not pressed, return
|
||||||
|
if (!pointer.event.altKey) return
|
||||||
|
|
||||||
|
// Check if there is a tile
|
||||||
|
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
|
||||||
|
if (!tile) return
|
||||||
|
|
||||||
|
// Check if object already exists on position
|
||||||
|
const existingObject = zoneEditorStore.zone.zoneObjects.find((object) => object.positionX === tile.x && object.positionY === tile.y)
|
||||||
|
if (!existingObject) return
|
||||||
|
|
||||||
|
// Select the object
|
||||||
|
zoneEditorStore.setSelectedObject(existingObject)
|
||||||
|
}
|
||||||
|
|
||||||
function moveZoneObject(id: string) {
|
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
|
||||||
@ -155,18 +179,29 @@ function deleteZoneObject(id: string) {
|
|||||||
selectedZoneObject.value = null
|
selectedZoneObject.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeMount(() => {
|
function clickZoneObject(zoneObject: ZoneObjectT) {
|
||||||
|
selectedZoneObject.value = zoneObject
|
||||||
|
|
||||||
|
// If alt is pressed, select the object
|
||||||
|
if (scene.input.activePointer.event.altKey) {
|
||||||
|
zoneEditorStore.setSelectedObject(zoneObject.object)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_DOWN, pencil)
|
scene.input.on(Phaser.Input.Events.POINTER_DOWN, pencil)
|
||||||
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_DOWN, eraser)
|
scene.input.on(Phaser.Input.Events.POINTER_DOWN, eraser)
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
|
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_DOWN, objectPicker)
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onUnmounted(() => {
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_DOWN, pencil)
|
scene.input.off(Phaser.Input.Events.POINTER_DOWN, pencil)
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
|
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_DOWN, eraser)
|
scene.input.off(Phaser.Input.Events.POINTER_DOWN, eraser)
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
|
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_DOWN, objectPicker)
|
||||||
})
|
})
|
||||||
|
|
||||||
// watch zoneEditorStore.objectList and update originX and originY of objects in zoneObjects
|
// watch zoneEditorStore.objectList and update originX and originY of objects in zoneObjects
|
227
src/components/gameMaster/zoneEditor/zonePartials/ZoneTiles.vue
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
<template>
|
||||||
|
<Controls :layer="tileLayer" :depth="0" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import config from '@/application/config'
|
||||||
|
import { useScene } from 'phavuer'
|
||||||
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
|
import { onMounted, onUnmounted, watch } from 'vue'
|
||||||
|
import { createTileArray, getTile, placeTile, setLayerTiles } from '@/composables/zoneComposable'
|
||||||
|
import Controls from '@/components/utilities/Controls.vue'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import type { AssetDataT } from '@/application/types'
|
||||||
|
|
||||||
|
const emit = defineEmits(['tileMap:create'])
|
||||||
|
|
||||||
|
const scene = useScene()
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
|
const tileMap = createTileMap()
|
||||||
|
const tileLayer = createTileLayer()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Tilemap is a container for Tilemap data.
|
||||||
|
* This isn't a display object, rather, it holds data about the map and allows you to add tilesets and tilemap layers to it.
|
||||||
|
* A map can have one or more tilemap layers, which are the display objects that actually render the tiles.
|
||||||
|
*/
|
||||||
|
function createTileMap() {
|
||||||
|
const zoneData = new Phaser.Tilemaps.MapData({
|
||||||
|
width: zoneEditorStore.zone?.width,
|
||||||
|
height: zoneEditorStore.zone?.height,
|
||||||
|
tileWidth: config.tile_size.x,
|
||||||
|
tileHeight: config.tile_size.y,
|
||||||
|
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
|
||||||
|
format: Phaser.Tilemaps.Formats.ARRAY_2D
|
||||||
|
})
|
||||||
|
|
||||||
|
const newTileMap = new Phaser.Tilemaps.Tilemap(scene, zoneData)
|
||||||
|
emit('tileMap:create', newTileMap)
|
||||||
|
|
||||||
|
return newTileMap
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Tileset is a combination of a single image containing the tiles and a container for data about each tile.
|
||||||
|
*/
|
||||||
|
function createTileLayer() {
|
||||||
|
const tilesArray = gameStore.getLoadedAssetsByGroup('tiles')
|
||||||
|
|
||||||
|
const tilesetImages = Array.from(tilesArray).map((tile: AssetDataT, index: number) => {
|
||||||
|
return tileMap.addTilesetImage(tile.key, tile.key, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y })
|
||||||
|
}) as any
|
||||||
|
|
||||||
|
// Add blank tile
|
||||||
|
tilesetImages.push(tileMap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 1, 2, 0, { x: 0, y: -config.tile_size.y }))
|
||||||
|
const layer = tileMap.createBlankLayer('tiles', tilesetImages, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
|
||||||
|
|
||||||
|
layer.setDepth(0)
|
||||||
|
layer.setCullPadding(2, 2)
|
||||||
|
|
||||||
|
return layer
|
||||||
|
}
|
||||||
|
|
||||||
|
function pencil(pointer: Phaser.Input.Pointer) {
|
||||||
|
// Check if zone is set
|
||||||
|
if (!zoneEditorStore.zone) return
|
||||||
|
|
||||||
|
// Check if tool is pencil
|
||||||
|
if (zoneEditorStore.tool !== 'pencil') return
|
||||||
|
|
||||||
|
// Check if draw mode is tile
|
||||||
|
if (zoneEditorStore.drawMode !== 'tile') return
|
||||||
|
|
||||||
|
// Check if there is a selected tile
|
||||||
|
if (!zoneEditorStore.selectedTile) return
|
||||||
|
|
||||||
|
// Check if left mouse button is pressed
|
||||||
|
if (!pointer.isDown) return
|
||||||
|
|
||||||
|
// Check if shift is not pressed, this means we are moving the camera
|
||||||
|
if (pointer.event.shiftKey) return
|
||||||
|
|
||||||
|
// Check if there is a tile
|
||||||
|
const tile = getTile(tileLayer, pointer.worldX, pointer.worldY)
|
||||||
|
if (!tile) return
|
||||||
|
|
||||||
|
// Place tile
|
||||||
|
placeTile(tileMap, tileLayer, tile.x, tile.y, zoneEditorStore.selectedTile)
|
||||||
|
|
||||||
|
// Adjust zoneEditorStore.zone.tiles
|
||||||
|
zoneEditorStore.zone.tiles[tile.y][tile.x] = zoneEditorStore.selectedTile
|
||||||
|
}
|
||||||
|
|
||||||
|
function eraser(pointer: Phaser.Input.Pointer) {
|
||||||
|
// Check if zone is set
|
||||||
|
if (!zoneEditorStore.zone) return
|
||||||
|
|
||||||
|
// Check if tool is pencil
|
||||||
|
if (zoneEditorStore.tool !== 'eraser') return
|
||||||
|
|
||||||
|
// Check if draw mode is tile
|
||||||
|
if (zoneEditorStore.eraserMode !== 'tile') return
|
||||||
|
|
||||||
|
// Check if left mouse button is pressed
|
||||||
|
if (!pointer.isDown) return
|
||||||
|
|
||||||
|
// Check if shift is not pressed, this means we are moving the camera
|
||||||
|
if (pointer.event.shiftKey) return
|
||||||
|
|
||||||
|
// Check if alt is pressed
|
||||||
|
if (pointer.event.altKey) return
|
||||||
|
|
||||||
|
// Check if there is a tile
|
||||||
|
const tile = getTile(tileLayer, pointer.worldX, pointer.worldY)
|
||||||
|
if (!tile) return
|
||||||
|
|
||||||
|
// Place tile
|
||||||
|
placeTile(tileMap, tileLayer, tile.x, tile.y, 'blank_tile')
|
||||||
|
|
||||||
|
// Adjust zoneEditorStore.zone.tiles
|
||||||
|
zoneEditorStore.zone.tiles[tile.y][tile.x] = 'blank_tile'
|
||||||
|
}
|
||||||
|
|
||||||
|
function paint(pointer: Phaser.Input.Pointer) {
|
||||||
|
// Check if zone is set
|
||||||
|
if (!zoneEditorStore.zone) return
|
||||||
|
|
||||||
|
// Check if tool is pencil
|
||||||
|
if (zoneEditorStore.tool !== 'paint') return
|
||||||
|
|
||||||
|
// Check if there is a selected tile
|
||||||
|
if (!zoneEditorStore.selectedTile) return
|
||||||
|
|
||||||
|
// Check if left mouse button is pressed
|
||||||
|
if (!pointer.isDown) return
|
||||||
|
|
||||||
|
// Check if shift is not pressed, this means we are moving the camera
|
||||||
|
if (pointer.event.shiftKey) return
|
||||||
|
|
||||||
|
// Check if alt is pressed
|
||||||
|
if (pointer.event.altKey) return
|
||||||
|
|
||||||
|
// Set new tileArray with selected tile
|
||||||
|
setLayerTiles(tileMap, tileLayer, createTileArray(tileMap.width, tileMap.height, zoneEditorStore.selectedTile))
|
||||||
|
|
||||||
|
// Adjust zoneEditorStore.zone.tiles
|
||||||
|
zoneEditorStore.zone.tiles = createTileArray(tileMap.width, tileMap.height, zoneEditorStore.selectedTile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// When alt is pressed, and the pointer is down, select the tile that the pointer is over
|
||||||
|
function tilePicker(pointer: Phaser.Input.Pointer) {
|
||||||
|
// Check if zone is set
|
||||||
|
if (!zoneEditorStore.zone) return
|
||||||
|
|
||||||
|
// Check if tool is pencil
|
||||||
|
if (zoneEditorStore.tool !== 'pencil') return
|
||||||
|
|
||||||
|
// Check if draw mode is tile
|
||||||
|
if (zoneEditorStore.drawMode !== 'tile') return
|
||||||
|
|
||||||
|
// Check if left mouse button is pressed
|
||||||
|
if (!pointer.isDown) return
|
||||||
|
|
||||||
|
// Check if shift is not pressed, this means we are moving the camera
|
||||||
|
if (pointer.event.shiftKey) return
|
||||||
|
|
||||||
|
// Check if alt is pressed
|
||||||
|
if (!pointer.event.altKey) return
|
||||||
|
|
||||||
|
// Check if there is a tile
|
||||||
|
const tile = getTile(tileLayer, pointer.worldX, pointer.worldY)
|
||||||
|
if (!tile) return
|
||||||
|
|
||||||
|
// Select the tile
|
||||||
|
zoneEditorStore.setSelectedTile(zoneEditorStore.zone.tiles[tile.y][tile.x])
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => zoneEditorStore.shouldClearTiles,
|
||||||
|
(shouldClear) => {
|
||||||
|
if (shouldClear && zoneEditorStore.zone) {
|
||||||
|
const blankTiles = createTileArray(tileMap.width, tileMap.height, 'blank_tile')
|
||||||
|
setLayerTiles(tileMap, tileLayer, blankTiles)
|
||||||
|
zoneEditorStore.zone.tiles = blankTiles
|
||||||
|
zoneEditorStore.resetClearTilesFlag()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!zoneEditorStore.zone?.tiles) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// First fill the entire map with blank tiles using current zone dimensions
|
||||||
|
const blankTiles = createTileArray(zoneEditorStore.zone.width, zoneEditorStore.zone.height, 'blank_tile')
|
||||||
|
|
||||||
|
// Then overlay the zone tiles, but only within the current zone dimensions
|
||||||
|
const zoneTiles = zoneEditorStore.zone.tiles
|
||||||
|
for (let y = 0; y < zoneEditorStore.zone.height; y++) {
|
||||||
|
for (let x = 0; x < zoneEditorStore.zone.width; x++) {
|
||||||
|
// Only copy if the source tiles array has this position
|
||||||
|
if (zoneTiles[y] && zoneTiles[y][x] !== undefined) {
|
||||||
|
blankTiles[y][x] = zoneTiles[y][x]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setLayerTiles(tileMap, tileLayer, blankTiles)
|
||||||
|
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_DOWN, paint)
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_DOWN, tilePicker)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_DOWN, paint)
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_DOWN, tilePicker)
|
||||||
|
|
||||||
|
tileMap.destroyLayer('tiles')
|
||||||
|
tileMap.removeAllLayers()
|
||||||
|
tileMap.destroy()
|
||||||
|
})
|
||||||
|
</script>
|
@ -1,115 +1,117 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative" v-if="gameStore.uiSettings.isCharacterProfileOpen" :style="modalStyle">
|
<div class="absolute" v-if="gameStore.uiSettings.isCharacterProfileOpen" :style="modalStyle">
|
||||||
<img src="/assets/ui-elements/profile-ui-box-outer.svg" class="absolute w-full h-full" />
|
<div class="relative">
|
||||||
<img src="/assets/ui-elements/profile-ui-box-inner.svg" class="absolute left-2 bottom-2 w-[calc(100%_-_16px)] h-[calc(100%_-_40px)]" />
|
<img src="/assets/ui-elements/profile-ui-box-outer.svg" class="absolute w-full h-full" />
|
||||||
<div @mousedown="startDrag" class="cursor-move px-5 py-2.5 flex justify-between items-center relative">
|
<img src="/assets/ui-elements/profile-ui-box-inner.svg" class="absolute left-2 bottom-2 w-[calc(100%_-_16px)] h-[calc(100%_-_40px)]" />
|
||||||
<span class="text-xs text-white font-thin">Character Profile [Alt+C]</span>
|
<div @mousedown="startDrag" class="cursor-move px-5 py-2.5 flex justify-between items-center relative">
|
||||||
<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">
|
<span class="text-xs text-white font-thin">Character Profile [Alt+C]</span>
|
||||||
<img alt="close" draggable="false" src="/assets/icons/close-button-white.svg" class="w-full h-full" />
|
<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>
|
<img alt="close" draggable="false" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" />
|
||||||
</div>
|
</button>
|
||||||
<div class="py-4 px-6 flex flex-col gap-7 relative z-10">
|
|
||||||
<div class="flex flex-col gap-2.5">
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<div>
|
|
||||||
<p class="text-sm m-0 font-bold text-white tracking-wide">{{ gameStore.character?.name }}</p>
|
|
||||||
<span class="text-xs">{{ gameStore.character?.experience }} / 18.600XP</span>
|
|
||||||
</div>
|
|
||||||
<a class="hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-small.svg')] bg-no-repeat block w-8 h-8 relative mx-[3px]">
|
|
||||||
<img class="hover:drop-shadow-default w-3.5 h-3.5 m-[9px] object-contain" draggable="false" src="/assets/icons/plus-green-icon.svg" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<div class="flex flex-col gap-0.5">
|
|
||||||
<div class="w-9 h-9 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600">
|
|
||||||
<span class="absolute w-full top-1/2 -translate-y-1/2 text-[6px] text-center">CROWN</span>
|
|
||||||
</div>
|
|
||||||
<div class="w-9 h-9 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600">
|
|
||||||
<span class="absolute w-full top-1/2 -translate-y-1/2 text-[6px] text-center">R-HAND</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-0.5 items-end">
|
|
||||||
<div class="w-9 h-9 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600">
|
|
||||||
<span class="absolute w-full top-1/2 -translate-y-1/2 text-[6px] text-center">L-HAND</span>
|
|
||||||
</div>
|
|
||||||
<div class="w-6 h-6 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600">
|
|
||||||
<span class="absolute w-full top-1/2 -translate-y-1/2 text-[6px] text-center">RING</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<img src="/assets/placeholders/inventory_player.png" class="w-8 h-auto" />
|
|
||||||
<div class="flex flex-col items-end gap-0.5">
|
|
||||||
<div class="w-9 h-9 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600">
|
|
||||||
<img class="absolute w-6 h-6 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/profile/helmet.svg" />
|
|
||||||
</div>
|
|
||||||
<div class="w-9 h-9 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600">
|
|
||||||
<img class="absolute w-6 h-6 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/profile/chestplate.svg" />
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-0.5 items-end">
|
|
||||||
<div class="w-6 h-6 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600">
|
|
||||||
<img class="absolute w-4 h-4 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/profile/boots.svg" />
|
|
||||||
</div>
|
|
||||||
<div class="w-9 h-9 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600">
|
|
||||||
<img class="absolute w-6 h-6 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/profile/legs.svg" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<div class="w-[105px] h-px mb-[3px] flex justify-between">
|
|
||||||
<div class="w-px h-full bg-gray-300"></div>
|
|
||||||
<div class="w-[101px] h-full bg-gray-300"></div>
|
|
||||||
<div class="w-px h-full bg-gray-300"></div>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-11">
|
|
||||||
<p class="m-0 text-xs text-white tracking-wide">Health</p>
|
|
||||||
<span class="m-0 text-xs text-white tracking-wide">+ 15</span>
|
|
||||||
</div>
|
|
||||||
<div class="w-[105px] h-px my-[3px] flex justify-between">
|
|
||||||
<div class="w-px h-full bg-gray-300"></div>
|
|
||||||
<div class="w-[101px] h-full bg-gray-300"></div>
|
|
||||||
<div class="w-px h-full bg-gray-300"></div>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-11">
|
|
||||||
<p class="m-0 text-xs text-white tracking-wide">Health</p>
|
|
||||||
<span class="m-0 text-xs text-white tracking-wide">+ 15</span>
|
|
||||||
</div>
|
|
||||||
<div class="w-[105px] h-px mt-[3px] flex justify-between">
|
|
||||||
<div class="w-px h-full bg-gray-300"></div>
|
|
||||||
<div class="w-[101px] h-full bg-gray-300"></div>
|
|
||||||
<div class="w-px h-full bg-gray-300"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<div class="w-[105px] h-px mb-[3px] flex justify-between">
|
|
||||||
<div class="w-px h-full bg-gray-300"></div>
|
|
||||||
<div class="w-[101px] h-full bg-gray-300"></div>
|
|
||||||
<div class="w-px h-full bg-gray-300"></div>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-11">
|
|
||||||
<p class="m-0 text-xs text-white tracking-wide">Health</p>
|
|
||||||
<span class="m-0 text-xs text-white tracking-wide">+ 15</span>
|
|
||||||
</div>
|
|
||||||
<div class="w-[105px] h-px my-[3px] flex justify-between">
|
|
||||||
<div class="w-px h-full bg-gray-300"></div>
|
|
||||||
<div class="w-[101px] h-full bg-gray-300"></div>
|
|
||||||
<div class="w-px h-full bg-gray-300"></div>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-11">
|
|
||||||
<p class="m-0 text-xs text-white tracking-wide">Health</p>
|
|
||||||
<span class="m-0 text-xs text-white tracking-wide">+ 15</span>
|
|
||||||
</div>
|
|
||||||
<div class="w-[105px] h-px mt-[3px] flex justify-between">
|
|
||||||
<div class="w-px h-full bg-gray-300"></div>
|
|
||||||
<div class="w-[101px] h-full bg-gray-300"></div>
|
|
||||||
<div class="w-px h-full bg-gray-300"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-rows-4 grid-cols-6 gap-0.5">
|
<div class="py-4 px-6 flex flex-col gap-7 relative z-10">
|
||||||
<div v-for="n in 24" class="w-9 h-9 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600"></div>
|
<div class="flex flex-col gap-2.5">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm m-0 font-bold text-white tracking-wide">{{ gameStore.character?.name }}</p>
|
||||||
|
<span class="text-xs">{{ gameStore.character?.experience }} / 18.600XP</span>
|
||||||
|
</div>
|
||||||
|
<a class="hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured-small.svg')] bg-no-repeat block w-8 h-8 relative mx-[3px]">
|
||||||
|
<img class="hover:drop-shadow-default w-3.5 h-3.5 m-[9px] object-contain" draggable="false" src="/assets/icons/plus-green-icon.svg" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div class="flex flex-col gap-0.5">
|
||||||
|
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
|
||||||
|
<span class="absolute w-full top-1/2 -translate-y-1/2 text-[6px] text-center">CROWN</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
|
||||||
|
<span class="absolute w-full top-1/2 -translate-y-1/2 text-[6px] text-center">R-HAND</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-0.5 items-end">
|
||||||
|
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
|
||||||
|
<span class="absolute w-full top-1/2 -translate-y-1/2 text-[6px] text-center">L-HAND</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-6 h-6 default-border rounded-sm bg-gray relative hover:bg-gray-600">
|
||||||
|
<span class="absolute w-full top-1/2 -translate-y-1/2 text-[6px] text-center">RING</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<img src="/assets/placeholders/inventory_player.png" class="w-8 h-auto" />
|
||||||
|
<div class="flex flex-col items-end gap-0.5">
|
||||||
|
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
|
||||||
|
<img class="w-6 h-6 center-element" src="/assets/icons/profile/helmet.svg" />
|
||||||
|
</div>
|
||||||
|
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
|
||||||
|
<img class="w-6 h-6 center-element" src="/assets/icons/profile/chestplate.svg" />
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-0.5 items-end">
|
||||||
|
<div class="w-6 h-6 default-border rounded-sm bg-gray relative hover:bg-gray-600">
|
||||||
|
<img class="w-4 h-4 center-element" src="/assets/icons/profile/boots.svg" />
|
||||||
|
</div>
|
||||||
|
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
|
||||||
|
<img class="w-6 h-6 center-element" src="/assets/icons/profile/legs.svg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="w-[105px] h-px mb-[3px] flex justify-between">
|
||||||
|
<div class="w-px h-full bg-gray-300"></div>
|
||||||
|
<div class="w-[101px] h-full bg-gray-300"></div>
|
||||||
|
<div class="w-px h-full bg-gray-300"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-11">
|
||||||
|
<p class="m-0 text-xs text-white tracking-wide">Health</p>
|
||||||
|
<span class="m-0 text-xs text-white tracking-wide">+ 15</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-[105px] h-px my-[3px] flex justify-between">
|
||||||
|
<div class="w-px h-full bg-gray-300"></div>
|
||||||
|
<div class="w-[101px] h-full bg-gray-300"></div>
|
||||||
|
<div class="w-px h-full bg-gray-300"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-11">
|
||||||
|
<p class="m-0 text-xs text-white tracking-wide">Health</p>
|
||||||
|
<span class="m-0 text-xs text-white tracking-wide">+ 15</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-[105px] h-px mt-[3px] flex justify-between">
|
||||||
|
<div class="w-px h-full bg-gray-300"></div>
|
||||||
|
<div class="w-[101px] h-full bg-gray-300"></div>
|
||||||
|
<div class="w-px h-full bg-gray-300"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="w-[105px] h-px mb-[3px] flex justify-between">
|
||||||
|
<div class="w-px h-full bg-gray-300"></div>
|
||||||
|
<div class="w-[101px] h-full bg-gray-300"></div>
|
||||||
|
<div class="w-px h-full bg-gray-300"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-11">
|
||||||
|
<p class="m-0 text-xs text-white tracking-wide">Health</p>
|
||||||
|
<span class="m-0 text-xs text-white tracking-wide">+ 15</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-[105px] h-px my-[3px] flex justify-between">
|
||||||
|
<div class="w-px h-full bg-gray-300"></div>
|
||||||
|
<div class="w-[101px] h-full bg-gray-300"></div>
|
||||||
|
<div class="w-px h-full bg-gray-300"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-11">
|
||||||
|
<p class="m-0 text-xs text-white tracking-wide">Health</p>
|
||||||
|
<span class="m-0 text-xs text-white tracking-wide">+ 15</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-[105px] h-px mt-[3px] flex justify-between">
|
||||||
|
<div class="w-px h-full bg-gray-300"></div>
|
||||||
|
<div class="w-[101px] h-full bg-gray-300"></div>
|
||||||
|
<div class="w-px h-full bg-gray-300"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-rows-4 grid-cols-6 gap-0.5">
|
||||||
|
<div v-for="n in 24" class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -168,8 +170,8 @@ function stopDrag() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function adjustPosition() {
|
function adjustPosition() {
|
||||||
x.value = Math.max(0, Math.min(x.value, window.innerWidth - width.value))
|
x.value = Math.min(x.value, window.innerWidth - width.value)
|
||||||
y.value = Math.max(0, Math.min(y.value, window.innerHeight - height.value))
|
y.value = Math.min(y.value, window.innerHeight - height.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializePosition() {
|
function initializePosition() {
|
||||||
@ -236,6 +238,7 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
removeEventListener('keydown', keyPress)
|
||||||
removeEventListener('mousemove', drag)
|
removeEventListener('mousemove', drag)
|
||||||
removeEventListener('mouseup', stopDrag)
|
removeEventListener('mouseup', stopDrag)
|
||||||
})
|
})
|
||||||
|