Compare commits
68 Commits
feature/ch
...
feature/#1
Author | SHA1 | Date | |
---|---|---|---|
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=Sylvan Quest
|
||||
VITE_DEVELOPMENT=true
|
||||
VITE_SERVER_ENDPOINT=http://localhost:4000
|
||||
VITE_TILE_SIZE_X=64
|
||||
|
745
package-lock.json
generated
@ -50,6 +50,7 @@
|
||||
"tailwindcss": "^3.4.13",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^5.4.9",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-vue-devtools": "^7.5.2",
|
||||
"vitest": "^2.0.3",
|
||||
"vue-tsc": "^1.6.5"
|
||||
|
@ -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 |
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 |
@ -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 |
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 |
Before Width: | Height: | Size: 301 KiB After Width: | Height: | Size: 301 KiB |
Before Width: | Height: | Size: 400 KiB After Width: | Height: | Size: 400 KiB |
46
src/App.vue
@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<Notifications />
|
||||
<GmTools v-if="gameStore.character?.role === 'gm'" />
|
||||
<BackgroundImageLoader />
|
||||
<GmPanel v-if="gameStore.character?.role === 'gm'" />
|
||||
|
||||
<component :is="currentScreen" />
|
||||
</template>
|
||||
|
||||
@ -10,24 +9,51 @@
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
import Notifications from '@/components/utilities/Notifications.vue'
|
||||
import GmTools from '@/components/gameMaster/GmTools.vue'
|
||||
import BackgroundImageLoader from '@/components/utilities/BackgroundImageLoader.vue'
|
||||
import GmPanel from '@/components/gameMaster/GmPanel.vue'
|
||||
import Login from '@/screens/Login.vue'
|
||||
import Characters from '@/screens/Characters.vue'
|
||||
import Game from '@/screens/Game.vue'
|
||||
// import Loading from '@/screens/Loading.vue'
|
||||
import ZoneEditor from '@/screens/ZoneEditor.vue'
|
||||
import { computed } from 'vue'
|
||||
import Login from '@/components/screens/Login.vue'
|
||||
import Characters from '@/components/screens/Characters.vue'
|
||||
import Game from '@/components/screens/Game.vue'
|
||||
import ZoneEditor from '@/components/screens/ZoneEditor.vue'
|
||||
import { computed, watch } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const zoneEditorStore = useZoneEditorStore()
|
||||
|
||||
const currentScreen = computed(() => {
|
||||
// if (!gameStore.isAssetsLoaded) return Loading
|
||||
if (!gameStore.connection) return Login
|
||||
if (!gameStore.token) return Login
|
||||
if (!gameStore.character) return Characters
|
||||
if (zoneEditorStore.active) return ZoneEditor
|
||||
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
|
||||
if (event.repeat || event.isComposing || event.defaultPrevented) return
|
||||
|
||||
if (event.key === 'G') {
|
||||
gameStore.toggleGmPanel()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
@ -111,6 +111,7 @@ button {
|
||||
@apply text-gray-50 border-2 border-solid border-gray-500 text-base leading-5 rounded py-2.5;
|
||||
|
||||
&.active,
|
||||
&.selected,
|
||||
&:hover {
|
||||
@apply bg-gray-700 border-gray-700;
|
||||
}
|
||||
@ -123,6 +124,16 @@ button {
|
||||
&.eye-open {
|
||||
@apply bg-[url('/assets/icons/eye-closed.svg')] w-5 h-4 right-2.5;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
@apply bg-gray-500 border-gray-400;
|
||||
}
|
||||
}
|
||||
|
||||
.character {
|
||||
&.active {
|
||||
@apply pr-px border-r-0;
|
||||
}
|
||||
}
|
||||
|
||||
.text-pixel {
|
||||
@ -133,6 +144,15 @@ button {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
.scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
@apply block w-0.5 bg-gray-300 rounded-sm;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
@apply bg-gray-175;
|
||||
}
|
||||
}
|
||||
|
||||
canvas {
|
||||
image-rendering: -moz-crisp-edges;
|
||||
image-rendering: -webkit-crisp-edges;
|
||||
|
@ -1,21 +1,41 @@
|
||||
<template>
|
||||
<Scene name="effects" @preload="preloadScene" @create="createScene" @update="updateScene"> </Scene>
|
||||
<Scene name="effects" @preload="preloadScene" @create="createScene" @update="updateScene" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Scene } from 'phavuer'
|
||||
import { useZoneStore } from '@/stores/zoneStore'
|
||||
import { onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import type { WeatherState } from '@/types'
|
||||
|
||||
// Constants
|
||||
const SUNRISE_HOUR = 6
|
||||
const SUNSET_HOUR = 20
|
||||
const DAY_STRENGTH = 100
|
||||
const NIGHT_STRENGTH = 30
|
||||
|
||||
// Stores
|
||||
const gameStore = useGameStore()
|
||||
const zoneStore = useZoneStore()
|
||||
|
||||
// Scene ref
|
||||
const sceneRef = ref<Phaser.Scene | null>(null)
|
||||
|
||||
// Effect-related refs
|
||||
// Effect refs
|
||||
const lightEffect = ref<Phaser.GameObjects.Graphics | null>(null)
|
||||
const rainEmitter = ref<Phaser.GameObjects.Particles.ParticleEmitter | null>(null)
|
||||
const fogSprite = ref<Phaser.GameObjects.Sprite | null>(null)
|
||||
|
||||
// State refs
|
||||
const weatherState = ref<WeatherState>({
|
||||
isRainEnabled: false,
|
||||
rainPercentage: 0,
|
||||
isFogEnabled: false,
|
||||
fogDensity: 0
|
||||
})
|
||||
|
||||
// Scene lifecycle methods
|
||||
const preloadScene = async (scene: Phaser.Scene) => {
|
||||
scene.load.image('raindrop', 'assets/raindrop.png')
|
||||
scene.load.image('fog', 'assets/fog.png')
|
||||
@ -23,15 +43,21 @@ const preloadScene = async (scene: Phaser.Scene) => {
|
||||
|
||||
const createScene = async (scene: Phaser.Scene) => {
|
||||
sceneRef.value = scene
|
||||
createLightEffect(scene)
|
||||
createRainEffect(scene)
|
||||
createFogEffect(scene)
|
||||
setupEffects(scene)
|
||||
setupSocketListeners()
|
||||
}
|
||||
|
||||
const updateScene = () => {
|
||||
updateEffects()
|
||||
}
|
||||
|
||||
// Effect setup
|
||||
const setupEffects = (scene: Phaser.Scene) => {
|
||||
createLightEffect(scene)
|
||||
createRainEffect(scene)
|
||||
createFogEffect(scene)
|
||||
}
|
||||
|
||||
const createLightEffect = (scene: Phaser.Scene) => {
|
||||
lightEffect.value = scene.add.graphics()
|
||||
lightEffect.value.setDepth(1000)
|
||||
@ -59,14 +85,55 @@ const createFogEffect = (scene: Phaser.Scene) => {
|
||||
fogSprite.value.setDepth(950)
|
||||
}
|
||||
|
||||
// Lighting calculations
|
||||
const calculateLightStrength = (time: Date): number => {
|
||||
const hour = time.getHours()
|
||||
const minute = time.getMinutes()
|
||||
|
||||
let strength = DAY_STRENGTH
|
||||
|
||||
// Night time (10 PM - 6 AM)
|
||||
if (hour >= SUNSET_HOUR || hour < SUNRISE_HOUR) {
|
||||
strength = NIGHT_STRENGTH
|
||||
}
|
||||
// Full daylight (7 AM - 7 PM)
|
||||
else if (hour > SUNRISE_HOUR && hour < SUNSET_HOUR - 2) {
|
||||
strength = DAY_STRENGTH
|
||||
}
|
||||
// Sunrise transition (6 AM - 7 AM)
|
||||
else if (hour === SUNRISE_HOUR) {
|
||||
strength = NIGHT_STRENGTH + ((DAY_STRENGTH - NIGHT_STRENGTH) * minute) / 60
|
||||
}
|
||||
// Sunset transition (8 PM - 10 PM)
|
||||
else if (hour >= SUNSET_HOUR - 2 && hour < SUNSET_HOUR) {
|
||||
const totalMinutes = (hour - (SUNSET_HOUR - 2)) * 60 + minute
|
||||
const transitionProgress = totalMinutes / 120 // 2 hours = 120 minutes
|
||||
strength = DAY_STRENGTH - (DAY_STRENGTH - NIGHT_STRENGTH) * transitionProgress
|
||||
}
|
||||
|
||||
return strength
|
||||
}
|
||||
|
||||
// Effect updates
|
||||
const updateEffects = () => {
|
||||
const effects = zoneStore.zone?.zoneEffects || []
|
||||
|
||||
if (effects.length > 0) {
|
||||
updateZoneEffects(effects)
|
||||
} else {
|
||||
// Make sure we're getting the current time
|
||||
const lightStrength = calculateLightStrength(gameStore.world.date)
|
||||
updateLightEffect(lightStrength)
|
||||
updateWeatherEffects()
|
||||
}
|
||||
}
|
||||
|
||||
const updateZoneEffects = (effects: any[]) => {
|
||||
// Always update light based on time when zone effects are present
|
||||
updateLightEffect(calculateLightStrength(gameStore.world.date))
|
||||
|
||||
effects.forEach((effect) => {
|
||||
switch (effect.effect) {
|
||||
case 'light':
|
||||
updateLightEffect(effect.strength)
|
||||
break
|
||||
case 'rain':
|
||||
updateRainEffect(effect.strength)
|
||||
break
|
||||
@ -77,6 +144,11 @@ const updateEffects = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const updateWeatherEffects = () => {
|
||||
updateRainEffect(weatherState.value.isRainEnabled ? weatherState.value.rainPercentage : 0)
|
||||
updateFogEffect(weatherState.value.isFogEnabled ? weatherState.value.fogDensity * 100 : 0)
|
||||
}
|
||||
|
||||
const updateLightEffect = (strength: number) => {
|
||||
if (!lightEffect.value) return
|
||||
const darkness = 1 - strength / 100
|
||||
@ -100,11 +172,47 @@ const updateFogEffect = (strength: number) => {
|
||||
fogSprite.value.setAlpha(strength / 100)
|
||||
}
|
||||
|
||||
// Socket handlers
|
||||
const setupSocketListeners = () => {
|
||||
// Initial weather state
|
||||
gameStore.connection?.emit('weather', (response: WeatherState) => {
|
||||
if (zoneStore.zone?.zoneEffects) return
|
||||
weatherState.value = response
|
||||
updateEffects()
|
||||
})
|
||||
|
||||
// Weather updates
|
||||
gameStore.connection?.on('weather', (data: WeatherState) => {
|
||||
weatherState.value = data
|
||||
updateEffects()
|
||||
})
|
||||
|
||||
// Time updates
|
||||
gameStore.connection?.on('date', () => {
|
||||
if (zoneStore.zone?.zoneEffects) return
|
||||
updateEffects()
|
||||
})
|
||||
}
|
||||
|
||||
const updateEffectWindowSize = () => {
|
||||
if(rainEmitter.value) rainEmitter.value.updateConfig({x: {min: 0, max: window.innerWidth}})
|
||||
if(fogSprite.value) {
|
||||
fogSprite.value.setX(window.innerWidth / 2)
|
||||
fogSprite.value.setY(window.innerHeight / 2)
|
||||
}
|
||||
}
|
||||
|
||||
// Watchers
|
||||
watch(() => zoneStore.zone?.zoneEffects, updateEffects, { deep: true })
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (sceneRef.value) sceneRef.value.scene.remove('effects')
|
||||
onMounted(() => {
|
||||
window.addEventListener("resize", updateEffectWindowSize);
|
||||
})
|
||||
|
||||
// @TODO : Fix resize issue
|
||||
// Cleanup
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", updateEffectWindowSize);
|
||||
if (sceneRef.value) sceneRef.value.scene.remove('effects')
|
||||
gameStore.connection?.off('weather')
|
||||
})
|
||||
</script>
|
||||
|
@ -6,6 +6,7 @@
|
||||
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Users</button>
|
||||
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Chats</button>
|
||||
<button @mousedown.stop class="btn-cyan active py-1.5 px-4 min-w-24">Asset manager</button>
|
||||
<button class="btn-cyan py-1.5 px-4 min-w-24" type="button" @click="() => zoneEditorStore.toggleActive()">Zone manager</button>
|
||||
</div>
|
||||
</template>
|
||||
<template #modalBody>
|
||||
@ -21,8 +22,10 @@ import { ref } from 'vue'
|
||||
import Modal from '@/components/utilities/Modal.vue'
|
||||
import AssetManager from '@/components/gameMaster/assetManager/AssetManager.vue'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const zoneEditorStore = useZoneEditorStore()
|
||||
|
||||
let toggle = ref('asset-manager')
|
||||
</script>
|
||||
|
@ -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,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>
|
||||
<Tiles @tilemap:create="tileMap = $event" />
|
||||
<Objects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
||||
<EventTiles v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
||||
<ZoneTiles @tileMap:create="tileMap = $event" />
|
||||
<ZoneObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
||||
<ZoneEventTiles v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
||||
|
||||
<Toolbar @save="save" />
|
||||
<Toolbar @save="save" @clear="clear" />
|
||||
|
||||
<ZoneList />
|
||||
<TileList />
|
||||
@ -18,7 +18,6 @@ import { onUnmounted, ref } from 'vue'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
import { type Zone } from '@/types'
|
||||
|
||||
// Components
|
||||
import Toolbar from '@/components/gameMaster/zoneEditor/partials/Toolbar.vue'
|
||||
import TileList from '@/components/gameMaster/zoneEditor/partials/TileList.vue'
|
||||
@ -26,15 +25,24 @@ import ObjectList from '@/components/gameMaster/zoneEditor/partials/ObjectList.v
|
||||
import ZoneSettings from '@/components/gameMaster/zoneEditor/partials/ZoneSettings.vue'
|
||||
import ZoneList from '@/components/gameMaster/zoneEditor/partials/ZoneList.vue'
|
||||
import TeleportModal from '@/components/gameMaster/zoneEditor/partials/TeleportModal.vue'
|
||||
import Tiles from '@/components/gameMaster/zoneEditor/Tiles.vue'
|
||||
import Objects from '@/components/gameMaster/zoneEditor/Objects.vue'
|
||||
import EventTiles from '@/components/gameMaster/zoneEditor/EventTiles.vue'
|
||||
import ZoneTiles from '@/components/gameMaster/zoneEditor/zonePartials/ZoneTiles.vue'
|
||||
import ZoneObjects from '@/components/gameMaster/zoneEditor/zonePartials/ZoneObjects.vue'
|
||||
import ZoneEventTiles from '@/components/gameMaster/zoneEditor/zonePartials/ZoneEventTiles.vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const zoneEditorStore = useZoneEditorStore()
|
||||
|
||||
const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null)
|
||||
|
||||
function clear() {
|
||||
if (!zoneEditorStore.zone) return
|
||||
|
||||
// Clear objects, event tiles and tiles
|
||||
zoneEditorStore.zone.zoneObjects = []
|
||||
zoneEditorStore.zone.zoneEventTiles = []
|
||||
zoneEditorStore.triggerClearTiles()
|
||||
}
|
||||
|
||||
function save() {
|
||||
if (!zoneEditorStore.zone) return
|
||||
|
||||
@ -55,6 +63,7 @@ function save() {
|
||||
}
|
||||
|
||||
gameStore.connection?.emit('gm:zone_editor:zone:update', data, (response: Zone) => {
|
||||
console.log(response.updatedAt)
|
||||
zoneEditorStore.setZone(response)
|
||||
})
|
||||
}
|
||||
|
@ -42,23 +42,18 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import config from '@/config'
|
||||
import { ref, onMounted, computed, watch } from 'vue'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import Modal from '@/components/utilities/Modal.vue'
|
||||
import type { Object } from '@/types'
|
||||
import type { Object, ZoneObject } from '@/types'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const isModalOpen = ref(false)
|
||||
const zoneEditorStore = useZoneEditorStore()
|
||||
const searchQuery = ref('')
|
||||
// const objectDepth = ref(0)
|
||||
const selectedTags = ref<string[]>([])
|
||||
|
||||
// watch(objectDepth, (depth) => {
|
||||
// zoneEditorStore.setObjectDepth(depth)
|
||||
// })
|
||||
|
||||
const uniqueTags = computed(() => {
|
||||
const allTags = zoneEditorStore.objectList.flatMap((obj) => obj.tags || [])
|
||||
return Array.from(new Set(allTags))
|
||||
@ -81,8 +76,6 @@ const toggleTag = (tag: string) => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
zoneEditorStore.setObjectDepth(0)
|
||||
|
||||
isModalOpen.value = true
|
||||
gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => {
|
||||
zoneEditorStore.setObjectList(response)
|
||||
|
@ -52,7 +52,7 @@
|
||||
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
|
||||
:src="`${config.server_endpoint}/assets/tiles/${selectedGroup.parent.id}.png`"
|
||||
:alt="selectedGroup.parent.name"
|
||||
@click="selectTile(selectedGroup.parent)"
|
||||
@click="selectTile(selectedGroup.parent.id)"
|
||||
:class="{
|
||||
'border-cyan shadow-lg scale-105': 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"
|
||||
:src="`${config.server_endpoint}/assets/tiles/${childTile.id}.png`"
|
||||
:alt="childTile.name"
|
||||
@click="selectTile(childTile)"
|
||||
@click="selectTile(childTile.id)"
|
||||
:class="{
|
||||
'border-cyan shadow-lg scale-105': isActiveTile(childTile),
|
||||
'border-transparent hover:border-gray-300': !isActiveTile(childTile)
|
||||
@ -218,12 +218,12 @@ function closeGroup() {
|
||||
selectedGroup.value = null
|
||||
}
|
||||
|
||||
function selectTile(tile: Tile) {
|
||||
function selectTile(tile: string) {
|
||||
zoneEditorStore.setSelectedTile(tile)
|
||||
}
|
||||
|
||||
function isActiveTile(tile: Tile): boolean {
|
||||
return zoneEditorStore.selectedTile?.id === tile.id
|
||||
return zoneEditorStore.selectedTile === tile.id
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
|
@ -8,7 +8,7 @@ import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
import { Image, useScene } from 'phavuer'
|
||||
import { getTile, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||
import { uuidv4 } from '@/utilities'
|
||||
import { onBeforeMount, onBeforeUnmount } from 'vue'
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const scene = useScene()
|
||||
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)
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
onMounted(() => {
|
||||
scene.input.on(Phaser.Input.Events.POINTER_DOWN, pencil)
|
||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
|
||||
scene.input.on(Phaser.Input.Events.POINTER_DOWN, eraser)
|
||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
onUnmounted(() => {
|
||||
scene.input.off(Phaser.Input.Events.POINTER_DOWN, pencil)
|
||||
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
|
||||
scene.input.off(Phaser.Input.Events.POINTER_DOWN, eraser)
|
@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<Image v-if="gameStore.getLoadedAsset(props.zoneObject.object.id)" v-bind="imageProps" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { Image, useScene } from 'phavuer'
|
||||
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||
import { loadTexture } from '@/composables/gameComposable'
|
||||
import type { AssetDataT, ZoneObject } from '@/types'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
|
||||
const props = defineProps<{
|
||||
tilemap: Phaser.Tilemaps.Tilemap
|
||||
zoneObject: ZoneObject
|
||||
selectedZoneObject: ZoneObject | null
|
||||
movingZoneObject: ZoneObject | null
|
||||
}>()
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const scene = useScene()
|
||||
|
||||
const imageProps = computed(() => ({
|
||||
alpha: props.movingZoneObject?.id === props.zoneObject.id ? 0.5 : 1,
|
||||
tint: props.selectedZoneObject?.id === props.zoneObject.id ? 0x00ff00 : 0xffffff,
|
||||
depth: calculateIsometricDepth(props.zoneObject.positionX, props.zoneObject.positionY, props.zoneObject.object.frameWidth, props.zoneObject.object.frameHeight),
|
||||
x: tileToWorldX(props.tilemap, props.zoneObject.positionX, props.zoneObject.positionY),
|
||||
y: tileToWorldY(props.tilemap, props.zoneObject.positionX, props.zoneObject.positionY),
|
||||
flipX: props.zoneObject.isRotated,
|
||||
texture: props.zoneObject.object.id,
|
||||
originY: Number(props.zoneObject.object.originX),
|
||||
originX: Number(props.zoneObject.object.originY)
|
||||
}))
|
||||
|
||||
loadTexture(scene, {
|
||||
key: props.zoneObject.object.id,
|
||||
data: '/assets/objects/' + props.zoneObject.object.id + '.png',
|
||||
group: 'objects',
|
||||
updatedAt: props.zoneObject.object.updatedAt,
|
||||
frameWidth: props.zoneObject.object.frameWidth,
|
||||
frameHeight: props.zoneObject.object.frameHeight
|
||||
} as AssetDataT).catch((error) => {
|
||||
console.error('Error loading texture:', error)
|
||||
})
|
||||
</script>
|
@ -1,40 +1,27 @@
|
||||
<template>
|
||||
<SelectedZoneObject v-if="selectedZoneObject" :zoneObject="selectedZoneObject" @move="moveZoneObject" @rotate="rotateZoneObject" @delete="deleteZoneObject" />
|
||||
<Image v-for="object in zoneEditorStore.zone?.zoneObjects" v-bind="getImageProps(object)" @pointerup="() => (selectedZoneObject = object)" />
|
||||
<SelectedZoneObject v-if="selectedZoneObject" :zoneObject="selectedZoneObject" :movingZoneObject="movingZoneObject" @move="moveZoneObject" @rotate="rotateZoneObject" @delete="deleteZoneObject" />
|
||||
<ZoneObject v-for="zoneObject in zoneEditorStore.zone?.zoneObjects" :tilemap="tilemap" :zoneObject :selectedZoneObject :movingZoneObject @pointerup="clickZoneObject(zoneObject)" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { uuidv4 } from '@/utilities'
|
||||
import { calculateIsometricDepth, getTile, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||
import { Image, useScene } from 'phavuer'
|
||||
import { getTile } from '@/composables/zoneComposable'
|
||||
import { useScene } from 'phavuer'
|
||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
import type { ZoneObject } from '@/types'
|
||||
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 '@/types'
|
||||
|
||||
const scene = useScene()
|
||||
const zoneEditorStore = useZoneEditorStore()
|
||||
const selectedZoneObject = ref<ZoneObject | null>(null)
|
||||
const movingZoneObject = ref<ZoneObject | null>(null)
|
||||
const selectedZoneObject = ref<ZoneObjectT | null>(null)
|
||||
const movingZoneObject = ref<ZoneObjectT | null>(null)
|
||||
|
||||
const props = defineProps<{
|
||||
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) {
|
||||
// Check if zone is set
|
||||
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
|
||||
if (pointer.event.shiftKey) return
|
||||
|
||||
// Check if alt is pressed, this means we are selecting the object
|
||||
if (pointer.event.altKey) return
|
||||
|
||||
// Check if there is a tile
|
||||
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
|
||||
if (!tile) return
|
||||
@ -66,7 +56,7 @@ function pencil(pointer: Phaser.Input.Pointer) {
|
||||
id: uuidv4(),
|
||||
zoneId: zoneEditorStore.zone.id,
|
||||
zone: zoneEditorStore.zone,
|
||||
objectId: zoneEditorStore.selectedObject.id,
|
||||
objectId: zoneEditorStore.selectedObject,
|
||||
object: zoneEditorStore.selectedObject,
|
||||
depth: 0,
|
||||
isRotated: false,
|
||||
@ -94,6 +84,9 @@ function eraser(pointer: Phaser.Input.Pointer) {
|
||||
// Check if shift is not pressed, this means we are moving the camera
|
||||
if (pointer.event.shiftKey) return
|
||||
|
||||
// Check if alt is pressed, this means we are selecting the object
|
||||
if (pointer.event.altKey) return
|
||||
|
||||
// Check if there is a tile
|
||||
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
|
||||
if (!tile) return
|
||||
@ -106,6 +99,37 @@ function eraser(pointer: Phaser.Input.Pointer) {
|
||||
zoneEditorStore.zone.zoneObjects = zoneEditorStore.zone.zoneObjects.filter((object) => object.id !== existingObject.id)
|
||||
}
|
||||
|
||||
function objectPicker(pointer: Phaser.Input.Pointer) {
|
||||
// Check if zone is set
|
||||
if (!zoneEditorStore.zone) return
|
||||
|
||||
// Check if tool is pencil
|
||||
if (zoneEditorStore.tool !== 'pencil') return
|
||||
|
||||
// Check if draw mode is object
|
||||
if (zoneEditorStore.drawMode !== 'object') return
|
||||
|
||||
// Check if left mouse button is pressed
|
||||
if (!pointer.isDown) return
|
||||
|
||||
// Check if shift is not pressed, this means we are moving the camera
|
||||
if (pointer.event.shiftKey) return
|
||||
|
||||
// If alt is not pressed, return
|
||||
if (!pointer.event.altKey) return
|
||||
|
||||
// Check if there is a tile
|
||||
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
|
||||
if (!tile) return
|
||||
|
||||
// Check if object already exists on position
|
||||
const existingObject = zoneEditorStore.zone.zoneObjects.find((object) => object.positionX === tile.x && object.positionY === tile.y)
|
||||
if (!existingObject) return
|
||||
|
||||
// Select the object
|
||||
zoneEditorStore.setSelectedObject(existingObject)
|
||||
}
|
||||
|
||||
function moveZoneObject(id: string) {
|
||||
// Check if zone is set
|
||||
if (!zoneEditorStore.zone) return
|
||||
@ -155,18 +179,29 @@ function deleteZoneObject(id: string) {
|
||||
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_MOVE, pencil)
|
||||
scene.input.on(Phaser.Input.Events.POINTER_DOWN, eraser)
|
||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
|
||||
scene.input.on(Phaser.Input.Events.POINTER_DOWN, objectPicker)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
onUnmounted(() => {
|
||||
scene.input.off(Phaser.Input.Events.POINTER_DOWN, pencil)
|
||||
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
|
||||
scene.input.off(Phaser.Input.Events.POINTER_DOWN, eraser)
|
||||
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
|
||||
scene.input.off(Phaser.Input.Events.POINTER_DOWN, objectPicker)
|
||||
})
|
||||
|
||||
// watch zoneEditorStore.objectList and update originX and originY of objects in zoneObjects
|
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 '@/config'
|
||||
import { useScene } from 'phavuer'
|
||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
import { onMounted, onUnmounted, watch } from 'vue'
|
||||
import { createTileArray, getTile, placeTile, setLayerTiles } from '@/composables/zoneComposable'
|
||||
import Controls from '@/components/utilities/Controls.vue'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import type { AssetDataT } from '@/types'
|
||||
|
||||
const emit = defineEmits(['tileMap:create'])
|
||||
|
||||
const scene = useScene()
|
||||
const gameStore = useGameStore()
|
||||
const zoneEditorStore = useZoneEditorStore()
|
||||
const tileMap = createTileMap()
|
||||
const tileLayer = createTileLayer()
|
||||
|
||||
/**
|
||||
* A Tilemap is a container for Tilemap data.
|
||||
* This isn't a display object, rather, it holds data about the map and allows you to add tilesets and tilemap layers to it.
|
||||
* A map can have one or more tilemap layers, which are the display objects that actually render the tiles.
|
||||
*/
|
||||
function createTileMap() {
|
||||
const zoneData = new Phaser.Tilemaps.MapData({
|
||||
width: zoneEditorStore.zone?.width,
|
||||
height: zoneEditorStore.zone?.height,
|
||||
tileWidth: config.tile_size.x,
|
||||
tileHeight: config.tile_size.y,
|
||||
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
|
||||
format: Phaser.Tilemaps.Formats.ARRAY_2D
|
||||
})
|
||||
|
||||
const newTileMap = new Phaser.Tilemaps.Tilemap(scene, zoneData)
|
||||
emit('tileMap:create', newTileMap)
|
||||
|
||||
return newTileMap
|
||||
}
|
||||
|
||||
/**
|
||||
* A Tileset is a combination of a single image containing the tiles and a container for data about each tile.
|
||||
*/
|
||||
function createTileLayer() {
|
||||
const tilesArray = gameStore.getLoadedAssetsByGroup('tiles')
|
||||
|
||||
const tilesetImages = Array.from(tilesArray).map((tile: AssetDataT, index: number) => {
|
||||
return tileMap.addTilesetImage(tile.key, tile.key, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y })
|
||||
}) as any
|
||||
|
||||
// Add blank tile
|
||||
tilesetImages.push(tileMap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 1, 2, 0, { x: 0, y: -config.tile_size.y }))
|
||||
const layer = tileMap.createBlankLayer('tiles', tilesetImages, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
|
||||
|
||||
layer.setDepth(0)
|
||||
layer.setCullPadding(2, 2)
|
||||
|
||||
return layer
|
||||
}
|
||||
|
||||
function pencil(pointer: Phaser.Input.Pointer) {
|
||||
// Check if zone is set
|
||||
if (!zoneEditorStore.zone) return
|
||||
|
||||
// Check if tool is pencil
|
||||
if (zoneEditorStore.tool !== 'pencil') return
|
||||
|
||||
// Check if draw mode is tile
|
||||
if (zoneEditorStore.drawMode !== 'tile') return
|
||||
|
||||
// Check if there is a selected tile
|
||||
if (!zoneEditorStore.selectedTile) return
|
||||
|
||||
// Check if left mouse button is pressed
|
||||
if (!pointer.isDown) return
|
||||
|
||||
// Check if shift is not pressed, this means we are moving the camera
|
||||
if (pointer.event.shiftKey) return
|
||||
|
||||
// Check if there is a tile
|
||||
const tile = getTile(tileLayer, pointer.worldX, pointer.worldY)
|
||||
if (!tile) return
|
||||
|
||||
// Place tile
|
||||
placeTile(tileMap, tileLayer, tile.x, tile.y, zoneEditorStore.selectedTile)
|
||||
|
||||
// Adjust zoneEditorStore.zone.tiles
|
||||
zoneEditorStore.zone.tiles[tile.y][tile.x] = zoneEditorStore.selectedTile
|
||||
}
|
||||
|
||||
function eraser(pointer: Phaser.Input.Pointer) {
|
||||
// Check if zone is set
|
||||
if (!zoneEditorStore.zone) return
|
||||
|
||||
// Check if tool is pencil
|
||||
if (zoneEditorStore.tool !== 'eraser') return
|
||||
|
||||
// Check if draw mode is tile
|
||||
if (zoneEditorStore.eraserMode !== 'tile') return
|
||||
|
||||
// Check if left mouse button is pressed
|
||||
if (!pointer.isDown) return
|
||||
|
||||
// Check if shift is not pressed, this means we are moving the camera
|
||||
if (pointer.event.shiftKey) return
|
||||
|
||||
// Check if alt is pressed
|
||||
if (pointer.event.altKey) return
|
||||
|
||||
// Check if there is a tile
|
||||
const tile = getTile(tileLayer, pointer.worldX, pointer.worldY)
|
||||
if (!tile) return
|
||||
|
||||
// Place tile
|
||||
placeTile(tileMap, tileLayer, tile.x, tile.y, 'blank_tile')
|
||||
|
||||
// Adjust zoneEditorStore.zone.tiles
|
||||
zoneEditorStore.zone.tiles[tile.y][tile.x] = 'blank_tile'
|
||||
}
|
||||
|
||||
function paint(pointer: Phaser.Input.Pointer) {
|
||||
// Check if zone is set
|
||||
if (!zoneEditorStore.zone) return
|
||||
|
||||
// Check if tool is pencil
|
||||
if (zoneEditorStore.tool !== 'paint') return
|
||||
|
||||
// Check if there is a selected tile
|
||||
if (!zoneEditorStore.selectedTile) return
|
||||
|
||||
// Check if left mouse button is pressed
|
||||
if (!pointer.isDown) return
|
||||
|
||||
// Check if shift is not pressed, this means we are moving the camera
|
||||
if (pointer.event.shiftKey) return
|
||||
|
||||
// Check if alt is pressed
|
||||
if (pointer.event.altKey) return
|
||||
|
||||
// Set new tileArray with selected tile
|
||||
setLayerTiles(tileMap, tileLayer, createTileArray(tileMap.width, tileMap.height, zoneEditorStore.selectedTile))
|
||||
|
||||
// Adjust zoneEditorStore.zone.tiles
|
||||
zoneEditorStore.zone.tiles = createTileArray(tileMap.width, tileMap.height, zoneEditorStore.selectedTile)
|
||||
}
|
||||
|
||||
// When alt is pressed, and the pointer is down, select the tile that the pointer is over
|
||||
function tilePicker(pointer: Phaser.Input.Pointer) {
|
||||
// Check if zone is set
|
||||
if (!zoneEditorStore.zone) return
|
||||
|
||||
// Check if tool is pencil
|
||||
if (zoneEditorStore.tool !== 'pencil') return
|
||||
|
||||
// Check if draw mode is tile
|
||||
if (zoneEditorStore.drawMode !== 'tile') return
|
||||
|
||||
// Check if left mouse button is pressed
|
||||
if (!pointer.isDown) return
|
||||
|
||||
// Check if shift is not pressed, this means we are moving the camera
|
||||
if (pointer.event.shiftKey) return
|
||||
|
||||
// Check if alt is pressed
|
||||
if (!pointer.event.altKey) return
|
||||
|
||||
// Check if there is a tile
|
||||
const tile = getTile(tileLayer, pointer.worldX, pointer.worldY)
|
||||
if (!tile) return
|
||||
|
||||
// Select the tile
|
||||
zoneEditorStore.setSelectedTile(zoneEditorStore.zone.tiles[tile.y][tile.x])
|
||||
}
|
||||
|
||||
watch(
|
||||
() => zoneEditorStore.shouldClearTiles,
|
||||
(shouldClear) => {
|
||||
if (shouldClear && zoneEditorStore.zone) {
|
||||
const blankTiles = createTileArray(tileMap.width, tileMap.height, 'blank_tile')
|
||||
setLayerTiles(tileMap, tileLayer, blankTiles)
|
||||
zoneEditorStore.zone.tiles = blankTiles
|
||||
zoneEditorStore.resetClearTilesFlag()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
if (!zoneEditorStore.zone?.tiles) {
|
||||
return
|
||||
}
|
||||
|
||||
// First fill the entire map with blank tiles using current zone dimensions
|
||||
const blankTiles = createTileArray(zoneEditorStore.zone.width, zoneEditorStore.zone.height, 'blank_tile')
|
||||
|
||||
// Then overlay the zone tiles, but only within the current zone dimensions
|
||||
const zoneTiles = zoneEditorStore.zone.tiles
|
||||
for (let y = 0; y < zoneEditorStore.zone.height; y++) {
|
||||
for (let x = 0; x < zoneEditorStore.zone.width; x++) {
|
||||
// Only copy if the source tiles array has this position
|
||||
if (zoneTiles[y] && zoneTiles[y][x] !== undefined) {
|
||||
blankTiles[y][x] = zoneTiles[y][x]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setLayerTiles(tileMap, tileLayer, blankTiles)
|
||||
|
||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
|
||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
|
||||
scene.input.on(Phaser.Input.Events.POINTER_DOWN, paint)
|
||||
scene.input.on(Phaser.Input.Events.POINTER_DOWN, tilePicker)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
|
||||
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
|
||||
scene.input.off(Phaser.Input.Events.POINTER_DOWN, paint)
|
||||
scene.input.off(Phaser.Input.Events.POINTER_DOWN, tilePicker)
|
||||
|
||||
tileMap.destroyLayer('tiles')
|
||||
tileMap.removeAllLayers()
|
||||
tileMap.destroy()
|
||||
})
|
||||
</script>
|
@ -5,7 +5,7 @@
|
||||
<div @mousedown="startDrag" class="cursor-move px-5 py-2.5 flex justify-between items-center relative">
|
||||
<span class="text-xs text-white font-thin">Character Profile [Alt+C]</span>
|
||||
<button @click="gameStore.uiSettings.isCharacterProfileOpen = false" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
|
||||
<img alt="close" draggable="false" src="/assets/icons/close-button-white.svg" class="w-full h-full" />
|
||||
<img alt="close" draggable="false" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="py-4 px-6 flex flex-col gap-7 relative z-10">
|
||||
@ -15,7 +15,7 @@
|
||||
<p class="text-sm m-0 font-bold text-white tracking-wide">{{ gameStore.character?.name }}</p>
|
||||
<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]">
|
||||
<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>
|
||||
@ -168,8 +168,8 @@ function stopDrag() {
|
||||
}
|
||||
|
||||
function adjustPosition() {
|
||||
x.value = Math.max(0, Math.min(x.value, window.innerWidth - width.value))
|
||||
y.value = Math.max(0, Math.min(y.value, window.innerHeight - height.value))
|
||||
x.value = Math.min(x.value, window.innerWidth - width.value)
|
||||
y.value = Math.min(y.value, window.innerHeight - height.value)
|
||||
}
|
||||
|
||||
function initializePosition() {
|
||||
@ -236,6 +236,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
removeEventListener('keydown', keyPress)
|
||||
removeEventListener('mousemove', drag)
|
||||
removeEventListener('mouseup', stopDrag)
|
||||
})
|
||||
|
@ -7,7 +7,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-96 mx-auto relative">
|
||||
<img src="/assets/icons/chat-icon.svg" class="absolute top-1/2 -translate-y-1/2 left-2.5 h-4 w-4 opacity-50" />
|
||||
<img src="/assets/icons/ingameUI/chat-icon.svg" class="absolute top-1/2 -translate-y-1/2 left-2.5 h-4 w-4 opacity-50" />
|
||||
<input
|
||||
class="w-[332px] h-8 rounded-sm text-xs font-default pl-8 pr-4 py-0 bg-gray-600 border-2 border-solid border-gray-500 text-gray-300 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover focus:outline-none focus:ring-0 focus:border-cyan-800"
|
||||
placeholder="Type something..."
|
||||
|
21
src/components/gui/Clock.vue
Normal file
@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div class="absolute top-0 right-4 hidden lg:block">
|
||||
<p class="text-white text-lg">{{ gameStore.world.date.toLocaleString() }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { onUnmounted } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
|
||||
// Listen for new date from socket
|
||||
gameStore.connection?.on('date', (data: Date) => {
|
||||
gameStore.world.date = new Date(data)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
gameStore.connection?.off('date')
|
||||
})
|
||||
</script>
|
@ -2,44 +2,44 @@
|
||||
<div class="absolute top-4 left-[300px] w-[422px]">
|
||||
<div class="flex gap-2.5">
|
||||
<div class="relative">
|
||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-frame-empty.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||
<span class="z-10 text-pixel absolute top-1 left-2">F1</span>
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f1-icon.png')] bg-no-repeat"></div>
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/ingameUI/f1-icon.png')] bg-no-repeat"></div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-frame-empty.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||
<span class="z-10 text-pixel absolute top-1 left-2">F2</span>
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f2-icon.png')] bg-no-repeat"></div>
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/ingameUI/f2-icon.png')] bg-no-repeat"></div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-frame-empty.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||
<span class="z-10 text-pixel absolute top-1 left-2">F3</span>
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f3-icon.png')] bg-no-repeat"></div>
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/ingameUI/f3-icon.png')] bg-no-repeat"></div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-frame-empty.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||
<span class="z-10 text-pixel absolute top-1 left-2">F4</span>
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f4-icon.png')] bg-no-repeat"></div>
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/ingameUI/f4-icon.png')] bg-no-repeat"></div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-frame-empty.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||
<span class="z-10 text-pixel absolute top-1 left-2">F5</span>
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f5-icon.png')] bg-no-repeat"></div>
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/ingameUI/f5-icon.png')] bg-no-repeat"></div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-frame-empty.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||
<span class="z-10 text-pixel absolute top-1 left-2">F6</span>
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f6-icon.png')] bg-no-repeat"></div>
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/ingameUI/f6-icon.png')] bg-no-repeat"></div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-frame-empty.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||
<span class="z-10 text-pixel absolute top-1 left-2">F7</span>
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f7-icon.png')] bg-no-repeat"></div>
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/ingameUI/f7-icon.png')] bg-no-repeat"></div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-frame-empty.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||
<span class="z-10 text-pixel absolute top-1 left-2">F8</span>
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f8-icon.png')] bg-no-repeat"></div>
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/ingameUI/f8-icon.png')] bg-no-repeat"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="absolute left-[66px] top-4 bg-[url('/assets/ui-elements/ui-rect-border-4-corners.svg')] bg-no-repeat px-4 py-2 w-[181px] h-[26px] flex flex-col justify-between">
|
||||
<div class="absolute left-[66px] top-4 bg-[url('/assets/ui-elements/hud-ui-box.svg')] bg-no-repeat px-4 py-2 w-[181px] h-[26px] flex flex-col justify-between">
|
||||
<div class="w-full flex items-center gap-2">
|
||||
<label class="text-xs leading-3 text-pixel" for="hp">HP</label>
|
||||
<progress class="h-2 rounded-sm w-full max-w-44 appearance-none accent-green" id="hp" :value="gameStore.character?.hitpoints" max="100">{{ gameStore.character?.hitpoints }}%</progress>
|
||||
|
@ -5,8 +5,8 @@
|
||||
<p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">Open menu</p>
|
||||
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
||||
</div>
|
||||
<a class="group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners.svg')] bg-no-repeat block w-[42px] h-[42px] relative">
|
||||
<img class="group-hover:drop-shadow-default w-6 h-5 mx-[9px] my-[11px] object-contain" draggable="false" src="/assets/icons/menu-icon.svg" />
|
||||
<a class="group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured.svg')] bg-no-repeat block w-[42px] h-[42px] relative">
|
||||
<img class="group-hover:drop-shadow-default w-6 h-5 mx-[9px] my-[11px] object-contain" draggable="false" src="/assets/icons/ingameUI/menu-icon.svg" />
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item group relative" @click="gameStore.toggleCharacterProfile">
|
||||
@ -14,7 +14,7 @@
|
||||
<p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">User Profile</p>
|
||||
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
||||
</div>
|
||||
<a class="group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners.svg')] bg-no-repeat block w-[42px] h-[42px] relative">
|
||||
<a class="group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured.svg')] bg-no-repeat block w-[42px] h-[42px] relative">
|
||||
<img class="group-hover:drop-shadow-default w-8 h-8 m-[5px] object-contain" draggable="false" src="/assets/avatar/default/head.png" />
|
||||
<p class="absolute bottom-0 -right-1.5 m-0 max-w-4 font-ui z-10 text-white text-[12px] leading-[6px] drop-shadow-pixel"><span class="font-ui text-white text-[8px] ml-0.5">LVL</span> {{ characterLevel }}</p>
|
||||
</a>
|
||||
@ -25,7 +25,7 @@
|
||||
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
||||
</div>
|
||||
<a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border border-b-4 border-solid rounded border-gray-500 block w-[34px] h-[31px]">
|
||||
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/chat-icon.svg" />
|
||||
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/chat-icon.svg" />
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item group relative">
|
||||
@ -34,7 +34,7 @@
|
||||
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
||||
</div>
|
||||
<a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border border-b-4 border-solid rounded border-gray-500 block w-[34px] h-[31px]">
|
||||
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/map-icon.svg" />
|
||||
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/map-icon.svg" />
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item group relative">
|
||||
@ -43,7 +43,7 @@
|
||||
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
||||
</div>
|
||||
<a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border border-b-4 border-solid rounded border-gray-500 block w-[34px] h-[31px]">
|
||||
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/socials-icon.svg" />
|
||||
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/socials-icon.svg" />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -6,11 +6,11 @@
|
||||
<div class="absolute -bottom-3 left-1/2 -translate-x-1/2 flex gap-1">
|
||||
<button class="w-6 h-6 relative p-0">
|
||||
<img class="absolute w-3 h-3 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/plus-icon.svg" />
|
||||
<img class="w-full h-full" src="/assets/ui-elements/ui-border-4-corners.svg" />
|
||||
<img class="w-full h-full" src="/assets/ui-elements/button-ui-box-textured.svg" />
|
||||
</button>
|
||||
<button class="w-6 h-6 relative p-0">
|
||||
<img class="absolute w-3 h-3 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/minus-icon.svg" />
|
||||
<img class="w-full h-full" src="/assets/ui-elements/ui-border-4-corners.svg" />
|
||||
<img class="w-full h-full" src="/assets/ui-elements/button-ui-box-textured.svg" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -11,7 +11,7 @@
|
||||
</div>
|
||||
<div class="flex gap-2.5">
|
||||
<button class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
|
||||
<img alt="close" draggable="false" src="/assets/icons/close-button-white.svg" class="w-full h-full" />
|
||||
<img alt="close" draggable="false" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex sm:hidden gap-1.5 flex-wrap">
|
||||
|
69
src/components/login/LoginForm.vue
Normal file
@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<form @submit.prevent="loginFunc" class="relative px-6 py-11">
|
||||
<div class="flex flex-col gap-5 p-2 mb-8 relative">
|
||||
<div class="w-full grid gap-3 relative">
|
||||
<input class="input-field xs:min-w-[350px] min-w-64" id="username-login" v-model="username" type="text" name="username" placeholder="Username" required autofocus />
|
||||
<div class="relative">
|
||||
<input class="input-field xs:min-w-[350px] min-w-64" id="password-login" v-model="password" :type="showPassword ? 'text' : 'password'" name="password" placeholder="Password" required />
|
||||
<button type="button" @click.prevent="showPassword = !showPassword" :class="{ 'eye-open': showPassword }" class="bg-[url('/assets/icons/eye.svg')] p-0 absolute right-3 w-4 h-3 top-1/2 -translate-y-1/2 bg-no-repeat"></button>
|
||||
</div>
|
||||
<span v-if="loginError" class="text-red-200 text-xs absolute top-full mt-1">{{ loginError }}</span>
|
||||
</div>
|
||||
<button @click.stop="() => emit('openResetPasswordModal')" type="button" class="inline-flex self-end p-0 text-cyan-300 text-base">Forgot password?</button>
|
||||
<button class="btn-cyan px-0 xs:w-full" type="submit">Play now</button>
|
||||
|
||||
<!-- Divider shape -->
|
||||
<div class="absolute w-40 h-0.5 -bottom-8 left-1/2 -translate-x-1/2 flex justify-between">
|
||||
<div class="w-0.5 h-full bg-gray-300"></div>
|
||||
<div class="w-36 h-full bg-gray-300"></div>
|
||||
<div class="w-0.5 h-full bg-gray-300"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-8">
|
||||
<p class="m-0 text-center">Don't have an account? <button class="text-cyan-300 text-base p-0" @click.prevent="() => emit('switchToRegister')">Sign up</button></p>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { login } from '@/services/authentication'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||
|
||||
const emit = defineEmits(['openResetPasswordModal', 'switchToRegister'])
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const loginError = ref('')
|
||||
const showPassword = ref(false)
|
||||
|
||||
// automatic login because of development
|
||||
onMounted(async () => {
|
||||
const token = useCookies().get('token')
|
||||
if (token) {
|
||||
gameStore.setToken(token)
|
||||
gameStore.initConnection()
|
||||
}
|
||||
})
|
||||
|
||||
async function loginFunc() {
|
||||
// check if username and password are valid
|
||||
if (username.value === '' || password.value === '') {
|
||||
loginError.value = 'Please enter a valid username and password'
|
||||
return
|
||||
}
|
||||
|
||||
// send login event to server
|
||||
const response = await login(username.value, password.value)
|
||||
|
||||
if (response.success === undefined) {
|
||||
loginError.value = response.error
|
||||
return
|
||||
}
|
||||
gameStore.setToken(response.token)
|
||||
gameStore.initConnection()
|
||||
return true // Indicate success
|
||||
}
|
||||
</script>
|
77
src/components/login/NewPasswordForm.vue
Normal file
@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<form @submit.prevent="newPasswordFunc" class="relative px-6 py-11">
|
||||
<div class="flex flex-col gap-5 p-2 mb-8 relative">
|
||||
<div class="w-full grid gap-3 relative">
|
||||
<input class="input-field xs:min-w-[350px] min-w-64" id="password-register" v-model="password" :type="showPassword ? 'text' : 'password'" name="password" placeholder="Password" required />
|
||||
<button type="button" @click.prevent="showPassword = !showPassword" :class="{ 'eye-open': showPassword }" class="bg-[url('/assets/icons/eye.svg')] p-0 absolute right-3 w-4 h-3 top-1/2 -translate-y-1/2 bg-no-repeat"></button>
|
||||
<span v-if="newPasswordError" class="text-red-200 text-xs absolute top-full mt-1">{{ newPasswordError }}</span>
|
||||
</div>
|
||||
<button class="btn-cyan xs:w-full" type="submit">Change password</button>
|
||||
|
||||
<!-- Divider shape -->
|
||||
<div class="absolute w-40 h-0.5 -bottom-8 left-1/2 -translate-x-1/2 flex justify-between">
|
||||
<div class="w-0.5 h-full bg-gray-300"></div>
|
||||
<div class="w-36 h-full bg-gray-300"></div>
|
||||
<div class="w-0.5 h-full bg-gray-300"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-8">
|
||||
<p class="m-0 text-center"><button class="text-cyan-300 text-base p-0" @click.prevent="cancelNewPassword">Back to login</button></p>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { newPassword } from '@/services/authentication'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||
|
||||
const emit = defineEmits(['switchToLogin'])
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const password = ref('')
|
||||
const newPasswordError = ref('')
|
||||
const showPassword = ref(false)
|
||||
|
||||
// automatic login because of development
|
||||
onMounted(async () => {
|
||||
const token = useCookies().get('token')
|
||||
if (token) {
|
||||
gameStore.setToken(token)
|
||||
gameStore.initConnection()
|
||||
}
|
||||
})
|
||||
|
||||
async function newPasswordFunc() {
|
||||
// check if username and password are valid
|
||||
if (password.value === '') {
|
||||
newPasswordError.value = 'Please enter a password'
|
||||
return
|
||||
}
|
||||
|
||||
const urlToken = window.location.hash.split('#')[1]
|
||||
|
||||
// send new password event to server along with the token
|
||||
const response = await newPassword(urlToken, password.value)
|
||||
|
||||
if (response.success === undefined) {
|
||||
newPasswordError.value = response.error
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* @TODO: #238, this wont work if we redirect to the login page
|
||||
* Find a way to just "close" this screen instead of redirecting
|
||||
*/
|
||||
gameStore.addNotification({
|
||||
title: 'Success',
|
||||
message: 'Password changed successfully'
|
||||
})
|
||||
window.location.href = '/'
|
||||
}
|
||||
|
||||
function cancelNewPassword() {
|
||||
window.location.href = '/'
|
||||
}
|
||||
</script>
|
97
src/components/login/RegisterForm.vue
Normal file
@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<form @submit.prevent="registerFunc" class="relative px-6 py-11">
|
||||
<div class="flex flex-col gap-5 p-2 mb-8 relative">
|
||||
<div class="w-full grid gap-3 relative">
|
||||
<input class="input-field xs:min-w-[350px] min-w-64" id="username-register" v-model="username" type="text" name="username" placeholder="Username" required autofocus />
|
||||
<input class="input-field xs:min-w-[350px] min-w-64" id="email-register" v-model="email" type="email" name="email" placeholder="Email" required />
|
||||
<div class="relative">
|
||||
<input class="input-field xs:min-w-[350px] min-w-64" id="password-register" v-model="password" :type="showPassword ? 'text' : 'password'" name="password" placeholder="Password" required />
|
||||
<button type="button" @click.prevent="showPassword = !showPassword" :class="{ 'eye-open': showPassword }" class="bg-[url('/assets/icons/eye.svg')] p-0 absolute right-3 w-4 h-3 top-1/2 -translate-y-1/2 bg-no-repeat"></button>
|
||||
</div>
|
||||
<span v-if="loginError" class="text-red-200 text-xs -mt-2">{{ loginError }}</span>
|
||||
</div>
|
||||
<button class="btn-cyan xs:w-full" type="submit">Register now</button>
|
||||
|
||||
<!-- Divider shape -->
|
||||
<div class="absolute w-40 h-0.5 -bottom-8 left-1/2 -translate-x-1/2 flex justify-between">
|
||||
<div class="w-0.5 h-full bg-gray-300"></div>
|
||||
<div class="w-36 h-full bg-gray-300"></div>
|
||||
<div class="w-0.5 h-full bg-gray-300"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-8">
|
||||
<p class="m-0 text-center">Already have an account? <button class="text-cyan-300 text-base p-0" @click.prevent="() => emit('switchToLogin')">Log in</button></p>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { login, register } from '@/services/authentication'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||
|
||||
const emit = defineEmits(['switchToLogin'])
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const email = ref('')
|
||||
const loginError = ref('')
|
||||
const showPassword = ref(false)
|
||||
|
||||
// automatic login because of development
|
||||
onMounted(async () => {
|
||||
const token = useCookies().get('token')
|
||||
if (token) {
|
||||
gameStore.setToken(token)
|
||||
gameStore.initConnection()
|
||||
}
|
||||
})
|
||||
|
||||
async function loginFunc() {
|
||||
// check if username and password are valid
|
||||
if (username.value === '' || password.value === '') {
|
||||
loginError.value = 'Please enter a valid username and password'
|
||||
return
|
||||
}
|
||||
|
||||
// send login event to server
|
||||
const response = await login(username.value, password.value)
|
||||
|
||||
if (response.success === undefined) {
|
||||
loginError.value = response.error
|
||||
return
|
||||
}
|
||||
gameStore.setToken(response.token)
|
||||
gameStore.initConnection()
|
||||
return true // Indicate success
|
||||
}
|
||||
|
||||
async function registerFunc() {
|
||||
// check if username and password are valid
|
||||
if (username.value === '' || email.value === '' || password.value === '') {
|
||||
loginError.value = 'Please enter a valid username, email, and password'
|
||||
return
|
||||
}
|
||||
|
||||
if (email.value === '') {
|
||||
loginError.value = 'Please enter an email'
|
||||
return
|
||||
}
|
||||
|
||||
// send register event to server
|
||||
const response = await register(username.value, email.value, password.value)
|
||||
|
||||
if (response.success === undefined) {
|
||||
loginError.value = response.error
|
||||
return
|
||||
}
|
||||
|
||||
const loginSuccess = await loginFunc()
|
||||
if (!loginSuccess) {
|
||||
loginError.value = 'Login after registration failed. Please try logging in manually.'
|
||||
return
|
||||
}
|
||||
}
|
||||
</script>
|
71
src/components/login/ResetPasswordModal.vue
Normal file
@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<Modal :isModalOpen="true" :modal-width="400" :modal-height="300" :is-resizable="false" @modal:close="() => emit('close')">
|
||||
<template #modalHeader>
|
||||
<h3 class="m-0 font-medium shrink-0 text-white">Reset Password</h3>
|
||||
</template>
|
||||
|
||||
<template #modalBody>
|
||||
<div class="h-[calc(100%_-_32px)] p-4">
|
||||
<form class="h-full flex flex-col justify-between" @submit.prevent="resetPasswordFunc">
|
||||
<div class="flex flex-col relative">
|
||||
<p>Fill in your email to receive a password reset request.</p>
|
||||
<input type="email" name="email" class="input-field" v-model="email" placeholder="E-mail" />
|
||||
<span v-if="resetPasswordError" class="text-red-200 text-xs absolute top-full mt-1">{{ resetPasswordError }}</span>
|
||||
</div>
|
||||
<div class="grid grid-flow-col justify-stretch gap-4">
|
||||
<button class="btn-empty py-1.5 px-4 min-w-24 inline-block" @click.stop="() => emit('close')">Cancel</button>
|
||||
<button class="btn-cyan py-1.5 px-4 min-w-24 inline-flex items-center justify-center" type="submit">
|
||||
<svg v-if="isLoading" xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 animate-spin mr-2" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Send mail
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { resetPassword } from '@/services/authentication'
|
||||
import Modal from '@/components/utilities/Modal.vue'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const isLoading = ref(false)
|
||||
const email = ref('')
|
||||
const resetPasswordError = ref('')
|
||||
|
||||
async function resetPasswordFunc() {
|
||||
// check if email is valid
|
||||
if (email.value === '') {
|
||||
resetPasswordError.value = 'Please enter an email'
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
// send reset password event to server
|
||||
const response = await resetPassword(email.value)
|
||||
|
||||
if (response.success === undefined) {
|
||||
resetPasswordError.value = response.error
|
||||
isLoading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
gameStore.addNotification({
|
||||
title: 'Success',
|
||||
message: 'Password reset email sent'
|
||||
})
|
||||
|
||||
isLoading.value = false
|
||||
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
188
src/components/screens/Characters.vue
Normal file
@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<div class="relative max-lg:h-dvh flex flex-row-reverse">
|
||||
<div class="lg:bg-gradient-to-l bg-gradient-to-b from-gray-900 to-transparent w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 z-10"></div>
|
||||
<div class="bg-[url('/assets/login/login-bg.png')] opacity-20 w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 bg-no-repeat bg-cover bg-center grayscale"></div>
|
||||
<div class="bg-gray-900 z-20 w-full lg:w-1/2 h-[65dvh] lg:h-dvh relative"></div>
|
||||
<div class="absolute top-8 right-0 py-[18px] pr-[15px] pl-32 bg-gradient-to-r from-transparent to-cyan-900 z-20">
|
||||
<h2 class="text-white">CHARACTER SELECTION</h2>
|
||||
</div>
|
||||
<div class="ui-wrapper h-dvh w-[calc(100%_-_80px)] sm:w-[calc(100%_-_160px)] absolute flex flex-col justify-center items-center gap-14 px-10 sm:px-20 z-30">
|
||||
<div class="filler"></div>
|
||||
<div class="w-2/3 max-w-[860px]" v-if="!isLoading">
|
||||
<div class="mb-5 flex flex-col gap-1">
|
||||
<h1 class="text-white font-bold">SELECT CHARACTER TO PLAY</h1>
|
||||
<p class="m-0">Maximum of 4 characters can be created per player</p>
|
||||
</div>
|
||||
<div class="flex w-full h-[400px] border border-solid border-gray-500 rounded-md rounded-tl-none bg-gray">
|
||||
<div class="w-1/3 h-full bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center border-0 border-r border-solid border-gray-500 rounded-bl-md relative">
|
||||
<div class="absolute right-full -top-px flex gap-1 flex-col">
|
||||
<div v-for="character in characters" :key="character.id" class="character relative rounded-l border border-solid border-gray-500 w-9 h-[50px] bg-[url('/assets/ui-texture.png')]" :class="{ active: selected_character == character.id }">
|
||||
<img src="/assets/avatar/default/head.png" class="w-9 h-9 object-contain absolute top-1/2 -translate-y-1/2" alt="Player head" />
|
||||
<input class="opacity-0 h-full w-full absolute m-0 z-10 hover:cursor-pointer" type="radio" :id="character.id" name="character" :value="character.id" v-model="selected_character" />
|
||||
</div>
|
||||
<div class="character relative rounded-l border border-solid border-gray-500 w-9 h-[50px] bg-[url('/assets/ui-texture.png')]" :class="{ active: characters.length == 0 }" v-if="characters.length < 4">
|
||||
<button class="p-0 h-full w-full flex flex-col justify-between" @click="isModalOpen = true">
|
||||
<img class="w-6 h-6 object-contain absolute top-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2" draggable="false" src="/assets/icons/plus-icon.svg" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-6 px-8 h-[calc(100%_-_48px)] flex flex-col items-center gap-6" v-if="selected_character">
|
||||
<input class="input-field w-[158px]" type="text" name="name" :placeholder="characters.find((c) => c.id == selected_character)?.name" />
|
||||
<div class="flex flex-col gap-4 items-center">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="bg-[url('/assets/ui-elements/character-select-ui-shape.svg')] w-[190px] h-52 bg-no-repeat bg-center flex items-center justify-between">
|
||||
<button class="ml-6 w-4 h-8 p-0">
|
||||
<img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 m-auto" alt="Arrow left" />
|
||||
</button>
|
||||
<img class="w-12 object-contain mb-3.5" src="/assets/avatar/default/0.png" alt="Player avatar" />
|
||||
<button class="mr-6 w-4 h-8 p-0">
|
||||
<img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 -scale-x-100" alt="Arrow right" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex justify-between w-[190px]">
|
||||
<!-- TODO: replace with color swatches -->
|
||||
<button v-for="n in 9" class="w-4 h-4 rounded-sm bg-white"></button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- TODO: update gender on (selected) character -->
|
||||
<div class="flex justify-between w-[190px]">
|
||||
<button class="btn-empty flex gap-2" :class="{ selected: characters.find((c) => c.id == selected_character)?.characterType?.gender === 'MALE' }">
|
||||
<img src="/assets/icons/male-icon.svg" class="w-4 h-4 m-auto" alt="Male symbol" />
|
||||
<span class="text-white">Male</span>
|
||||
</button>
|
||||
<button class="btn-empty flex gap-2" :class="{ selected: characters.find((c) => c.id == selected_character)?.characterType?.gender === 'FEMALE' }">
|
||||
<img src="/assets/icons/male-icon.svg" class="w-4 h-4 m-auto" alt="Male symbol" />
|
||||
<span class="text-white">Female</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-2/3 h-full bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center rounded-r-md">
|
||||
<div class="py-6 px-8 h-[calc(100%_-_48px)] flex flex-col items-center gap-10" v-if="selected_character">
|
||||
<div class="flex flex-col gap-3 w-full">
|
||||
<span class="text-sm">Hairstyle</span>
|
||||
<div class="flex gap-2 flex-wrap max-h-20 overflow-y-auto scrollbar">
|
||||
<button class="bg-gray border border-solid border-gray-500 min-w-9 max-w-9 min-h-9 max-h-9 p-2 rounded-sm hover:bg-gray-500 hover:border-gray-400">
|
||||
<img src="/assets/icons/x-button-gray.svg" class="w-4 h-4 m-auto" alt="Male symbol" />
|
||||
</button>
|
||||
<!-- TODO: replace with hairstyles -->
|
||||
<button v-for="n in 30" class="bg-gray border border-solid border-gray-500 min-w-9 max-w-9 min-h-9 max-h-9 p-2 rounded-sm hover:bg-gray-500 hover:border-gray-400"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 w-full">
|
||||
<span class="text-sm">Hair color</span>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<!-- TODO: replace with hairstyles -->
|
||||
<button v-for="n in 10" class="bg-white w-6 h-6 rounded-sm"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<img class="w-20 invert-80" src="/assets/icons/loading-icon1.svg" />
|
||||
</div>
|
||||
|
||||
<div class="button-wrapper flex self-center justify-end gap-4 max-w-[860px] w-full" v-if="!isLoading">
|
||||
<button class="btn-empty min-w-48" @click.stop="gameStore.disconnectSocket()">Back</button>
|
||||
<button class="btn-cyan min-w-48 disabled:bg-cyan-800 disabled:cursor-not-allowed" :disabled="!selected_character" @click="select_character()">Play now</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CREATE CHARACTER MODAL -->
|
||||
<Modal :isModalOpen="isModalOpen" @modal:close="isModalOpen = false" :modal-width="430" :modal-height="275">
|
||||
<template #modalHeader>
|
||||
<h3 class="m-0 font-medium text-white">Create your character</h3>
|
||||
</template>
|
||||
|
||||
<template #modalBody>
|
||||
<div class="p-4 h-[calc(100%_-_32px)]">
|
||||
<form method="post" @submit.prevent="create" class="h-full flex flex-col justify-between">
|
||||
<div class="form-field-full">
|
||||
<label for="name" class="text-white">Nickname</label>
|
||||
<input class="input-field" v-model="name" name="name" id="name" placeholder="Enter a nickname.." />
|
||||
</div>
|
||||
<div class="grid grid-flow-col justify-stretch gap-4">
|
||||
<button type="button" class="btn-empty py-1.5 px-4 inline-block" @click.prevent="isModalOpen = false">Cancel</button>
|
||||
<button class="btn-cyan py-1.5 px-4 inline-block" type="submit">Create</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
|
||||
<!-- DELETE CHARACTER MODAL -->
|
||||
<ConfirmationModal v-if="deletingCharacter != null" :confirm-function="delete_character.bind(this, deletingCharacter.id)" :cancel-function="(() => (deletingCharacter = null)).bind(this)" confirm-button-text="Delete">
|
||||
<template #modalHeader>
|
||||
<h3 class="m-0 font-medium text-white">Delete character?</h3>
|
||||
</template>
|
||||
<template #modalBody>
|
||||
<p class="mt-0 mb-5 text-white text-lg">
|
||||
Do you want to permanently delete <span class="font-extrabold text-white">{{ deletingCharacter.name }}</span
|
||||
>?
|
||||
</p>
|
||||
</template>
|
||||
</ConfirmationModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import Modal from '@/components/utilities/Modal.vue'
|
||||
import { type Character as CharacterT } from '@/types'
|
||||
import ConfirmationModal from '@/components/utilities/ConfirmationModal.vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const isLoading = ref(true)
|
||||
const characters = ref([] as CharacterT[])
|
||||
const deletingCharacter = ref(null as CharacterT | null)
|
||||
|
||||
// Fetch characters
|
||||
gameStore.connection?.on('character:list', (data: any) => {
|
||||
characters.value = data
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
// wait 0.75 sec
|
||||
setTimeout(() => {
|
||||
gameStore.connection?.emit('character:list')
|
||||
isLoading.value = false
|
||||
}, 750)
|
||||
})
|
||||
|
||||
// Select character logics
|
||||
const selected_character = ref(null)
|
||||
function select_character() {
|
||||
if (!selected_character.value) return
|
||||
deletingCharacter.value = null
|
||||
gameStore.connection?.emit('character:connect', { characterId: selected_character.value })
|
||||
gameStore.connection?.on('character:connect', (data: CharacterT) => gameStore.setCharacter(data))
|
||||
}
|
||||
|
||||
// Delete character logics
|
||||
function delete_character(characterId: number) {
|
||||
if (!characterId) return
|
||||
deletingCharacter.value = null
|
||||
gameStore.connection?.emit('character:delete', { characterId: characterId })
|
||||
}
|
||||
|
||||
// Create character logics
|
||||
const isModalOpen = ref(false)
|
||||
const name = ref('')
|
||||
function create() {
|
||||
gameStore.connection?.on('character:create:success', (data: CharacterT) => {
|
||||
gameStore.setCharacter(data)
|
||||
isModalOpen.value = false
|
||||
})
|
||||
gameStore.connection?.emit('character:create', { name: name.value })
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
gameStore.connection?.off('character:list')
|
||||
gameStore.connection?.off('character:connect')
|
||||
gameStore.connection?.off('character:create:success')
|
||||
})
|
||||
</script>
|
@ -5,7 +5,7 @@
|
||||
<Menu />
|
||||
<Hud />
|
||||
<Hotkeys />
|
||||
<Minimap />
|
||||
<Clock />
|
||||
<Zone />
|
||||
<Chat />
|
||||
<ExpBar />
|
||||
@ -30,12 +30,11 @@ import Hotkeys from '@/components/gui/Hotkeys.vue'
|
||||
import Chat from '@/components/gui/Chat.vue'
|
||||
import CharacterProfile from '@/components/gui/CharacterProfile.vue'
|
||||
import Effects from '@/components/Effects.vue'
|
||||
import Minimap from '@/components/gui/Minimap.vue'
|
||||
// import Minimap from '@/components/gui/Minimap.vue'
|
||||
import Clock from '@/components/gui/Clock.vue'
|
||||
import AwaitLoaderPlugin from 'phaser3-rex-plugins/plugins/awaitloader-plugin'
|
||||
import { useAssetManager } from '@/utilities/assetManager'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const assetManager = useAssetManager
|
||||
|
||||
const gameConfig = {
|
||||
name: config.name,
|
||||
@ -78,41 +77,7 @@ function preloadScene(scene: Phaser.Scene) {
|
||||
*/
|
||||
scene.load.image('blank_tile', '/assets/zone/blank_tile.png')
|
||||
scene.load.image('waypoint', '/assets/waypoint.png')
|
||||
|
||||
scene.load.rexAwait(async function (successCallback) {
|
||||
await assetManager.getAssetsByGroup('tiles').then((assets) => {
|
||||
assets.forEach((asset) => {
|
||||
if (scene.load.textureManager.exists(asset.key)) return
|
||||
scene.textures.addBase64(asset.key, asset.data)
|
||||
})
|
||||
})
|
||||
|
||||
// Load objects
|
||||
await assetManager.getAssetsByGroup('objects').then((assets) => {
|
||||
assets.forEach((asset) => {
|
||||
if (scene.load.textureManager.exists(asset.key)) return
|
||||
scene.textures.addBase64(asset.key, asset.data)
|
||||
})
|
||||
})
|
||||
|
||||
successCallback()
|
||||
})
|
||||
}
|
||||
|
||||
function createScene(scene: Phaser.Scene) {
|
||||
/**
|
||||
* Create sprite animations
|
||||
* This is done here because phaser forces us to
|
||||
*/
|
||||
assetManager.getAssetsByGroup('sprite_animations').then((assets) => {
|
||||
assets.forEach((asset) => {
|
||||
scene.anims.create({
|
||||
key: asset.key,
|
||||
frameRate: 7,
|
||||
frames: scene.anims.generateFrameNumbers(asset.key, { start: 0, end: asset.frameCount! - 1 }),
|
||||
repeat: -1
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
function createScene(scene: Phaser.Scene) {}
|
||||
</script>
|
25
src/components/screens/Loading.vue
Normal file
@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div class="flex flex-col justify-center items-center h-dvh relative">
|
||||
<button @click="continueBtnClick" class="w-32 h-12 rounded-full bg-gray-500 flex items-center justify-between px-4 hover:bg-gray-600 transition-colors">
|
||||
<span class="text-white text-lg flex-1 text-center">Play</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" async>
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
|
||||
function continueBtnClick() {
|
||||
// Play music
|
||||
const audio = new Audio('/assets/music/login.mp3')
|
||||
audio.play()
|
||||
|
||||
// Set isLoaded to true
|
||||
gameStore.game.isLoaded = true
|
||||
}
|
||||
</script>
|
51
src/components/screens/Login.vue
Normal file
@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<!-- @TODO this must be shown over the login screen -->
|
||||
<div class="relative max-lg:h-dvh flex flex-row-reverse">
|
||||
<ResetPassword :isModalOpen="isPasswordResetFormShown" @close="() => (isPasswordResetFormShown = false)" />
|
||||
<div class="lg:bg-gradient-to-l bg-gradient-to-b from-gray-900 to-transparent w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 z-10"></div>
|
||||
<div class="bg-[url('/assets/login/login-bg.png')] w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 bg-no-repeat bg-cover bg-center"></div>
|
||||
<div class="bg-gray-900 z-20 w-full lg:w-1/2 h-[65dvh] lg:h-dvh relative">
|
||||
<div class="h-dvh flex items-center lg:justify-center flex-col px-8 max-lg:pt-20">
|
||||
<img src="/assets/login/sq-logo-v1.svg" class="mb-10" alt="Sylvan Quest logo" />
|
||||
<div class="relative">
|
||||
<img src="/assets/ui-elements/login-ui-box-outer.svg" class="absolute w-full h-full" alt="UI box outer" />
|
||||
<img src="/assets/ui-elements/login-ui-box-inner.svg" class="absolute left-2 top-2 w-[calc(100%_-_16px)] h-[calc(100%_-_16px)] max-lg:hidden" alt="UI box inner" />
|
||||
|
||||
<!-- Login Form -->
|
||||
<LoginForm v-if="currentForm === 'login' && !doesUrlHaveToken" @openResetPasswordModal="() => (isPasswordResetFormShown = true)" @switchToRegister="currentForm = 'register'" />
|
||||
|
||||
<!-- Register Form -->
|
||||
<RegisterForm v-if="currentForm === 'register' && !doesUrlHaveToken" @switchToLogin="currentForm = 'login'" />
|
||||
|
||||
<!-- New Password Form -->
|
||||
<NewPasswordForm v-if="doesUrlHaveToken" @switchToLogin="currentForm = 'login'" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||
import LoginForm from '@/components/login/LoginForm.vue'
|
||||
import RegisterForm from '@/components/login/RegisterForm.vue'
|
||||
import NewPasswordForm from '@/components/login/NewPasswordForm.vue'
|
||||
import ResetPassword from '@/components/login/ResetPasswordModal.vue'
|
||||
|
||||
const isPasswordResetFormShown = ref(false)
|
||||
const doesUrlHaveToken = window.location.hash.includes('#')
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const currentForm = ref('login')
|
||||
|
||||
// automatic login because of development
|
||||
onMounted(async () => {
|
||||
const token = useCookies().get('token')
|
||||
if (token) {
|
||||
gameStore.setToken(token)
|
||||
gameStore.initConnection()
|
||||
}
|
||||
})
|
||||
</script>
|
@ -2,7 +2,7 @@
|
||||
<div class="flex justify-center items-center h-dvh relative">
|
||||
<Game :config="gameConfig" @create="createGame">
|
||||
<Scene name="main" @preload="preloadScene" @create="createScene">
|
||||
<ZoneEditor v-if="isLoaded" :key="JSON.stringify(`${zoneEditorStore.zone?.id}_${zoneEditorStore.zone?.createdAt}_${zoneEditorStore.zone?.updatedAt}`)" />
|
||||
<ZoneEditor :key="JSON.stringify(`${zoneEditorStore.zone?.id}_${zoneEditorStore.zone?.createdAt}_${zoneEditorStore.zone?.updatedAt}`)" />
|
||||
</Scene>
|
||||
</Game>
|
||||
</div>
|
||||
@ -11,22 +11,32 @@
|
||||
<script setup lang="ts">
|
||||
import config from '@/config'
|
||||
import 'phaser'
|
||||
import { ref, onBeforeUnmount } from 'vue'
|
||||
import { Game, Scene } from 'phavuer'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
import ZoneEditor from '@/components/gameMaster/zoneEditor/ZoneEditor.vue'
|
||||
import AwaitLoaderPlugin from 'phaser3-rex-plugins/plugins/awaitloader-plugin'
|
||||
import { loadTexture } from '@/composables/gameComposable'
|
||||
import type { AssetDataT } from '@/types'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const zoneEditorStore = useZoneEditorStore()
|
||||
const isLoaded = ref(false)
|
||||
|
||||
const gameConfig = {
|
||||
name: config.name,
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS
|
||||
resolution: 5
|
||||
resolution: 5,
|
||||
plugins: {
|
||||
global: [
|
||||
{
|
||||
key: 'rexAwaitLoader',
|
||||
plugin: AwaitLoaderPlugin,
|
||||
start: true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const createGame = (game: Phaser.Game) => {
|
||||
@ -48,43 +58,6 @@ const createGame = (game: Phaser.Game) => {
|
||||
}
|
||||
|
||||
const preloadScene = async (scene: Phaser.Scene) => {
|
||||
isLoaded.value = false
|
||||
|
||||
/**
|
||||
* Create loading bar
|
||||
*/
|
||||
const width = scene.cameras.main.width
|
||||
const height = scene.cameras.main.height
|
||||
|
||||
const progressBox = scene.add.graphics()
|
||||
const progressBar = scene.add.graphics()
|
||||
progressBox.fillStyle(0x222222, 0.8)
|
||||
progressBox.fillRect(width / 2 - 180, height / 2, 320, 50)
|
||||
|
||||
const loadingText = scene.make.text({
|
||||
x: width / 2,
|
||||
y: height / 2 - 50,
|
||||
text: 'Loading...',
|
||||
style: {
|
||||
font: '20px monospace',
|
||||
fill: '#ffffff'
|
||||
}
|
||||
})
|
||||
loadingText.setOrigin(0.5, 0.5)
|
||||
|
||||
scene.load.on(Phaser.Loader.Events.PROGRESS, function (value: any) {
|
||||
progressBar.clear()
|
||||
progressBar.fillStyle(0x368f8b, 1)
|
||||
progressBar.fillRect(width / 2 - 180 + 10, height / 2 + 10, 300 * value, 30)
|
||||
})
|
||||
|
||||
scene.load.on(Phaser.Loader.Events.COMPLETE, function () {
|
||||
progressBar.destroy()
|
||||
progressBox.destroy()
|
||||
loadingText.destroy()
|
||||
isLoaded.value = true
|
||||
})
|
||||
|
||||
/**
|
||||
* Load the base assets into the Phaser scene
|
||||
*/
|
||||
@ -92,26 +65,21 @@ const preloadScene = async (scene: Phaser.Scene) => {
|
||||
scene.load.image('TELEPORT', '/assets/zone/tp_tile.png')
|
||||
scene.load.image('blank_tile', '/assets/zone/blank_tile.png')
|
||||
scene.load.image('waypoint', '/assets/waypoint.png')
|
||||
}
|
||||
|
||||
const createScene = async (scene: Phaser.Scene) => {
|
||||
/**
|
||||
* Create sprite animations
|
||||
* This is done here because phaser forces us to
|
||||
* Because Phaser can't load tiles after the scene with map in it is created,
|
||||
* we need to load and cache all the tiles first.
|
||||
* Then load them into the scene.
|
||||
*/
|
||||
gameStore.assets.forEach((asset) => {
|
||||
if (asset.group !== 'sprite_animations') return
|
||||
scene.load.rexAwait(async function (successCallback: any) {
|
||||
const tiles: AssetDataT[] = await fetch(config.server_endpoint + '/assets/list_tiles').then((response) => response.json())
|
||||
for await (const tile of tiles) {
|
||||
await loadTexture(scene, tile)
|
||||
}
|
||||
|
||||
scene.anims.create({
|
||||
key: asset.key,
|
||||
frameRate: 7,
|
||||
frames: scene.anims.generateFrameNumbers(asset.key, { start: 0, end: asset.frameCount! - 1 }),
|
||||
repeat: -1
|
||||
})
|
||||
successCallback()
|
||||
})
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
isLoaded.value = false
|
||||
})
|
||||
const createScene = async (scene: Phaser.Scene) => {}
|
||||
</script>
|
@ -6,7 +6,7 @@
|
||||
</Container>
|
||||
<!-- Character name and health -->
|
||||
<Container :depth="999" :x="currentX" :y="currentY">
|
||||
<Text @create="createNicknameText" :text="character.name" />
|
||||
<Text @create="createNicknameText" :text="props.zoneCharacter.character.name" />
|
||||
<RoundRectangle :origin-x="0.5" :origin-y="18.5" :fillColor="0xffffff" :width="74" :height="6" :radius="5" />
|
||||
<RoundRectangle :origin-x="0.5" :origin-y="36.4" :fillColor="0x00b3b3" :width="70" :height="3" :radius="5" />
|
||||
</Container>
|
||||
@ -18,12 +18,13 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import config from '@/config'
|
||||
import { type ExtendedCharacter } from '@/types'
|
||||
import { type Sprite as SpriteT, type ZoneCharacter } from '@/types'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useZoneStore } from '@/stores/zoneStore'
|
||||
import { watch, computed, ref, onMounted, onUnmounted } from 'vue'
|
||||
import { Container, refObj, RoundRectangle, Sprite, Text, useGame, useScene } from 'phavuer'
|
||||
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||
import { loadSpriteTextures } from '@/composables/gameComposable'
|
||||
|
||||
enum Direction {
|
||||
POSITIVE,
|
||||
@ -33,7 +34,7 @@ enum Direction {
|
||||
|
||||
const props = defineProps<{
|
||||
layer: Phaser.Tilemaps.TilemapLayer
|
||||
character: ExtendedCharacter
|
||||
zoneCharacter: ZoneCharacter
|
||||
}>()
|
||||
|
||||
const charChatContainer = refObj<Phaser.GameObjects.Container>()
|
||||
@ -109,19 +110,19 @@ const calcDirection = (oldX: number, oldY: number, newX: number, newY: number):
|
||||
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 { rotation, characterType, isMoving } = props.character
|
||||
const { rotation, characterType } = props.zoneCharacter.character
|
||||
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'
|
||||
|
||||
return `${spriteId}-${action}_${direction}`
|
||||
})
|
||||
|
||||
const updateSprite = () => {
|
||||
if (props.character.isMoving) {
|
||||
if (props.zoneCharacter.isMoving) {
|
||||
charSprite.value!.anims.play(charTexture.value, true)
|
||||
return
|
||||
}
|
||||
@ -132,11 +133,11 @@ const updateSprite = () => {
|
||||
}
|
||||
|
||||
const createChatBubble = (container: Phaser.GameObjects.Container) => {
|
||||
container.setName(`${props.character.name}_chatBubble`)
|
||||
container.setName(`${props.zoneCharacter.character.name}_chatBubble`)
|
||||
}
|
||||
|
||||
const createChatText = (text: Phaser.GameObjects.Text) => {
|
||||
text.setName(`${props.character.name}_chatText`)
|
||||
text.setName(`${props.zoneCharacter.character.name}_chatText`)
|
||||
text.setFontSize(13)
|
||||
text.setFontFamily('Arial')
|
||||
text.setOrigin(0.5, 10.9)
|
||||
@ -167,7 +168,7 @@ const createNicknameText = (text: Phaser.GameObjects.Text) => {
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.character,
|
||||
() => props.zoneCharacter.character,
|
||||
(newChar, oldChar) => {
|
||||
if (!newChar) return
|
||||
|
||||
@ -178,15 +179,23 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
watch(() => props.character.isMoving, updateSprite)
|
||||
watch(() => props.character.rotation, updateSprite)
|
||||
watch(() => props.zoneCharacter, 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(() => {
|
||||
charChatContainer.value!.setName(`${props.character!.name}_chatContainer`)
|
||||
charChatContainer.value!.setName(`${props.zoneCharacter.character!.name}_chatContainer`)
|
||||
charChatContainer.value!.setVisible(false)
|
||||
charContainer.value!.setName(props.character!.name)
|
||||
charContainer.value!.setName(props.zoneCharacter.character!.name)
|
||||
|
||||
if (props.character.id === gameStore.character!.id) {
|
||||
if (props.zoneCharacter.character.id === gameStore.character!.id) {
|
||||
zoneStore.setCharacterLoaded(true)
|
||||
|
||||
// #146 : Set camera position to character, need to be improved still
|
||||
@ -194,11 +203,7 @@ onMounted(() => {
|
||||
scene.cameras.main.stopFollow()
|
||||
}
|
||||
|
||||
// Set sprite
|
||||
charSprite.value!.setTexture(charTexture.value)
|
||||
charSprite.value!.setFlipX(isFlippedX.value)
|
||||
|
||||
updatePosition(props.character.positionX, props.character.positionY, props.character.rotation)
|
||||
updatePosition(props.zoneCharacter.character.positionX, props.zoneCharacter.character.positionY, props.zoneCharacter.character.rotation)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
|
23
src/components/utilities/BackgroundImageLoader.vue
Normal file
@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div style="display: none">
|
||||
<img v-for="(url, index) in imageUrls" :key="index" :src="url" alt="" @load="handleImageLoad(index)" @error="handleImageError(index)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
// Internal array of images to preload
|
||||
const imageUrls = ref<string[]>(['/assets/ui-elements/button-ui-box-textured.svg', '/assets/ui-elements/button-ui-frame-empty.svg', '/assets/ui-elements/button-ui-box-textured-small.svg'])
|
||||
|
||||
const loadedImages = ref<Set<number>>(new Set())
|
||||
|
||||
const handleImageLoad = (index: number) => {
|
||||
loadedImages.value.add(index)
|
||||
console.log(`Image ${index} loaded:`, imageUrls.value[index])
|
||||
}
|
||||
|
||||
const handleImageError = (index: number) => {
|
||||
console.log(`Image ${index} failed to load:`, imageUrls.value[index])
|
||||
}
|
||||
</script>
|
@ -1,245 +1,216 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-if="isModalOpenRef" class="fixed border-solid border-2 border-gray-500 z-50 flex flex-col backdrop-blur-sm shadow-lg" :style="modalStyle">
|
||||
<!-- Header -->
|
||||
<div @mousedown="startDrag" class="cursor-move p-2.5 flex justify-between items-center border-solid border-0 border-b border-gray-500 relative">
|
||||
<div class="rounded-t-md absolute w-full h-full top-0 left-0 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-center bg-cover opacity-90"></div>
|
||||
<div class="rounded-t absolute w-full h-full top-0 left-0 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-center bg-cover opacity-90" />
|
||||
<div class="relative z-10">
|
||||
<slot name="modalHeader" />
|
||||
</div>
|
||||
<div class="flex gap-2.5">
|
||||
<button @click="toggleFullScreen" class="w-5 h-5 m-0 p-0 relative hover:scale-110 transition-transform duration-300 ease-in-out" v-if="canFullScreen">
|
||||
<img :alt="isFullScreen ? 'exit full-screen' : 'full-screen'" draggable="false" :src="isFullScreen ? '/assets/icons/minimize.svg' : '/assets/icons/increase-size-option.svg'" class="w-3.5 h-3.5 invert" />
|
||||
<button v-if="canFullScreen" @click="toggleFullScreen" class="w-5 h-5 m-0 p-0 relative hover:scale-110 transition-transform duration-300 ease-in-out">
|
||||
<img :alt="isFullScreen ? 'exit full-screen' : 'full-screen'" :src="isFullScreen ? '/assets/icons/modal/minimize.svg' : '/assets/icons/modal/increase-size-option.svg'" class="w-3.5 h-3.5 invert" draggable="false" />
|
||||
</button>
|
||||
<button @click="close" v-if="closable" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
|
||||
<img alt="close" draggable="false" src="/assets/icons/close-button-white.svg" class="w-full h-full" />
|
||||
<button v-if="closable" @click="emit('modal:close')" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
|
||||
<img alt="close" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" draggable="false" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="overflow-hidden grow relative">
|
||||
<div class="rounded-b-md absolute w-full h-full top-0 left-0 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center opacity-90"></div>
|
||||
<div class="rounded-b absolute w-full h-full top-0 left-0 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center opacity-90" />
|
||||
<div class="relative z-10 h-full">
|
||||
<slot name="modalBody" />
|
||||
</div>
|
||||
<img v-if="isResizable && !isFullScreen" src="/assets/icons/resize-icon.svg" alt="resize" class="absolute z-10 bottom-0 right-0 w-5 h-5 cursor-nwse-resize" @mousedown="startResize" />
|
||||
<img v-if="isResizable && !isFullScreen" src="/assets/icons/modal/resize-icon.svg" alt="resize" class="absolute z-10 bottom-0 right-0 w-5 h-5 cursor-nwse-resize" @mousedown="startResize" />
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineEmits, onMounted, onUnmounted, ref, watch, computed } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
isModalOpen: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
closable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
isResizable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
canFullScreen: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
modalPositionX: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
modalPositionY: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
modalWidth: {
|
||||
type: Number,
|
||||
default: 500
|
||||
},
|
||||
modalHeight: {
|
||||
type: Number,
|
||||
default: 280
|
||||
}
|
||||
interface ModalProps {
|
||||
isModalOpen: boolean
|
||||
closable?: boolean
|
||||
isResizable?: boolean
|
||||
canFullScreen?: boolean
|
||||
modalPositionX?: number
|
||||
modalPositionY?: number
|
||||
modalWidth?: number
|
||||
modalHeight?: number
|
||||
}
|
||||
|
||||
interface Position {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<ModalProps>(), {
|
||||
isModalOpen: false,
|
||||
closable: true,
|
||||
isResizable: true,
|
||||
canFullScreen: false,
|
||||
modalPositionX: 0,
|
||||
modalPositionY: 0,
|
||||
modalWidth: 500,
|
||||
modalHeight: 280
|
||||
})
|
||||
|
||||
const isModalOpenRef = ref(props.isModalOpen)
|
||||
const emit = defineEmits(['modal:close', 'character:create'])
|
||||
const emit = defineEmits<{
|
||||
'modal:close': []
|
||||
'character:create': []
|
||||
}>()
|
||||
|
||||
const isModalOpenRef = ref(props.isModalOpen)
|
||||
const width = ref(props.modalWidth)
|
||||
const height = ref(props.modalHeight)
|
||||
const x = ref(0)
|
||||
const y = ref(0)
|
||||
|
||||
const minWidth = ref(200)
|
||||
const minHeight = ref(100)
|
||||
const isResizing = ref(false)
|
||||
const isDragging = ref(false)
|
||||
const isFullScreen = ref(false)
|
||||
|
||||
let startX = 0
|
||||
let startY = 0
|
||||
let initialX = 0
|
||||
let initialY = 0
|
||||
let startWidth = 0
|
||||
let startHeight = 0
|
||||
let preFullScreenState = { x: 0, y: 0, width: 0, height: 0 }
|
||||
const minDimensions = {
|
||||
width: 200,
|
||||
height: 100
|
||||
}
|
||||
|
||||
let dragState = {
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
initialX: 0,
|
||||
initialY: 0,
|
||||
startWidth: 0,
|
||||
startHeight: 0
|
||||
}
|
||||
|
||||
let preFullScreenState: Position = { x: 0, y: 0, width: 0, height: 0 }
|
||||
|
||||
const modalStyle = computed(() => ({
|
||||
borderRadius: isFullScreen.value ? '0' : '6px',
|
||||
top: isFullScreen.value ? '0' : `${y.value}px`,
|
||||
left: isFullScreen.value ? '0' : `${x.value}px`,
|
||||
width: isFullScreen.value ? '100vw' : `${width.value}px`,
|
||||
height: isFullScreen.value ? '100vh' : `${height.value}px`,
|
||||
maxWidth: '100vw',
|
||||
maxHeight: '100vh'
|
||||
height: isFullScreen.value ? '100vh' : `${height.value}px`
|
||||
}))
|
||||
|
||||
function close() {
|
||||
emit('modal:close')
|
||||
}
|
||||
|
||||
function startResize(event: MouseEvent) {
|
||||
if (isFullScreen.value) return
|
||||
isResizing.value = true
|
||||
startWidth = width.value - event.clientX
|
||||
startHeight = height.value - event.clientY
|
||||
dragState.startWidth = width.value - event.clientX
|
||||
dragState.startHeight = height.value - event.clientY
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
function resizeModal(event: MouseEvent) {
|
||||
if (!isResizing.value || isFullScreen.value) return
|
||||
const newWidth = Math.min(startWidth + event.clientX, window.innerWidth)
|
||||
const newHeight = Math.min(startHeight + event.clientY, window.innerHeight)
|
||||
width.value = Math.max(newWidth, minWidth.value)
|
||||
height.value = Math.max(newHeight, minHeight.value)
|
||||
adjustPosition()
|
||||
}
|
||||
|
||||
function stopResize() {
|
||||
isResizing.value = false
|
||||
width.value = Math.max(dragState.startWidth + event.clientX, minDimensions.width)
|
||||
height.value = Math.max(dragState.startHeight + event.clientY, minDimensions.height)
|
||||
}
|
||||
|
||||
function startDrag(event: MouseEvent) {
|
||||
if (isFullScreen.value) return
|
||||
isDragging.value = true
|
||||
startX = event.clientX
|
||||
startY = event.clientY
|
||||
initialX = x.value
|
||||
initialY = y.value
|
||||
dragState = {
|
||||
startX: event.clientX,
|
||||
startY: event.clientY,
|
||||
initialX: x.value,
|
||||
initialY: y.value,
|
||||
startWidth: width.value,
|
||||
startHeight: height.value
|
||||
}
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
function drag(event: MouseEvent) {
|
||||
if (!isDragging.value || isFullScreen.value) return
|
||||
const dx = event.clientX - startX
|
||||
const dy = event.clientY - startY
|
||||
x.value = initialX + dx
|
||||
y.value = initialY + dy
|
||||
adjustPosition()
|
||||
}
|
||||
|
||||
function stopDrag() {
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
function adjustPosition() {
|
||||
if (isFullScreen.value) return
|
||||
x.value = Math.max(0, Math.min(x.value, window.innerWidth - width.value))
|
||||
y.value = Math.max(0, Math.min(y.value, window.innerHeight - height.value))
|
||||
}
|
||||
|
||||
function handleResize() {
|
||||
if (isFullScreen.value) return
|
||||
width.value = Math.min(width.value, window.innerWidth)
|
||||
height.value = Math.min(height.value, window.innerHeight)
|
||||
adjustPosition()
|
||||
}
|
||||
|
||||
function initializePosition() {
|
||||
width.value = Math.min(props.modalWidth, window.innerWidth)
|
||||
height.value = Math.min(props.modalHeight, window.innerHeight)
|
||||
if (props.modalPositionX !== 0 && props.modalPositionY !== 0) {
|
||||
x.value = props.modalPositionX
|
||||
y.value = props.modalPositionY
|
||||
} else {
|
||||
x.value = (window.innerWidth - width.value) / 2
|
||||
y.value = (window.innerHeight - height.value) / 2
|
||||
}
|
||||
x.value = dragState.initialX + (event.clientX - dragState.startX)
|
||||
y.value = dragState.initialY + (event.clientY - dragState.startY)
|
||||
}
|
||||
|
||||
function toggleFullScreen() {
|
||||
if (isFullScreen.value) {
|
||||
// Exit full-screen
|
||||
x.value = preFullScreenState.x
|
||||
y.value = preFullScreenState.y
|
||||
width.value = preFullScreenState.width
|
||||
height.value = preFullScreenState.height
|
||||
isFullScreen.value = false
|
||||
Object.assign({ x, y, width, height }, preFullScreenState)
|
||||
} else {
|
||||
// Enter full-screen
|
||||
preFullScreenState = { x: x.value, y: y.value, width: width.value, height: height.value }
|
||||
isFullScreen.value = true
|
||||
}
|
||||
isFullScreen.value = !isFullScreen.value
|
||||
}
|
||||
|
||||
function initializePosition() {
|
||||
width.value = props.modalWidth
|
||||
height.value = props.modalHeight
|
||||
x.value = props.modalPositionX || (window.innerWidth - width.value) / 2
|
||||
y.value = props.modalPositionY || (window.innerHeight - height.value) / 2
|
||||
}
|
||||
|
||||
// Watchers
|
||||
watch(
|
||||
() => props.isModalOpen,
|
||||
(value) => {
|
||||
isModalOpenRef.value = value
|
||||
if (value) {
|
||||
initializePosition()
|
||||
}
|
||||
if (value) initializePosition()
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.modalWidth,
|
||||
(value) => {
|
||||
width.value = Math.min(value, window.innerWidth)
|
||||
}
|
||||
(value) => (width.value = value)
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.modalHeight,
|
||||
(value) => {
|
||||
height.value = Math.min(value, window.innerHeight)
|
||||
}
|
||||
(value) => (height.value = value)
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.modalPositionX,
|
||||
(value) => {
|
||||
x.value = value
|
||||
}
|
||||
(value) => (x.value = value)
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.modalPositionY,
|
||||
(value) => {
|
||||
y.value = value
|
||||
}
|
||||
(value) => (y.value = value)
|
||||
)
|
||||
|
||||
// Lifecycle hooks
|
||||
onMounted(() => {
|
||||
window.addEventListener('mousemove', drag)
|
||||
window.addEventListener('mouseup', stopDrag)
|
||||
window.addEventListener('mousemove', resizeModal)
|
||||
window.addEventListener('mouseup', stopResize)
|
||||
if (props.modalPositionX !== 0 && props.modalPositionY !== 0) {
|
||||
window.addEventListener('resize', handleResize)
|
||||
const handlers: Record<string, EventListener[]> = {
|
||||
mousemove: [(e: Event) => drag(e as MouseEvent), (e: Event) => resizeModal(e as MouseEvent)],
|
||||
mouseup: [
|
||||
() => {
|
||||
isDragging.value = false
|
||||
},
|
||||
() => {
|
||||
isResizing.value = false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Object.entries(handlers).forEach(([event, fns]) => {
|
||||
fns.forEach((fn) => window.addEventListener(event, fn))
|
||||
})
|
||||
|
||||
initializePosition()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('mousemove', drag)
|
||||
window.removeEventListener('mouseup', stopDrag)
|
||||
window.removeEventListener('mousemove', resizeModal)
|
||||
window.removeEventListener('mouseup', stopResize)
|
||||
if (props.modalPositionX !== 0 && props.modalPositionY !== 0) {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
const handlers: Record<string, EventListener[]> = {
|
||||
mousemove: [(e: Event) => drag(e as MouseEvent), (e: Event) => resizeModal(e as MouseEvent)],
|
||||
mouseup: [
|
||||
() => {
|
||||
isDragging.value = false
|
||||
},
|
||||
() => {
|
||||
isResizing.value = false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Object.entries(handlers).forEach(([event, fns]) => {
|
||||
fns.forEach((fn) => window.removeEventListener(event, fn))
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
@ -12,7 +12,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import Modal from '@/components/utilities/Modal.vue'
|
||||
import { onBeforeMount, onBeforeUnmount, watch } from 'vue'
|
||||
import { onBeforeMount, onBeforeUnmount, onMounted, onUnmounted, watch } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
|
||||
@ -34,7 +34,7 @@ function setupNotificationListener(connection: any) {
|
||||
})
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
onMounted(() => {
|
||||
const connection = gameStore.connection
|
||||
if (connection) {
|
||||
setupNotificationListener(connection)
|
||||
@ -49,7 +49,7 @@ onBeforeMount(() => {
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
onUnmounted(() => {
|
||||
const connection = gameStore.connection
|
||||
if (connection) {
|
||||
connection.off('notification')
|
||||
|
@ -1,11 +1,10 @@
|
||||
<template>
|
||||
<Character v-for="item in zoneStore.characters" :key="item.id" :layer="tilemap" :character="item" />
|
||||
<Character v-for="item in zoneStore.characters" :key="item.character.id" :layer="tilemap" :zoneCharacter="item" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Character from '@/components/sprites/Character.vue'
|
||||
import { useZoneStore } from '@/stores/zoneStore'
|
||||
import { calculateIsometricDepth } from '@/composables/zoneComposable'
|
||||
|
||||
const zoneStore = useZoneStore()
|
||||
|
||||
|
@ -1,18 +1,21 @@
|
||||
<template>
|
||||
<ZoneTiles :key="zoneStore.zone?.id ?? 0" @tilemap:create="tileMap = $event" />
|
||||
<ZoneTiles :key="zoneStore.zone?.id ?? 0" @tileMap:create="tileMap = $event" />
|
||||
<ZoneObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
||||
<Characters v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onUnmounted } from 'vue'
|
||||
import { useScene } from 'phavuer'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useZoneStore } from '@/stores/zoneStore'
|
||||
import { onBeforeUnmount, ref, onBeforeMount } from 'vue'
|
||||
import type { Character as CharacterT, Zone as ZoneT, ExtendedCharacter as ExtendedCharacterT } from '@/types'
|
||||
import { loadZoneTilesIntoScene } from '@/composables/zoneComposable'
|
||||
import type { Zone as ZoneT, ZoneCharacter } from '@/types'
|
||||
import ZoneTiles from '@/components/zone/ZoneTiles.vue'
|
||||
import ZoneObjects from '@/components/zone/ZoneObjects.vue'
|
||||
import Characters from '@/components/zone/Characters.vue'
|
||||
|
||||
const scene = useScene()
|
||||
const gameStore = useGameStore()
|
||||
const zoneStore = useZoneStore()
|
||||
|
||||
@ -20,7 +23,7 @@ const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null)
|
||||
|
||||
type zoneLoadData = {
|
||||
zone: ZoneT
|
||||
characters: CharacterT[]
|
||||
characters: ZoneCharacter[]
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
@ -33,33 +36,33 @@ gameStore.connection!.on('zone:character:teleport', async (data: zoneLoadData) =
|
||||
zoneId: data.zone.id
|
||||
})
|
||||
|
||||
await loadZoneTilesIntoScene(data.zone, scene)
|
||||
zoneStore.setZone(data.zone)
|
||||
zoneStore.setCharacters(data.characters)
|
||||
})
|
||||
|
||||
gameStore.connection!.on('zone:character:join', async (data: ExtendedCharacterT) => {
|
||||
gameStore.connection!.on('zone:character:join', async (data: ZoneCharacter) => {
|
||||
// If data is from the current user, don't add it to the store
|
||||
if (data.id === gameStore.character?.id) return
|
||||
if (data.character.id === gameStore.character?.id) return
|
||||
zoneStore.addCharacter(data)
|
||||
})
|
||||
|
||||
gameStore.connection!.on('zone:character:leave', (character_id: number) => {
|
||||
zoneStore.removeCharacter(character_id)
|
||||
gameStore.connection!.on('zone:character:leave', (characterId: number) => {
|
||||
zoneStore.removeCharacter(characterId)
|
||||
})
|
||||
|
||||
gameStore.connection!.on('character:move', (data: ExtendedCharacterT) => {
|
||||
gameStore.connection!.on('character:move', (data: ZoneCharacter) => {
|
||||
zoneStore.updateCharacter(data)
|
||||
})
|
||||
|
||||
onBeforeMount(async () => {
|
||||
gameStore!.connection!.emit('zone:character:join', async (response: zoneLoadData) => {
|
||||
// Set zone and characters
|
||||
// Emit zone:character:join event to server and wait for response, then set zone and characters
|
||||
gameStore!.connection!.emit('zone:character:join', async (response: zoneLoadData) => {
|
||||
await loadZoneTilesIntoScene(response.zone, scene)
|
||||
zoneStore.setZone(response.zone)
|
||||
zoneStore.setCharacters(response.characters)
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
onUnmounted(() => {
|
||||
zoneStore.reset()
|
||||
gameStore.connection!.off('zone:character:teleport')
|
||||
gameStore.connection!.off('zone:character:join')
|
||||
|
@ -1,23 +1,29 @@
|
||||
<template>
|
||||
<Controls :layer="tiles" :depth="0" />
|
||||
<Controls :layer="tileLayer" :depth="0" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import config from '@/config'
|
||||
import { useScene } from 'phavuer'
|
||||
import { useZoneStore } from '@/stores/zoneStore'
|
||||
import { onBeforeMount, onBeforeUnmount } from 'vue'
|
||||
import { setLayerTiles } from '@/composables/zoneComposable'
|
||||
import { onBeforeUnmount } from 'vue'
|
||||
import { FlattenZoneArray, setLayerTiles } from '@/composables/zoneComposable'
|
||||
import Controls from '@/components/utilities/Controls.vue'
|
||||
import { unduplicateArray } from '@/utilities'
|
||||
|
||||
const emit = defineEmits(['tilemap:create'])
|
||||
const emit = defineEmits(['tileMap:create'])
|
||||
|
||||
const zoneStore = useZoneStore()
|
||||
const scene = useScene()
|
||||
const zoneTilemap = createTilemap()
|
||||
const tiles = createTileLayer()
|
||||
const zoneStore = useZoneStore()
|
||||
const tileMap = createTileMap()
|
||||
const tileLayer = createTileLayer()
|
||||
|
||||
function createTilemap() {
|
||||
/**
|
||||
* 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,
|
||||
@ -26,22 +32,26 @@ function createTilemap() {
|
||||
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
|
||||
|
||||
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 tilesFromZone = zoneStore.zone?.tiles || []
|
||||
const uniqueTiles = new Set(tilesFromZone.flat().filter(Boolean))
|
||||
const tilesArray = unduplicateArray(FlattenZoneArray(zoneStore.zone?.tiles ?? []))
|
||||
|
||||
const tilesetImages = Array.from(uniqueTiles).map((tile, index) => {
|
||||
return zoneTilemap.addTilesetImage(tile, tile, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y })
|
||||
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(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, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
|
||||
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)
|
||||
@ -49,16 +59,11 @@ function createTileLayer() {
|
||||
return layer
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
if (!zoneStore.zone?.tiles) {
|
||||
return
|
||||
}
|
||||
setLayerTiles(zoneTilemap, tiles, zoneStore.zone.tiles)
|
||||
})
|
||||
setLayerTiles(tileMap, tileLayer, zoneStore.zone?.tiles)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
zoneTilemap.destroyLayer('tiles')
|
||||
zoneTilemap.removeAllLayers()
|
||||
zoneTilemap.destroy()
|
||||
tileMap.destroyLayer('tiles')
|
||||
tileMap.removeAllLayers()
|
||||
tileMap.destroy()
|
||||
})
|
||||
</script>
|
||||
|
@ -1,22 +1,22 @@
|
||||
<template>
|
||||
<Image v-if="isTextureLoaded" v-bind="imageProps" />
|
||||
<Image v-if="gameStore.getLoadedAsset(props.zoneObject.object.id)" v-bind="imageProps" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { Image, useScene } from 'phavuer'
|
||||
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||
import { useAssetManager } from '@/utilities/assetManager'
|
||||
import type { ZoneObject } from '@/types'
|
||||
import { loadTexture } from '@/composables/gameComposable'
|
||||
import type { AssetDataT, ZoneObject } from '@/types'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
|
||||
const props = defineProps<{
|
||||
tilemap: Phaser.Tilemaps.Tilemap
|
||||
zoneObject: ZoneObject
|
||||
}>()
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const scene = useScene()
|
||||
const assetManager = useAssetManager
|
||||
const isTextureLoaded = ref(false)
|
||||
|
||||
const imageProps = computed(() => ({
|
||||
depth: calculateIsometricDepth(props.zoneObject.positionX, props.zoneObject.positionY, props.zoneObject.object.frameWidth, props.zoneObject.object.frameHeight),
|
||||
@ -28,36 +28,14 @@ const imageProps = computed(() => ({
|
||||
originX: Number(props.zoneObject.object.originY)
|
||||
}))
|
||||
|
||||
const loadTexture = async () => {
|
||||
const textureId = props.zoneObject.object.id
|
||||
|
||||
// Check if the texture is already loaded in Phaser
|
||||
if (scene.textures.exists(textureId)) {
|
||||
isTextureLoaded.value = true
|
||||
return
|
||||
}
|
||||
|
||||
let assetData = await assetManager.getAsset(textureId)
|
||||
|
||||
if (!assetData) {
|
||||
await assetManager.downloadAsset(textureId, `/assets/objects/${textureId}.png`, 'objects', props.zoneObject.object.updatedAt)
|
||||
assetData = await assetManager.getAsset(textureId)
|
||||
}
|
||||
|
||||
if (assetData) {
|
||||
return new Promise<void>((resolve) => {
|
||||
scene.textures.addBase64(textureId, assetData.data)
|
||||
scene.textures.once(`addtexture-${textureId}`, () => {
|
||||
isTextureLoaded.value = true
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadTexture().catch((error) => {
|
||||
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>
|
||||
|
34
src/composables/game/scene/loaderComposable.ts
Normal file
@ -0,0 +1,34 @@
|
||||
export function createSceneLoader(scene: Phaser.Scene) {
|
||||
const width = scene.cameras.main.width
|
||||
const height = scene.cameras.main.height
|
||||
|
||||
const progressBox = scene.add.graphics()
|
||||
const progressBar = scene.add.graphics()
|
||||
progressBox.fillStyle(0x222222, 0.8)
|
||||
progressBox.fillRect(width / 2 - 180, height / 2, 320, 50)
|
||||
|
||||
const loadingText = scene.make.text({
|
||||
x: width / 2,
|
||||
y: height / 2 - 50,
|
||||
text: 'Loading...',
|
||||
style: {
|
||||
font: '20px monospace',
|
||||
// @ts-ignore
|
||||
fill: '#ffffff'
|
||||
}
|
||||
})
|
||||
loadingText.setOrigin(0.5, 0.5)
|
||||
|
||||
scene.load.on(Phaser.Loader.Events.PROGRESS, function (value: any) {
|
||||
progressBar.clear()
|
||||
progressBar.fillStyle(0x368f8b, 1)
|
||||
progressBar.fillRect(width / 2 - 180 + 10, height / 2 + 10, 300 * value, 30)
|
||||
})
|
||||
|
||||
scene.load.on(Phaser.Loader.Events.COMPLETE, function () {
|
||||
progressBar.destroy()
|
||||
progressBox.destroy()
|
||||
loadingText.destroy()
|
||||
return true
|
||||
})
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
import type { AssetDataT, Sprite } from '@/types'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { AssetStorage } from '@/storage/assetStorage'
|
||||
import config from '@/config'
|
||||
|
||||
const textureLoadingPromises = new Map<string, Promise<boolean>>()
|
||||
|
||||
export async function loadTexture(scene: Phaser.Scene, assetData: AssetDataT): Promise<boolean> {
|
||||
const gameStore = useGameStore()
|
||||
const assetStorage = new AssetStorage()
|
||||
|
||||
// Check if the texture is already loaded in Phaser
|
||||
if (gameStore.game.loadedAssets.find((asset) => asset.key === assetData.key)) {
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
|
||||
// If there's already a loading promise for this texture, return it
|
||||
if (textureLoadingPromises.has(assetData.key)) {
|
||||
return await textureLoadingPromises.get(assetData.key)!
|
||||
}
|
||||
|
||||
// Create new loading promise
|
||||
const loadingPromise = (async () => {
|
||||
// Check if the asset is already cached
|
||||
let asset = await assetStorage.get(assetData.key)
|
||||
|
||||
// If asset is not found, download it
|
||||
if (!asset) {
|
||||
await assetStorage.download(assetData)
|
||||
asset = await assetStorage.get(assetData.key)
|
||||
}
|
||||
|
||||
// If asset is found, add it to the scene
|
||||
if (asset) {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
// Remove existing texture if it exists
|
||||
if (scene.textures.exists(asset.key)) {
|
||||
scene.textures.remove(asset.key)
|
||||
}
|
||||
|
||||
scene.textures.addBase64(asset.key, asset.data)
|
||||
scene.textures.once(`addtexture-${asset.key}`, () => {
|
||||
gameStore.game.loadedAssets.push(assetData)
|
||||
textureLoadingPromises.delete(assetData.key) // Clean up the promise
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
textureLoadingPromises.delete(assetData.key) // Clean up the promise
|
||||
return Promise.resolve(false)
|
||||
})()
|
||||
|
||||
// Store the loading promise
|
||||
textureLoadingPromises.set(assetData.key, loadingPromise)
|
||||
return loadingPromise
|
||||
}
|
||||
|
||||
export async function loadSpriteTextures(scene: Phaser.Scene, sprite: Sprite) {
|
||||
const sprite_actions = await fetch(config.server_endpoint + '/assets/list_sprite_actions/' + sprite?.id).then((response) => response.json())
|
||||
for await (const sprite_action of sprite_actions) {
|
||||
await loadTexture(scene, {
|
||||
key: sprite_action.key,
|
||||
data: sprite_action.data,
|
||||
group: sprite_action.isAnimated ? 'sprite_animations' : 'sprites',
|
||||
updatedAt: sprite_action.updatedAt,
|
||||
frameCount: sprite_action.frameCount,
|
||||
frameWidth: sprite_action.frameWidth,
|
||||
frameHeight: sprite_action.frameHeight
|
||||
} as AssetDataT)
|
||||
|
||||
// If the sprite is not animated, skip
|
||||
if (!sprite_action.isAnimated) continue
|
||||
|
||||
// Check if animation already exists
|
||||
if (scene.anims.get(sprite_action.key)) continue
|
||||
|
||||
// Add the animation to the scene
|
||||
const anim = scene.textures.get(sprite_action.key)
|
||||
scene.textures.addSpriteSheet(sprite_action.key, anim, { frameWidth: sprite_action.frameWidth ?? 0, frameHeight: sprite_action.frameHeight ?? 0 })
|
||||
scene.anims.create({
|
||||
key: sprite_action.key,
|
||||
frameRate: 7,
|
||||
frames: scene.anims.generateFrameNumbers(sprite_action.key, { start: 0, end: sprite_action.frameCount! - 1 }),
|
||||
repeat: -1
|
||||
})
|
||||
}
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ export function useGamePointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilema
|
||||
const { worldX, worldY } = pointer
|
||||
updateWaypoint(worldX, worldY)
|
||||
|
||||
if (gameStore.isPlayerDraggingCamera) {
|
||||
if (gameStore.game.isPlayerDraggingCamera) {
|
||||
const distance = Phaser.Math.Distance.Between(pointerStartPosition.value.x, pointerStartPosition.value.y, pointer.x, pointer.y)
|
||||
|
||||
if (distance > dragThreshold) {
|
||||
@ -48,7 +48,7 @@ export function useGamePointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilema
|
||||
if (distance <= dragThreshold) {
|
||||
const pointerTile = getTile(layer, pointer.worldX, pointer.worldY)
|
||||
if (pointerTile) {
|
||||
gameStore.connection?.emit('character:initMove', {
|
||||
gameStore.connection?.emit('character:move', {
|
||||
positionX: pointerTile.x,
|
||||
positionY: pointerTile.y
|
||||
})
|
||||
|
@ -26,7 +26,7 @@ export function useZoneEditorPointerHandlers(scene: Phaser.Scene, layer: Phaser.
|
||||
}
|
||||
|
||||
function dragZone(pointer: Phaser.Input.Pointer) {
|
||||
if (gameStore.isPlayerDraggingCamera) {
|
||||
if (gameStore.game.isPlayerDraggingCamera) {
|
||||
const { x, y, prevPosition } = pointer
|
||||
const { scrollX, scrollY, zoom } = camera
|
||||
camera.setScroll(scrollX - (x - prevPosition.x) / zoom, scrollY - (y - prevPosition.y) / zoom)
|
||||
|
@ -3,6 +3,8 @@ import Tilemap = Phaser.Tilemaps.Tilemap
|
||||
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
|
||||
import Tileset = Phaser.Tilemaps.Tileset
|
||||
import Tile = Phaser.Tilemaps.Tile
|
||||
import type { AssetDataT, Zone as ZoneT } from '@/types'
|
||||
import { loadTexture } from '@/composables/gameComposable'
|
||||
|
||||
export function getTile(layer: TilemapLayer | Tilemap, x: number, y: number): Tile | undefined {
|
||||
const tile = layer.getTileAtWorldXY(x, y)
|
||||
@ -45,12 +47,15 @@ export function tileToWorldY(layer: TilemapLayer | Tilemap, pos_x: number, pos_y
|
||||
export function placeTile(zone: Tilemap, layer: TilemapLayer, x: number, y: number, tileName: string) {
|
||||
let tileImg = zone.getTileset(tileName) as Tileset
|
||||
if (!tileImg) {
|
||||
console.log('tile not found:', tileName)
|
||||
tileImg = zone.getTileset('blank_tile') as Tileset
|
||||
}
|
||||
layer.putTileAt(tileImg.firstgid, x, y)
|
||||
}
|
||||
|
||||
export function setLayerTiles(zone: Tilemap, layer: TilemapLayer, tiles: string[][]) {
|
||||
if (!tiles) return
|
||||
|
||||
tiles.forEach((row: string[], y: number) => {
|
||||
row.forEach((tile: string, x: number) => {
|
||||
placeTile(zone, layer, x, y, tile)
|
||||
@ -69,3 +74,23 @@ export const calculateIsometricDepth = (x: number, y: number, width: number = 0,
|
||||
}
|
||||
return baseDepth + (width + height) / (2 * config.tile_size.x)
|
||||
}
|
||||
|
||||
export function FlattenZoneArray(tiles: string[][]) {
|
||||
const normalArray = []
|
||||
|
||||
for (const row of tiles) {
|
||||
normalArray.push(...row)
|
||||
}
|
||||
|
||||
return normalArray
|
||||
}
|
||||
|
||||
export async function loadZoneTilesIntoScene(zone: ZoneT, scene: Phaser.Scene) {
|
||||
// Fetch the list of tiles from the server
|
||||
const tileArray: AssetDataT[] = await fetch(config.server_endpoint + '/assets/list_tiles/' + zone.id).then((response) => response.json())
|
||||
|
||||
// Load each tile into the scene
|
||||
for (const tile of tileArray) {
|
||||
await loadTexture(scene, tile)
|
||||
}
|
||||
}
|
||||
|
@ -1,156 +0,0 @@
|
||||
<template>
|
||||
<div class="bg-gray-900 relative">
|
||||
<div class="absolute bg-[url('/assets/shapes/select-screen-bg-shape.svg')] bg-no-repeat bg-center w-full h-full"></div>
|
||||
<div class="ui-wrapper h-dvh flex flex-col justify-center items-center gap-20 px-10 sm:px-20">
|
||||
<div class="filler"></div>
|
||||
<div class="flex gap-14 w-full max-h-[650px] overflow-x-auto" v-if="!isLoading">
|
||||
<!-- CHARACTER LIST -->
|
||||
<div v-for="character in characters" :key="character.id" class="group first:ml-auto last:mr-auto m-4 w-[170px] h-[275px] flex flex-col shrink-0 relative shadow-character" :class="{ active: selected_character == character.id }">
|
||||
<img src="/assets/ui-elements/ui-box-outer.svg" class="absolute w-full h-full" />
|
||||
<img src="/assets/ui-elements/ui-box-inner.svg" class="absolute left-2 bottom-2 w-[calc(100%_-_16px)] h-[calc(100%_-_40px)]" />
|
||||
<input class="opacity-0 h-full w-full absolute m-0 z-10" type="radio" :id="character.id" name="character" :value="character.id" v-model="selected_character" />
|
||||
<label class="font-bold absolute left-1/2 top-4 max-w-32 -translate-x-1/2 -translate-y-1/2 text-center text-ellipsis overflow-hidden whitespace-nowrap drop-shadow-text" :for="character.id">{{ character.name }}</label>
|
||||
|
||||
<button
|
||||
class="delete bg-red w-8 h-8 p-[3px] rounded-full absolute -right-4 top-0 -translate-y-1/2 z-10 border-2 border-solid border-white hover:bg-red-300"
|
||||
@click="
|
||||
() => {
|
||||
deletingCharacter = character
|
||||
}
|
||||
"
|
||||
>
|
||||
<img draggable="false" src="/assets/icons/trashcan.svg" />
|
||||
</button>
|
||||
|
||||
<div class="sprite-container flex flex-col items-center m-auto">
|
||||
<img class="drop-shadow-20" draggable="false" src="/assets/avatar/default/0.png" />
|
||||
</div>
|
||||
<span class="absolute bottom-6 w-full text-center translate-y-1/2 z-10">Lvl. {{ character.level }}</span>
|
||||
<div class="selected-character group-[.active]:max-w-[170px] absolute max-w-0 w-4/6 h-[3px] bg-gray-500 rounded-[3px] left-1/2 -bottom-4 -translate-x-1/2 transition-all ease-in-out duration-300"></div>
|
||||
</div>
|
||||
|
||||
<div class="character new-character first:ml-auto mr-auto m-4 w-[170px] h-[275px] flex flex-col shrink-0 rounded-2xl relative bg-gray-500/50 bg-no-repeat shadow-character" v-if="characters.length < 4">
|
||||
<button class="h-full w-full py-10 flex flex-col justify-between" @click="isModalOpen = true">
|
||||
<div class="filler"></div>
|
||||
<img class="w-24 h-24 m-auto" draggable="false" src="/assets/icons/plus-icon.svg" />
|
||||
<span class="self-center text-base absolute bottom-5 w-full text-center translate-y-1/2 z-10">Create new</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<img class="w-20 invert-80" src="/assets/icons/loading-icon1.svg" />
|
||||
</div>
|
||||
|
||||
<div class="button-wrapper flex gap-8" v-if="!isLoading">
|
||||
<button
|
||||
class="btn-red py-2 pr-2.5 pl-8 min-w-24 relative rounded text-xl flex gap-4 items-center transition-all ease-in-out duration-200 hover:gap-5 disabled:bg-red/50 disabled:hover:bg-opacity-50 disabled:cursor-not-allowed disabled:hover:gap-[15px]"
|
||||
@click.stop="gameStore.disconnectSocket()"
|
||||
>
|
||||
<img class="h-8 drop-shadow-20 rotate-180" draggable="false" src="/assets/icons/arrow.svg" alt="Logout icon" />
|
||||
</button>
|
||||
<button
|
||||
class="btn-cyan py-2 px-2.5 pl-8 min-w-24 relative rounded text-xl flex gap-4 items-center transition-all ease-in-out duration-200 hover:gap-5 disabled:bg-cyan-800 disabled:hover:bg-opacity-50 disabled:cursor-not-allowed disabled:hover:gap-[15px]"
|
||||
:disabled="!selected_character"
|
||||
@click="select_character()"
|
||||
>
|
||||
PLAY
|
||||
<img class="h-8 drop-shadow-20" draggable="false" src="/assets/icons/arrow.svg" alt="Play icon" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CREATE CHARACTER MODAL -->
|
||||
<Modal :isModalOpen="isModalOpen" @modal:close="isModalOpen = false" :modal-width="430" :modal-height="275">
|
||||
<template #modalHeader>
|
||||
<h3 class="m-0 font-medium text-white">Create your character</h3>
|
||||
</template>
|
||||
|
||||
<template #modalBody>
|
||||
<div class="p-4 h-[calc(100%_-_32px)]">
|
||||
<form method="post" @submit.prevent="create" class="h-full flex flex-col justify-between">
|
||||
<div class="form-field-full">
|
||||
<label for="name" class="text-white">Nickname</label>
|
||||
<input class="input-field" v-model="name" name="name" id="name" placeholder="Enter a nickname.." />
|
||||
</div>
|
||||
<div class="grid grid-flow-col justify-stretch gap-4">
|
||||
<button type="button" class="btn-empty py-1.5 px-4 inline-block" @click.prevent="isModalOpen = false">Cancel</button>
|
||||
<button class="btn-cyan py-1.5 px-4 inline-block" type="submit">Create</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
|
||||
<!-- DELETE CHARACTER MODAL -->
|
||||
<ConfirmationModal v-if="deletingCharacter != null" :confirm-function="delete_character.bind(this, deletingCharacter.id)" :cancel-function="(() => (deletingCharacter = null)).bind(this)" confirm-button-text="Delete">
|
||||
<template #modalHeader>
|
||||
<h3 class="m-0 font-medium text-white">Delete character?</h3>
|
||||
</template>
|
||||
<template #modalBody>
|
||||
<p class="mt-0 mb-5 text-white text-lg">
|
||||
Do you want to permanently delete <span class="font-extrabold text-white">{{ deletingCharacter.name }}</span
|
||||
>?
|
||||
</p>
|
||||
</template>
|
||||
</ConfirmationModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import Modal from '@/components/utilities/Modal.vue'
|
||||
import { type Character as CharacterT } from '@/types'
|
||||
import ConfirmationModal from '@/components/utilities/ConfirmationModal.vue'
|
||||
|
||||
const isLoading = ref(true)
|
||||
const characters = ref([])
|
||||
const gameStore = useGameStore()
|
||||
const deletingCharacter = ref(null)
|
||||
|
||||
// Fetch characters
|
||||
gameStore.connection?.on('character:list', (data: any) => {
|
||||
characters.value = data
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
// wait 0.75 sec
|
||||
setTimeout(() => {
|
||||
gameStore.connection?.emit('character:list')
|
||||
isLoading.value = false
|
||||
}, 750)
|
||||
})
|
||||
|
||||
// Select character logics
|
||||
const selected_character = ref(null)
|
||||
function select_character() {
|
||||
if (!selected_character.value) return
|
||||
deletingCharacter.value = null
|
||||
gameStore.connection?.emit('character:connect', { character_id: selected_character.value })
|
||||
gameStore.connection?.on('character:connect', (data: CharacterT) => gameStore.setCharacter(data))
|
||||
}
|
||||
|
||||
// Delete character logics
|
||||
function delete_character(character_id: number) {
|
||||
if (!character_id) return
|
||||
deletingCharacter.value = null
|
||||
gameStore.connection?.emit('character:delete', { character_id: character_id })
|
||||
}
|
||||
|
||||
// Create character logics
|
||||
const isModalOpen = ref(false)
|
||||
const name = ref('')
|
||||
function create() {
|
||||
gameStore.connection?.on('character:create:success', (data: CharacterT) => {
|
||||
gameStore.setCharacter(data)
|
||||
isModalOpen.value = false
|
||||
})
|
||||
gameStore.connection?.emit('character:create', { name: name.value })
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
gameStore.connection?.off('character:list')
|
||||
gameStore.connection?.off('character:connect')
|
||||
gameStore.connection?.off('character:create:success')
|
||||
})
|
||||
</script>
|
@ -1,74 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col justify-center items-center h-dvh relative">
|
||||
<div v-if="!isLoaded" class="w-20 h-20 rounded-full border-4 border-solid border-gray-300 border-t-transparent animate-spin"></div>
|
||||
<button v-else @click="continueBtnClick" class="w-20 h-20 rounded-full bg-gray-500 flex items-center justify-center hover:bg-gray-600 transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
<div v-if="!isLoaded" class="text-center mt-6">
|
||||
<h1 class="text-2xl font-bold">Loading...</h1>
|
||||
<p class="text-gray-400">Please wait while we load the assets.</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" async>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import config from '@/config'
|
||||
import type { AssetT as ServerAsset } from '@/types'
|
||||
import { useAssetManager } from '@/utilities/assetManager'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
|
||||
/**
|
||||
* This component downloads all assets from the server and
|
||||
* stores them in the asset manager.
|
||||
*/
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const assetManager = useAssetManager
|
||||
const isLoaded = ref(false)
|
||||
|
||||
async function getAssets() {
|
||||
return fetch(config.server_endpoint + '/assets/')
|
||||
.then((response) => {
|
||||
if (!response.ok) throw new Error('Failed to fetch assets')
|
||||
console.log(response)
|
||||
return response.json()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error fetching assets:', error)
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
async function loadAssetsIntoAssetManager(assets: ServerAsset[]): Promise<void> {
|
||||
for (const asset of assets) {
|
||||
// Check if the asset is already loaded
|
||||
const existingAsset = await assetManager.getAsset(asset.key)
|
||||
|
||||
// Check if the asset needs to be updated
|
||||
if (!existingAsset || new Date(asset.updatedAt) > new Date(existingAsset.updatedAt)) {
|
||||
// Check if the asset is already loaded, if so, delete it
|
||||
if (existingAsset) {
|
||||
await assetManager.deleteAsset(asset.key)
|
||||
}
|
||||
|
||||
// Add the asset to the asset manager
|
||||
await assetManager.downloadAsset(asset.key, asset.url, asset.group, asset.updatedAt, asset.frameCount, asset.frameWidth, asset.frameHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function continueBtnClick() {
|
||||
gameStore.isAssetsLoaded = true
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const assets = await getAssets()
|
||||
if (assets) {
|
||||
await loadAssetsIntoAssetManager(assets)
|
||||
isLoaded.value = true
|
||||
}
|
||||
})
|
||||
</script>
|
@ -1,130 +0,0 @@
|
||||
<template>
|
||||
<div class="relative max-lg:h-dvh flex flex-row-reverse">
|
||||
<div class="lg:bg-gradient-to-l bg-gradient-to-b from-gray-900 to-transparent w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 z-10"></div>
|
||||
<div class="bg-[url('/assets/login/login-bg.png')] w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 bg-no-repeat bg-cover bg-center"></div>
|
||||
<div class="bg-gray-900 z-20 w-full lg:w-1/2 h-[65dvh] lg:h-dvh relative">
|
||||
<div class="h-dvh flex items-center lg:justify-center flex-col px-8 max-lg:pt-20">
|
||||
<img src="/assets/login/sq-logo-v1.svg" class="mb-10" />
|
||||
<div class="relative">
|
||||
<img src="/assets/ui-elements/ui-box-outer.svg" class="absolute w-full h-full" />
|
||||
<img src="/assets/ui-elements/ui-box-inner.svg" class="absolute left-2 top-2 w-[calc(100%_-_16px)] h-[calc(100%_-_16px)] max-lg:hidden" />
|
||||
|
||||
<!-- Login Form -->
|
||||
<form v-show="switchForm === 'login'" @submit.prevent="loginFunc" class="relative px-6 py-11">
|
||||
<div class="flex flex-col gap-5 p-2 mb-8 relative">
|
||||
<div class="w-full grid gap-3 relative">
|
||||
<input class="input-field xs:min-w-[350px] min-w-64" id="username-login" v-model="username" type="text" name="username" placeholder="Username" required autofocus />
|
||||
<div class="relative">
|
||||
<input class="input-field xs:min-w-[350px] min-w-64" id="password-login" v-model="password" :type="showPassword ? 'text' : 'password'" name="password" placeholder="Password" required />
|
||||
<button type="button" @click.prevent="showPassword = !showPassword" :class="{ 'eye-open': showPassword }" class="bg-[url('/assets/icons/eye.svg')] p-0 absolute right-3 w-4 h-3 top-1/2 -translate-y-1/2 bg-no-repeat"></button>
|
||||
</div>
|
||||
<span v-if="loginError" class="text-red-200 text-xs absolute top-full mt-1">{{ loginError }}</span>
|
||||
</div>
|
||||
<button class="inline-flex self-end p-0 text-cyan-300 text-base">Forgot password?</button>
|
||||
<button class="btn-cyan px-0 xs:w-full" type="submit">Play now</button>
|
||||
|
||||
<!-- Divider shape -->
|
||||
<div class="absolute w-40 h-0.5 -bottom-8 left-1/2 -translate-x-1/2 flex justify-between">
|
||||
<div class="w-0.5 h-full bg-gray-300"></div>
|
||||
<div class="w-36 h-full bg-gray-300"></div>
|
||||
<div class="w-0.5 h-full bg-gray-300"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-8">
|
||||
<p class="m-0 text-center">Don't have an account? <button class="text-cyan-300 text-base p-0" @click.prevent="switchForm = 'register'">Sign up</button></p>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Register Form -->
|
||||
<form v-show="switchForm === 'register'" @submit.prevent="registerFunc" class="relative px-6 py-11">
|
||||
<div class="flex flex-col gap-5 p-2 mb-8 relative">
|
||||
<div class="w-full grid gap-3 relative">
|
||||
<input class="input-field xs:min-w-[350px] min-w-64" id="username-register" v-model="username" type="text" name="username" placeholder="Username" required autofocus />
|
||||
<div class="relative">
|
||||
<input class="input-field xs:min-w-[350px] min-w-64" id="password-register" v-model="password" :type="showPassword ? 'text' : 'password'" name="password" placeholder="Password" required />
|
||||
<button type="button" @click.prevent="showPassword = !showPassword" :class="{ 'eye-open': showPassword }" class="bg-[url('/assets/icons/eye.svg')] p-0 absolute right-3 w-4 h-3 top-1/2 -translate-y-1/2 bg-no-repeat"></button>
|
||||
</div>
|
||||
<span v-if="loginError" class="text-red-200 text-xs absolute top-full mt-1">{{ loginError }}</span>
|
||||
</div>
|
||||
<button class="btn-cyan xs:w-full" type="submit">Register now</button>
|
||||
|
||||
<!-- Divider shape -->
|
||||
<div class="absolute w-40 h-0.5 -bottom-8 left-1/2 -translate-x-1/2 flex justify-between">
|
||||
<div class="w-0.5 h-full bg-gray-300"></div>
|
||||
<div class="w-36 h-full bg-gray-300"></div>
|
||||
<div class="w-0.5 h-full bg-gray-300"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-8">
|
||||
<p class="m-0 text-center">Already have an account? <button class="text-cyan-300 text-base p-0" @click.prevent="switchForm = 'login'">Log in</button></p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { login, register } from '@/services/authentication'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const switchForm = ref('login')
|
||||
const loginError = ref('')
|
||||
const showPassword = ref(false)
|
||||
|
||||
// automatic login because of development
|
||||
onMounted(async () => {
|
||||
const token = useCookies().get('token')
|
||||
if (token) {
|
||||
gameStore.setToken(token)
|
||||
gameStore.initConnection()
|
||||
}
|
||||
})
|
||||
|
||||
async function loginFunc() {
|
||||
// check if username and password are valid
|
||||
if (username.value === '' || password.value === '') {
|
||||
loginError.value = 'Please enter a valid username and password'
|
||||
return
|
||||
}
|
||||
|
||||
// send login event to server
|
||||
const response = await login(username.value, password.value)
|
||||
|
||||
if (response.success === undefined) {
|
||||
loginError.value = response.error
|
||||
return
|
||||
}
|
||||
gameStore.setToken(response.token)
|
||||
gameStore.initConnection()
|
||||
return true // Indicate success
|
||||
}
|
||||
|
||||
async function registerFunc() {
|
||||
// check if username and password are valid
|
||||
if (username.value === '' || password.value === '') {
|
||||
loginError.value = 'Please enter a valid username and password'
|
||||
return
|
||||
}
|
||||
|
||||
// send register event to server
|
||||
const response = await register(username.value, password.value)
|
||||
|
||||
if (response.success === undefined) {
|
||||
loginError.value = response.error
|
||||
return
|
||||
}
|
||||
|
||||
const loginSuccess = await loginFunc()
|
||||
if (!loginSuccess) {
|
||||
loginError.value = 'Login after registration failed. Please try logging in manually.'
|
||||
return
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,13 +1,17 @@
|
||||
import axios from 'axios'
|
||||
import config from '@/config'
|
||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||
import { getDomain } from '@/utilities'
|
||||
|
||||
export async function register(username: string, password: string) {
|
||||
export async function register(username: string, email: string, password: string) {
|
||||
try {
|
||||
const response = await axios.post(`${config.server_endpoint}/register`, { username, password })
|
||||
const response = await axios.post(`${config.server_endpoint}/register`, { username, email, password })
|
||||
useCookies().set('token', response.data.token as string)
|
||||
return { success: true, token: response.data.token }
|
||||
} catch (error: any) {
|
||||
if (typeof error.response?.data === 'undefined') {
|
||||
return { error: 'Could not connect to server' }
|
||||
}
|
||||
return { error: error.response.data.message }
|
||||
}
|
||||
}
|
||||
@ -16,12 +20,37 @@ export async function login(username: string, password: string) {
|
||||
try {
|
||||
const response = await axios.post(`${config.server_endpoint}/login`, { username, password })
|
||||
useCookies().set('token', response.data.token as string, {
|
||||
// for whole domain
|
||||
// @TODO : #190
|
||||
// domain: window.location.hostname.split('.').slice(-2).join('.')
|
||||
domain: getDomain()
|
||||
})
|
||||
return { success: true, token: response.data.token }
|
||||
} catch (error: any) {
|
||||
if (typeof error.response?.data === 'undefined') {
|
||||
return { error: 'Could not connect to server' }
|
||||
}
|
||||
return { error: error.response.data.message }
|
||||
}
|
||||
}
|
||||
|
||||
export async function resetPassword(email: string) {
|
||||
try {
|
||||
const response = await axios.post(`${config.server_endpoint}/reset-password`, { email })
|
||||
return { success: true, token: response.data.token }
|
||||
} catch (error: any) {
|
||||
if (typeof error.response?.data === 'undefined') {
|
||||
return { error: 'Could not connect to server' }
|
||||
}
|
||||
return { error: error.response.data.message }
|
||||
}
|
||||
}
|
||||
|
||||
export async function newPassword(urlToken: string, password: string) {
|
||||
try {
|
||||
const response = await axios.post(`${config.server_endpoint}/new-password`, { urlToken, password })
|
||||
return { success: true, token: response.data.token }
|
||||
} catch (error: any) {
|
||||
if (typeof error.response?.data === 'undefined') {
|
||||
return { error: 'Could not connect to server' }
|
||||
}
|
||||
return { error: error.response.data.message }
|
||||
}
|
||||
}
|
||||
|
69
src/storage/assetStorage.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import config from '@/config'
|
||||
import Dexie from 'dexie'
|
||||
import type { AssetDataT } from '@/types'
|
||||
|
||||
export class AssetStorage {
|
||||
private db: Dexie
|
||||
|
||||
constructor() {
|
||||
this.db = new Dexie('assets')
|
||||
this.db.version(1).stores({
|
||||
assets: 'key, group'
|
||||
})
|
||||
}
|
||||
|
||||
async download(asset: AssetDataT) {
|
||||
try {
|
||||
// Check if the asset already exists, then check if updatedAt is newer
|
||||
const _asset = await this.db.table('assets').get(asset.key)
|
||||
if (_asset && _asset.updatedAt > asset.updatedAt) {
|
||||
return
|
||||
}
|
||||
|
||||
// Download the asset
|
||||
const response = await fetch(config.server_endpoint + asset.data)
|
||||
const blob = await response.blob()
|
||||
|
||||
// Store the asset in the database
|
||||
await this.db.table('assets').put({ key: asset.key, data: blob, group: asset.group, updatedAt: asset.updatedAt, frameCount: asset.frameCount, frameWidth: asset.frameWidth, frameHeight: asset.frameHeight })
|
||||
} catch (error) {
|
||||
console.error(`Failed to add asset ${asset.key}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
async get(key: string) {
|
||||
try {
|
||||
const asset = await this.db.table('assets').get(key)
|
||||
if (asset) {
|
||||
return {
|
||||
...asset,
|
||||
data: URL.createObjectURL(asset.data) // Convert blob to data URL
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to retrieve asset ${key}:`, error)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async getByGroup(group: string) {
|
||||
try {
|
||||
const assets = await this.db.table('assets').where('group').equals(group).toArray()
|
||||
return assets.map((asset) => ({
|
||||
...asset,
|
||||
data: URL.createObjectURL(asset.data) // Convert blob to data URL
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error(`Failed to retrieve assets for group ${group}:`, error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
async delete(key: string) {
|
||||
try {
|
||||
await this.db.table('assets').delete(key)
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete asset ${key}:`, error)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,27 +1,29 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { io, Socket } from 'socket.io-client'
|
||||
import type { Asset, Character, Notification, User, WorldSettings } from '@/types'
|
||||
import type { AssetDataT, Character, Notification, User, WorldSettings } from '@/types'
|
||||
import config from '@/config'
|
||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||
import { getDomain } from '@/utilities'
|
||||
|
||||
export const useGameStore = defineStore('game', {
|
||||
state: () => {
|
||||
return {
|
||||
notifications: [] as Notification[],
|
||||
isAssetsLoaded: false,
|
||||
loadedAssets: [] as string[],
|
||||
token: '' as string | null,
|
||||
connection: null as Socket | null,
|
||||
user: null as User | null,
|
||||
character: null as Character | null,
|
||||
isPlayerDraggingCamera: false,
|
||||
world: {
|
||||
date: new Date(),
|
||||
isRainEnabled: false,
|
||||
isFogEnabled: false,
|
||||
fogDensity: 0.5
|
||||
} as WorldSettings,
|
||||
gameSettings: {
|
||||
game: {
|
||||
isLoading: false,
|
||||
isLoaded: false, // isLoaded is currently being used to determine if the player has interacted with the game
|
||||
loadedAssets: [] as AssetDataT[],
|
||||
isPlayerDraggingCamera: false,
|
||||
isCameraFollowingCharacter: false
|
||||
},
|
||||
uiSettings: {
|
||||
@ -31,6 +33,17 @@ export const useGameStore = defineStore('game', {
|
||||
}
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
getLoadedAssets: (state) => {
|
||||
return state.game.loadedAssets
|
||||
},
|
||||
getLoadedAsset: (state) => {
|
||||
return (key: string) => state.game.loadedAssets.find((asset) => asset.key === key)
|
||||
},
|
||||
getLoadedAssetsByGroup: (state) => {
|
||||
return (group: string) => state.game.loadedAssets.filter((asset) => asset.group === group)
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
addNotification(notification: Notification) {
|
||||
if (!notification.id) {
|
||||
@ -53,17 +66,8 @@ export const useGameStore = defineStore('game', {
|
||||
toggleGmPanel() {
|
||||
this.uiSettings.isGmPanelOpen = !this.uiSettings.isGmPanelOpen
|
||||
},
|
||||
togglePlayerDraggingCamera() {
|
||||
this.isPlayerDraggingCamera = !this.isPlayerDraggingCamera
|
||||
},
|
||||
setPlayerDraggingCamera(moving: boolean) {
|
||||
this.isPlayerDraggingCamera = moving
|
||||
},
|
||||
toggleCameraFollowingCharacter() {
|
||||
this.gameSettings.isCameraFollowingCharacter = !this.gameSettings.isCameraFollowingCharacter
|
||||
},
|
||||
setCameraFollowingCharacter(following: boolean) {
|
||||
this.gameSettings.isCameraFollowingCharacter = following
|
||||
this.game.isPlayerDraggingCamera = moving
|
||||
},
|
||||
toggleChat() {
|
||||
this.uiSettings.isChatOpen = !this.uiSettings.isChatOpen
|
||||
@ -101,21 +105,22 @@ export const useGameStore = defineStore('game', {
|
||||
this.connection?.disconnect()
|
||||
|
||||
useCookies().remove('token', {
|
||||
// for whole domain
|
||||
// @TODO : #190
|
||||
// domain: window.location.hostname.split('.').slice(-2).join('.')
|
||||
domain: getDomain()
|
||||
})
|
||||
|
||||
this.isAssetsLoaded = false
|
||||
this.connection = null
|
||||
this.token = null
|
||||
this.user = null
|
||||
this.character = null
|
||||
this.uiSettings.isGmPanelOpen = false
|
||||
this.isPlayerDraggingCamera = false
|
||||
this.gameSettings.isCameraFollowingCharacter = false
|
||||
|
||||
this.game.isLoaded = false
|
||||
this.game.loadedAssets = []
|
||||
this.game.isPlayerDraggingCamera = false
|
||||
this.game.isCameraFollowingCharacter = false
|
||||
|
||||
this.uiSettings.isChatOpen = false
|
||||
this.uiSettings.isCharacterProfileOpen = false
|
||||
this.uiSettings.isGmPanelOpen = false
|
||||
|
||||
this.world.date = new Date()
|
||||
this.world.isRainEnabled = false
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import type { Zone, Object, Tile, ZoneEffect } from '@/types'
|
||||
import type { Zone, Object, Tile, ZoneEffect, ZoneObject } from '@/types'
|
||||
|
||||
export type TeleportSettings = {
|
||||
toZoneId: number
|
||||
@ -20,14 +20,14 @@ export const useZoneEditorStore = defineStore('zoneEditor', {
|
||||
zoneList: [] as Zone[],
|
||||
tileList: [] as Tile[],
|
||||
objectList: [] as Object[],
|
||||
selectedTile: null as Tile | null,
|
||||
selectedTile: '',
|
||||
selectedObject: null as Object | null,
|
||||
objectDepth: 0,
|
||||
isTileListModalShown: false,
|
||||
isObjectListModalShown: false,
|
||||
isZoneListModalShown: false,
|
||||
isCreateZoneModalShown: false,
|
||||
isSettingsModalShown: false,
|
||||
shouldClearTiles: false,
|
||||
zoneSettings: {
|
||||
name: '',
|
||||
width: 0,
|
||||
@ -88,15 +88,12 @@ export const useZoneEditorStore = defineStore('zoneEditor', {
|
||||
setObjectList(objects: Object[]) {
|
||||
this.objectList = objects
|
||||
},
|
||||
setSelectedTile(tile: Tile) {
|
||||
setSelectedTile(tile: string) {
|
||||
this.selectedTile = tile
|
||||
},
|
||||
setSelectedObject(object: any) {
|
||||
setSelectedObject(object: Object) {
|
||||
this.selectedObject = object
|
||||
},
|
||||
setObjectDepth(depth: number) {
|
||||
this.objectDepth = depth
|
||||
},
|
||||
toggleSettingsModal() {
|
||||
this.isSettingsModalShown = !this.isSettingsModalShown
|
||||
},
|
||||
@ -110,6 +107,13 @@ export const useZoneEditorStore = defineStore('zoneEditor', {
|
||||
setTeleportSettings(teleportSettings: TeleportSettings) {
|
||||
this.teleportSettings = teleportSettings
|
||||
},
|
||||
triggerClearTiles() {
|
||||
this.shouldClearTiles = true
|
||||
},
|
||||
|
||||
resetClearTilesFlag() {
|
||||
this.shouldClearTiles = false
|
||||
},
|
||||
reset(resetZone = false) {
|
||||
if (resetZone) this.zone = null
|
||||
this.zoneList = []
|
||||
@ -117,12 +121,14 @@ export const useZoneEditorStore = defineStore('zoneEditor', {
|
||||
this.objectList = []
|
||||
this.tool = 'move'
|
||||
this.drawMode = 'tile'
|
||||
this.selectedTile = null
|
||||
this.selectedTile = ''
|
||||
this.selectedObject = null
|
||||
this.objectDepth = 0
|
||||
this.isTileListModalShown = false
|
||||
this.isObjectListModalShown = false
|
||||
this.isSettingsModalShown = false
|
||||
this.isZoneListModalShown = false
|
||||
this.isCreateZoneModalShown = false
|
||||
this.shouldClearTiles = false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -1,17 +1,17 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import type { ExtendedCharacter, Zone } from '@/types'
|
||||
import type { ZoneCharacter, Zone } from '@/types'
|
||||
|
||||
export const useZoneStore = defineStore('zone', {
|
||||
state: () => {
|
||||
return {
|
||||
zone: null as Zone | null,
|
||||
characters: [] as ExtendedCharacter[],
|
||||
characters: [] as ZoneCharacter[],
|
||||
characterLoaded: false
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
getCharacterById: (state) => {
|
||||
return (id: number) => state.characters.find((char) => char.id === id)
|
||||
return (id: number) => state.characters.find((char) => char.character.id === id)
|
||||
},
|
||||
getCharacterCount: (state) => {
|
||||
return state.characters.length
|
||||
@ -24,20 +24,18 @@ export const useZoneStore = defineStore('zone', {
|
||||
setZone(zone: Zone | null) {
|
||||
this.zone = zone
|
||||
},
|
||||
setCharacters(characters: ExtendedCharacter[]) {
|
||||
setCharacters(characters: ZoneCharacter[]) {
|
||||
this.characters = characters
|
||||
},
|
||||
addCharacter(character: ExtendedCharacter) {
|
||||
addCharacter(character: ZoneCharacter) {
|
||||
this.characters.push(character)
|
||||
},
|
||||
updateCharacter(updatedCharacter: ExtendedCharacter) {
|
||||
const index = this.characters.findIndex((char) => char.id === updatedCharacter.id)
|
||||
if (index !== -1) {
|
||||
this.characters[index] = { ...this.characters[index], ...updatedCharacter }
|
||||
}
|
||||
updateCharacter(updatedCharacter: ZoneCharacter) {
|
||||
const index = this.characters.findIndex((char) => char.character.id === updatedCharacter.character.id)
|
||||
if (index !== -1) this.characters[index] = updatedCharacter
|
||||
},
|
||||
removeCharacter(character_id: number) {
|
||||
this.characters = this.characters.filter((char) => char.id !== character_id)
|
||||
removeCharacter(characterId: number) {
|
||||
this.characters = this.characters.filter((char) => char.character.id !== characterId)
|
||||
},
|
||||
setCharacterLoaded(loaded: boolean) {
|
||||
this.characterLoaded = loaded
|
||||
|
17
src/types.ts
@ -4,11 +4,12 @@ export type Notification = {
|
||||
message?: string
|
||||
}
|
||||
|
||||
export type AssetT = {
|
||||
export type AssetDataT = {
|
||||
key: string
|
||||
url: string
|
||||
data: string
|
||||
group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
|
||||
updatedAt: Date
|
||||
isAnimated?: boolean
|
||||
frameCount?: number
|
||||
frameWidth?: number
|
||||
frameHeight?: number
|
||||
@ -165,6 +166,11 @@ export type Character = {
|
||||
items: CharacterItem[]
|
||||
}
|
||||
|
||||
export type ZoneCharacter = {
|
||||
character: Character
|
||||
isMoving?: boolean
|
||||
}
|
||||
|
||||
export type ExtendedCharacter = Character & {
|
||||
isMoving?: boolean
|
||||
}
|
||||
@ -223,3 +229,10 @@ export type WorldSettings = {
|
||||
isFogEnabled: boolean
|
||||
fogDensity: number
|
||||
}
|
||||
|
||||
export type WeatherState = {
|
||||
isRainEnabled: boolean
|
||||
rainPercentage: number
|
||||
isFogEnabled: boolean
|
||||
fogDensity: number
|
||||
}
|
||||
|
@ -1,3 +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('.')
|
||||
}
|
||||
|
@ -1,72 +0,0 @@
|
||||
import config from '@/config'
|
||||
import Dexie from 'dexie'
|
||||
|
||||
class AssetManager extends Dexie {
|
||||
assets!: Dexie.Table<
|
||||
{
|
||||
key: string
|
||||
data: Blob
|
||||
group: string
|
||||
updatedAt: Date
|
||||
frameCount?: number
|
||||
frameWidth?: number
|
||||
frameHeight?: number
|
||||
},
|
||||
string
|
||||
>
|
||||
|
||||
constructor() {
|
||||
super('Assets')
|
||||
this.version(1).stores({
|
||||
assets: 'key, group'
|
||||
})
|
||||
}
|
||||
|
||||
async downloadAsset(key: string, url: string, group: string, updatedAt: Date, frameCount?: number, frameWidth?: number, frameHeight?: number) {
|
||||
try {
|
||||
const response = await fetch(config.server_endpoint + url)
|
||||
const blob = await response.blob()
|
||||
await this.assets.put({ key, data: blob, group, updatedAt, frameCount, frameWidth, frameHeight })
|
||||
} catch (error) {
|
||||
console.error(`Failed to add asset ${key}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
async getAsset(key: string) {
|
||||
try {
|
||||
const asset = await this.assets.get(key)
|
||||
if (asset) {
|
||||
return {
|
||||
...asset,
|
||||
data: URL.createObjectURL(asset.data)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to retrieve asset ${key}:`, error)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async getAssetsByGroup(group: string) {
|
||||
try {
|
||||
const assets = await this.assets.where('group').equals(group).toArray()
|
||||
return assets.map((asset) => ({
|
||||
...asset,
|
||||
data: URL.createObjectURL(asset.data)
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error(`Failed to retrieve assets for group ${group}:`, error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
async deleteAsset(key: string) {
|
||||
try {
|
||||
await this.assets.delete(key)
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete asset ${key}:`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const useAssetManager = new AssetManager()
|
@ -2,12 +2,14 @@ import { fileURLToPath, URL } from 'node:url'
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import VueDevTools from 'vite-plugin-vue-devtools'
|
||||
import viteCompression from 'vite-plugin-compression';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
VueDevTools(),
|
||||
viteCompression()
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
|