Compare commits
66 Commits
feature/#2
...
feature/ne
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
f75528b2af | |||
141cd225c8 | |||
2870fea15e | |||
8374dc91da |
@ -1,4 +1,4 @@
|
|||||||
VITE_NAME=New Quest
|
VITE_NAME=Sylvan Quest
|
||||||
VITE_DEVELOPMENT=true
|
VITE_DEVELOPMENT=true
|
||||||
VITE_SERVER_ENDPOINT=http://localhost:4000
|
VITE_SERVER_ENDPOINT=http://localhost:4000
|
||||||
VITE_TILE_SIZE_X=64
|
VITE_TILE_SIZE_X=64
|
||||||
|
684
package-lock.json
generated
@ -50,6 +50,7 @@
|
|||||||
"tailwindcss": "^3.4.13",
|
"tailwindcss": "^3.4.13",
|
||||||
"typescript": "~5.6.2",
|
"typescript": "~5.6.2",
|
||||||
"vite": "^5.4.9",
|
"vite": "^5.4.9",
|
||||||
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vite-plugin-vue-devtools": "^7.5.2",
|
"vite-plugin-vue-devtools": "^7.5.2",
|
||||||
"vitest": "^2.0.3",
|
"vitest": "^2.0.3",
|
||||||
"vue-tsc": "^1.6.5"
|
"vue-tsc": "^1.6.5"
|
||||||
|
Before Width: | Height: | Size: 4.5 MiB |
Before Width: | Height: | Size: 4.5 MiB |
Before Width: | Height: | Size: 2.7 MiB |
Before Width: | Height: | Size: 440 KiB |
@ -1,2 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
<svg width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#b1b2b5" d="M226.125 32.032a84.287 84.287 0 0 0-3.51.026c-11.4.318-24.464 2.935-40.945 8.63l-2.682.926-2.726-.777c-38.965-11.123-65.026.52-90.262 14.957 5.557 9.074 10.35 19.569 8.365 32.049l-.045.29-.066.286c-2.043 9.01-7.64 22.012-14.363 35.676-1.723 3.502-3.57 6.712-5.37 10.1 12.791-6.326 24.681-12.344 38.95-13.885l2.53-.274 2.302 1.094c-.018-.008 2.436.899 5.261 1.853 2.826.955 6.331 2.16 9.977 3.68 5.207 2.172 10.979 4.697 15.512 9.256 22.303-6.732 40.75-6.51 60.802-1.414 18.213-10.407 34.866-11.39 47.99-6.512 8.553 3.178 15.291 8.373 20.93 13.842l10.557-22.625 20.805 3.62c9.72-15.164 9.644-33.821 3.234-49.167-7.012-16.786-20.608-27.857-34.928-26.795l-2.404.178-2.172-1.047c-16.758-8.082-30.442-13.73-47.742-13.967zM112.791 138.688a44.845 44.845 0 0 0-3.287.745c-7.307 47.909-26.314 187.24-6.9 293.404 158.558 12.92 313.945 3.207 334.82 1.793.099-2.715.066-5.485-.121-8.313-1.354-20.44-10.822-42.312-22.235-55.045-6.318-7.049-23.99-13.695-48.029-16.789-24.04-3.094-54.057-3.4-85.057-2.056l-6.337.275c-21.183 1.115-40.742 2.29-58.89 5.111-4.051 8.895-13.356 14.998-23.735 14.998-13.904 0-25.885-10.947-25.885-24.892s11.981-24.89 25.885-24.89c5.778 0 11.22 1.896 15.613 5.107 8.084-10.448 16.365-21.222 25.375-31.897-8.386-8.22-17.33-15.993-27.44-22.722-3.941 2.49-8.595 3.943-13.548 3.943-13.842 0-25.39-11.288-25.39-25.14 0-13.854 11.548-25.141 25.39-25.141 2.64 0 5.194.413 7.603 1.174l33.02-38.875c-8.516-9.7-17.736-19.149-28.409-28.37-4.074 2.75-8.973 4.364-14.2 4.364-10.517 0-19.703-6.518-23.513-15.723-5.762 1.045-11.908 2.718-18.841 5.137l-7.551 2.634-3.504-7.189c-.353-.723-5.078-4.58-11.012-7.055-2.967-1.237-6.073-2.315-8.81-3.24-1.348-.455-2.457-.895-3.582-1.31l-1.43-.038zm96.748 2.565c13.9 11.186 25.49 22.656 35.904 34.33l18.24-21.475c-.711-3.822-1.422-7.642-2.142-11.478l-52.002-1.377zm58.313 35.754l-10.586 12.463c5.451 6.674 10.675 13.403 15.857 20.174-1.556-10.837-3.35-21.714-5.271-32.637zm-22.471 26.455l-30.586 36.012a24.652 24.652 0 0 1 3.613 12.845c0 2.354-.34 4.63-.96 6.793 10.699 7.168 20.067 15.208 28.613 23.489 9.55-10.298 19.972-20.277 31.78-29.457a476.305 476.305 0 0 0-.661-8.694c-11.152-14.168-21.175-27.806-31.8-40.988zm-52.361 41.717c-4.261 0-7.39 3.165-7.39 7.14 0 3.976 3.129 7.14 7.39 7.14 4.26 0 7.388-3.164 7.388-7.14 0-3.975-3.128-7.14-7.388-7.14zm85.732 30.572c-7.128 6.278-13.756 12.912-20.057 19.762 6.337 6.753 12.324 13.432 18.213 19.724 1.44-13.148 2-26.304 1.844-39.486zm-32.096 33.514c-8.101 9.677-15.774 19.602-23.41 29.44 15.249-1.938 31.118-2.91 47.58-3.776-8.238-8.276-16.088-17.033-24.17-25.664zm-53.636 31.763c-4.748 0-7.885 3.282-7.885 6.89 0 3.61 3.137 6.893 7.885 6.893 4.747 0 7.884-3.283 7.884-6.892 0-3.609-3.137-6.89-7.884-6.89zm-86.577 110.164c1.968 8.31 4.2 16.358 6.746 24.059 103.476 5.837 209.68 7.195 303.832-1.3 8.328-5.386 13.8-12.612 16.975-21.06-36.35 2.27-180.346 9.84-327.553-1.699z"/></svg>
|
|
Before Width: | Height: | Size: 3.0 KiB |
@ -1,2 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
<svg width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#b1b2b5" d="M162 35.75l-94.49 27.1c-12.05 6.3-23.47 23.9-31.01 46.35-6.07 18.2-9.62 38.9-10.93 58.3L136.7 112zm188 .1L375.4 112l111 55.6c-1.3-19.3-4.9-40.2-10.9-58.3-5.7-17.05-13.6-31.35-22.5-40.05-2.7-2.8-5.5-4.9-8.4-6.4zm-172.9 11.5l-25.7 77.45-92.9 46.4 14.08 53.5 88.82 44.4 94.6-15.9 94.6 15.9 88.8-44.4 14.1-53.5-92.8-46.4-25.8-77.35h-10.5l-59.3 73.95-.1 61.1h-18.1l.1-61-59.3-74.15zM78.65 247.7l22.05 83.9 146.2-43.8v-14.7l-88.4 14.7zm354.75 0l-80 40.1-88.4-14.7v14.7l146.3 43.8zm-186.5 58.7l-31.6 9.6-35.1 70.2 66.7-33.3zm18.1 0v46.5l66.9 33.4-35.2-70.3zM191.7 323l-86.4 26 25.3 96.3zm128.6.1l61.1 122.1 25.3-96.2zm-55.3 50l.1 43.2 100.7 37.8-20.4-40.8zm-18.1 0l-80.2 40.1-20.5 40.9L247 416.3zm.1 62.4l-81.6 30.6 81.6 10.2zm18.1 0v40.7l81.7-10.2z"/></svg>
|
|
Before Width: | Height: | Size: 985 B |
@ -1,2 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
<svg width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#b1b2b5" d="M256 68.02L169.7 240.7l14.8 44.5-17 5.6-15.2-45.6-66.42-53-12.18 60.9 12.62 12.6-12.66 12.6-15.21-15.2-30.53-20.4 25.15 163.4 39.17-65.2 89.66 35.8 30.9 46.4h86.4l30.9-46.4 89.7-35.8 39.1 65.2 25.2-163.4-30.6 20.4-15.2 15.2-12.6-12.6 12.6-12.6-12.2-60.9-66.4 53-15.2 45.6-17-5.6 14.8-44.5zm0 122.58l55.7 92.8 1.9 3.2-17.5 70-20.4 20.3h-39.4l-20.4-20.3-17.5-70zm0 34.8l-38.4 64 14.5 58 11.6 11.7h24.6l11.6-11.7 14.5-58z"/></svg>
|
|
Before Width: | Height: | Size: 661 B |
@ -1,2 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
<svg width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#b1b2b5" d="M256 16c-36.446 0-73.264 13.433-139.97 40h279.94C329.263 29.433 292.445 16 256 16zM95.344 72L64 448c56 28 112 31.5 168 31.938V240H112v-48h288v48H280v239.938C336 479.5 392 476 448 448L416.656 72H95.344zm64.875 88a8 8 0 0 1 7.78 8 8 8 0 0 1-16 0 8 8 0 0 1 8.22-8zm48 0a8 8 0 0 1 7.78 8 8 8 0 0 1-16 0 8 8 0 0 1 8.22-8zm48 0a8 8 0 0 1 7.78 8 8 8 0 0 1-16 0 8 8 0 0 1 8.22-8zm48 0a8 8 0 0 1 7.78 8 8 8 0 0 1-16 0 8 8 0 0 1 8.22-8zm48 0a8 8 0 0 1 7.78 8 8 8 0 0 1-16 0 8 8 0 0 1 8.22-8zM248 240v240c2.667.002 5.333 0 8 0s5.333.002 8 0V240h-16zm-120 48h16v16h-16v-16zm32 0h16v16h-16v-16zm32 0h16v16h-16v-16zm112 0h16v16h-16v-16zm32 0h16v16h-16v-16zm32 0h16v16h-16v-16zm-240 32h16v16h-16v-16zm32 0h16v16h-16v-16zm32 0h16v16h-16v-16zm112 0h16v16h-16v-16zm32 0h16v16h-16v-16zm32 0h16v16h-16v-16zm-240 32h16v16h-16v-16zm32 0h16v16h-16v-16zm32 0h16v16h-16v-16zm112 0h16v16h-16v-16zm32 0h16v16h-16v-16zm32 0h16v16h-16v-16z"/></svg>
|
|
Before Width: | Height: | Size: 1.1 KiB |
@ -1,2 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
<svg width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#b1b2b5" d="M144 37.28c-25.883 0-63.05 25.96-65.22 82.845l65.345 28.47 71.625-28.626C213.124 63.69 169.852 37.28 144 37.28zm224 0c-25.852 0-69.124 26.412-71.75 82.69l71.625 28.624 65.344-28.47c-2.17-56.883-39.337-82.843-65.22-82.843zM76.594 136.626c-10.887 40.29-12.11 90.026-9.844 141.03 46.136 10.675 93.357 7.68 141.406.44 8.464-50.25 12.923-99.296 3.625-139.314l-64.81 25.907-3.095 1.25-3.063-1.343-64.218-27.97zm358.812 0l-64.22 27.97-3.06 1.343-3.095-1.25-64.81-25.907c-9.3 40.02-4.84 89.064 3.624 139.314 48.05 7.242 95.27 10.236 141.406-.438 2.266-51.005 1.043-100.74-9.844-141.03zm-250.25 160.72c-31.678 3.654-63.5 4.865-95.25.75 12.238 12.217 23.424 24.845 35.47 36.217 2.198 2.077 4.425 4.107 6.686 6.094 18.086-12.095 35.813-26.27 53.094-43.062zm141.688 0c17.28 16.792 35.008 30.966 53.094 43.06 2.26-1.986 4.488-4.016 6.687-6.092 12.045-11.373 23.23-24 35.47-36.22-31.752 4.117-63.573 2.906-95.25-.75zm-258.97 1.842c1.515 24.774 3.633 49.29 5.69 73.188 15.054-6.598 29.912-14.187 44.53-23.03-1.24-1.13-2.477-2.264-3.688-3.408-16.52-15.597-30.655-32.307-46.53-46.75zm376.25 0c-15.875 14.443-30.01 31.153-46.53 46.75-1.21 1.144-2.448 2.278-3.688 3.407 14.618 8.844 29.476 16.433 44.53 23.03 2.057-23.897 4.175-48.413 5.69-73.187zm-240.093 1.844c-19.095 19.538-38.774 35.968-58.936 49.845 12.672 9.234 27 16.825 44.78 21.625 4.59-23.255 9.642-47.365 14.157-71.47zm103.94 0c4.514 24.105 9.566 48.215 14.155 71.47 17.78-4.8 32.11-12.39 44.78-21.625-20.16-13.877-39.84-30.307-58.936-49.844zM130.936 360.19c-15.606 9.753-31.47 18.076-47.5 25.28 11.79 18.345 27.05 33.88 44.282 47.97 18.94-13.89 36.69-28.745 51.124-47.532-18.94-6.09-34.415-15.227-47.906-25.72zm250.125 0c-13.49 10.49-28.966 19.628-47.906 25.718 14.435 18.787 32.183 33.642 51.125 47.53 17.234-14.087 32.494-29.623 44.283-47.967-16.03-7.206-31.894-15.53-47.5-25.283zM76.125 403.125c1.866 23.033 3.352 44.754 3.75 64.22 11.39-8.415 22.977-16.29 34.313-24.25-14.114-11.857-27.09-24.923-38.063-39.97zm359.75 0c-10.973 15.047-23.95 28.113-38.063 39.97 11.336 7.96 22.924 15.835 34.313 24.25.398-19.466 1.884-41.187 3.75-64.22zm-252.156 2.063c-12.975 14.874-27.61 27.23-42.75 38.53 11.24 8.348 23.104 16.282 35.218 24.126.75-19.013 3.624-40.192 7.53-62.656zm144.56 0c3.908 22.464 6.783 43.643 7.533 62.656 12.113-7.844 23.977-15.778 35.218-24.125-15.14-11.303-29.775-23.658-42.75-38.533zm-200.967 48.375c-13.318 9.394-26.726 18.344-39.47 27.812 26.524 12.555 53.04 11.06 79.563-.156-13.63-8.813-27.194-17.85-40.094-27.658zm257.375 0c-12.9 9.808-26.464 18.844-40.094 27.656 26.523 11.214 53.04 12.71 79.562.155-12.743-9.468-26.15-18.418-39.47-27.813z"/></svg>
|
|
Before Width: | Height: | Size: 2.8 KiB |
@ -1,2 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
<svg width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#b1b2b5" d="M186.438 20.56l-13.184 26.365c6.8-.26 13.626-.488 20.47-.686l3.84-7.68h116.874l3.77 7.54c6.838.187 13.658.408 20.456.66l-13.102-26.2H186.437zm69.56 42.742c-45.757.056-91.452 1.566-135.38 4.363-3.24 50.58-8.4 100.987-.786 145.824 89.297 12.395 180.102 12.985 272.764-.054 7.055-30.988 5.117-84.68-1.04-145.89-43.974-2.893-89.73-4.3-135.558-4.244zm153.783 5.54c6.42 64.12 9.113 119.825-1.135 155.22l-1.61 5.56-5.726.842c-98.8 14.528-195.613 13.81-290.605.002l-6.285-.914-1.246-6.23c-9.89-49.49-4.085-102.785-.664-154.42-4.89.354-9.765.72-14.602 1.107-8.596 58.568-9.39 116.957-.05 175.292 110.24 12.088 222.275 12.205 336.203-.01 8.502-57.83 8.29-116.25-.017-175.313-4.725-.4-9.485-.776-14.262-1.14zM255.966 92.3c32.526-.025 65.067 2.746 97.574 8.39l7.46 1.295v7.572c0 15.554 1.683 35.105-12.69 50.25-9.912 10.444-25.655 17.337-51.31 20.585v18.164h-82v-18.452c-23.992-3.37-39.352-10.175-49.363-20.185C150.807 145.093 151 125.56 151 109.56v-7.594l7.484-1.278c32.444-5.54 64.955-8.362 97.48-8.386zm.012 17.994c-28.96.022-57.913 2.444-86.858 6.996.265 12.28 1.635 22.296 9.243 29.904 5.914 5.914 16.952 11.416 36.637 14.582v-29.22h82v29.51c21.367-3.115 32.66-8.755 38.254-14.65 7.033-7.41 7.696-17.502 7.73-30.124-29-4.63-58.006-7.02-87.007-6.998zM233 150.56v30h46v-30h-46zm209.674 92.42c-.503 3.625-1.042 7.25-1.61 10.87.214 2.352.42 4.706.63 7.06L471 290.213v-22.24l-28.326-24.995zm-373.485.12L41 267.973v22.24l29.318-29.318c.205-2.327.406-4.655.616-6.982-.618-3.605-1.202-7.21-1.745-10.813zm354.634 20.397c-10.29 1.09-20.564 2.076-30.824 2.967v74.095h16v66h-16v80.615c10.318-.633 20.63-1.313 30.928-2.082 9.445-74.01 6.478-147.698-.104-221.596zm-335.576.03C81.725 338.09 78.58 412.1 88.06 485.1c10.324.79 20.638 1.504 30.94 2.145V406.56h-16v-66h16v-74.024c-10.266-.902-20.517-1.903-30.752-3.01zm286.752 4.4c-10.014.76-20.014 1.424-30 1.992v70.64h30v-72.632zm-238 .085v72.547h30v-70.55c-10.015-.568-20.014-1.237-30-1.997zm190 2.825c-47.65 2.173-94.984 2.19-142 .078v19.314c23.95-5.165 47.8-7.652 71.516-7.59 23.638.06 47.145 2.654 70.484 7.626v-19.43zM68.05 288.62L41 315.67v56.89h23.06c.376-27.987 1.88-55.975 3.99-83.94zm375.948.047c2.12 27.872 3.61 55.83 3.957 83.892H471v-56.89l-27.002-27.003zm-187.52 11.95c-23.68-.063-47.487 2.577-71.478 8.052v31.89h16v18.443c17.033 5.346 31.73 8.493 46 9.426v-2.87h18v2.868c14.27-.932 28.967-4.08 46-9.425V340.56h16v-31.866c-23.42-5.267-46.907-8.016-70.523-8.078zM121 358.558v30h22v-23h18v23h22v-30h-62zm208 0v30h22v-23h18v23h22v-30h-62zM201 377.8v28.76h-16v15.857c48.528 10.865 95.713 10.664 142 .045V406.56h-16V377.8c-16.332 4.747-31.283 7.52-46 8.326v11.433h-18v-11.434c-14.717-.806-29.668-3.58-46-8.326zM41 390.56v14h23.14c-.09-4.667-.143-9.334-.163-14H41zm407.012 0c-.027 4.663-.083 9.33-.18 14H471v-14h-22.988zM137 406.56v19.798c6.137 7.214 11.222 9.77 14.934 9.844 3.734.075 8.697-2.122 15.066-9.79V406.56h-6v7h-18v-7h-6zm208 0v19.798c6.137 7.214 11.222 9.77 14.934 9.844 3.734.075 8.697-2.122 15.066-9.79V406.56h-6v7h-18v-7h-6zm-304 16v35.154c5.596 5.51 8.677 8.25 11.846 9.306 2.454.818 7.713 1.15 15.045 1.317-1.544-15.25-2.586-30.51-3.204-45.778H41zm406.27 0c-.628 15.224-1.674 30.483-3.21 45.78 7.358-.168 12.635-.5 15.094-1.32 3.17-1.056 6.25-3.795 11.846-9.306V422.56h-23.73zM185 440.842v49.498c47.55 1.51 94.877 1.446 142-.074V440.9c-46.316 10.03-93.74 10.185-142-.057zm-48 9.123v38.318c10.01.54 20.01 1.008 30 1.408v-39.678c-4.86 2.786-10.01 4.293-15.43 4.184-5.192-.104-10.036-1.624-14.57-4.232zm208 0v39.654c10.01-.403 20.01-.878 30-1.412v-38.194c-4.86 2.786-10.01 4.293-15.43 4.184-5.192-.104-10.036-1.624-14.57-4.232z"/></svg>
|
|
Before Width: | Height: | Size: 3.7 KiB |
@ -1,2 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
<svg width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#b1b2b5" d="M329.5 29.12l-8.1 11.4L359 67.16l8.1-11.44zm-88 5.04l24.2 45.36 1.8 1.29 14.8-40.36zm57.6 12.63l-16.4 44.8 40.7 28.81 35.3-31.54c-.9-.58-1.9-1.19-2.8-1.84zM59.83 48.56l10.84 45.83 29.63 2.6 2.7-29.63zM470.9 75.41c-5.6 4.71-12.2 8.59-19.5 11.74 5 46.45-14.7 83.45-45.2 109.75-26.5 22.9-60.9 38.4-95 47.9-2.5 4.8-5 9.2-7.4 13.1 41.5 5.4 93.2-21.2 129.2-60 19.8-21.3 34.8-45.9 41.1-69.2 5.2-19.4 4.7-37.42-3.2-53.29zm-351.3 8.71l-3 32.48-32.35-2.9 226.55 271 20-16.7 15.3-12.8zM434 93.09c-4.2 1-8.5 2-12.8 2.7-14.9 2.5-30.1 3.1-43.5.3l-41 36.61c4 7 5 15.7 4.5 24.5-.6 12.6-4.3 26.7-9.3 40.9-3 8.3-6.3 16.6-9.9 24.6 26.9-9.2 52.6-22.3 72.5-39.4 26.2-22.8 42.5-51.6 39.5-90.21zM274 107.4l-51.2 72.2 30.6 36.5 58.2-82.1zM173.8 248.8L34.53 445.2l37.53 26.6L204.3 285.3zm233 79.2L273.3 439.5l19.2 23.1L426 351zm-18.3 77.9l-35.3 29.4 39.7 47.6 35.3-29.4z"/></svg>
|
|
Before Width: | Height: | Size: 1.1 KiB |
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 |
10
public/assets/icons/plus-green-icon.svg
Normal file
After Width: | Height: | Size: 485 KiB |
3
public/assets/icons/profile/boots.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4.15761 1.28332C3.96537 1.15583 3.71087 1.17639 3.54159 1.3331C3.23088 1.62074 2.98798 2.00897 2.80986 2.34288C2.69323 2.27414 2.5689 2.21392 2.43729 2.1694C2.27798 2.11552 2.10225 2.14533 1.96963 2.24875C1.83701 2.35216 1.76526 2.51531 1.77868 2.68295C1.96117 4.96193 2.51766 7.79669 3.14534 10.1876C3.15716 10.2326 3.17507 10.2753 3.19825 10.3146C2.89349 10.543 2.6183 10.8624 2.48626 11.2592C2.45915 11.3406 2.45376 11.4266 2.46923 11.5092L1.01702 12.5343C0.971683 12.5663 0.93199 12.6056 0.899569 12.6506C0.521457 13.1759 0.404537 13.8064 0.579107 14.3656C0.635354 14.5458 0.788273 14.679 0.974476 14.7099C2.62972 14.9848 3.76851 14.6009 4.99802 13.9624C5.09724 13.9109 5.17356 13.8294 5.21926 13.7331C5.28208 13.7365 5.3457 13.7281 5.40668 13.7074L6.36918 13.3813C6.58065 13.3096 6.71897 13.1064 6.70811 12.8834L6.66986 12.0978C6.6651 11.9999 6.6317 11.9057 6.5738 11.8266C6.56495 11.8145 6.55602 11.8025 6.54703 11.7905C6.68693 11.6781 6.75773 11.4969 6.72648 11.3158C6.51037 10.0625 6.7448 8.79384 7.001 7.4073C7.02616 7.27116 7.05153 7.13387 7.0767 6.99535C7.35132 5.48411 7.59458 3.84747 7.06807 2.17953C6.98994 1.93202 6.73543 1.78545 6.48214 1.84209C6.12136 1.92278 5.70715 2.0529 5.33106 2.21214C5.27748 2.23483 5.22893 2.26639 5.1871 2.30496C5.15959 2.27265 5.13071 2.23811 5.09985 2.201C5.09087 2.1902 5.08173 2.17919 5.07242 2.16799C5.00591 2.0879 4.93138 1.99816 4.85218 1.90837C4.6703 1.7022 4.44556 1.47429 4.15761 1.28332ZM12.4584 1.33311C12.2891 1.1764 12.0346 1.15583 11.8424 1.28331C11.5544 1.47427 11.3296 1.70219 11.1478 1.90836C11.0685 1.99816 10.994 2.08791 10.9275 2.16801C10.9182 2.1792 10.9091 2.19021 10.9001 2.201C10.8692 2.2381 10.8404 2.27263 10.8129 2.30493C10.771 2.26634 10.7224 2.23476 10.6688 2.21207C10.2927 2.05285 9.87856 1.9228 9.51784 1.8421C9.26454 1.78544 9.01 1.93201 8.93187 2.17953C8.40536 3.84747 8.64862 5.48411 8.92324 6.99535C8.94841 7.13386 8.97378 7.27114 8.99893 7.40728C9.25513 8.79381 9.48956 10.0625 9.27343 11.3157C9.24218 11.4969 9.31299 11.6781 9.45292 11.7905C9.44392 11.8025 9.435 11.8145 9.42615 11.8266C9.36825 11.9056 9.33484 11.9999 9.33008 12.0978L9.29183 12.8834C9.28097 13.1064 9.41929 13.3096 9.63076 13.3813L10.5933 13.7074C10.6543 13.7281 10.7179 13.7365 10.7807 13.7331C10.8264 13.8294 10.9028 13.9109 11.002 13.9624C12.2315 14.6008 13.3703 14.9848 15.0255 14.7099C15.2117 14.679 15.3647 14.5458 15.4209 14.3656C15.5955 13.8064 15.4785 13.1759 15.1004 12.6506C15.068 12.6056 15.0283 12.5663 14.983 12.5343L13.5307 11.5091C13.5462 11.4266 13.5408 11.3406 13.5137 11.2592C13.3816 10.8624 13.1064 10.543 12.8017 10.3146C12.8248 10.2753 12.8428 10.2326 12.8546 10.1876C13.4823 7.79669 14.0388 4.9619 14.2212 2.68292C14.2347 2.51528 14.1629 2.35213 14.0303 2.24872C13.8977 2.14531 13.7219 2.11549 13.5626 2.16937C13.431 2.21389 13.3067 2.27411 13.1901 2.34286C13.0119 2.00898 12.769 1.62075 12.4584 1.33311Z" stroke="#666666" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
3
public/assets/icons/profile/chestplate.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12.504 1.63801L12.252 1.84501C11.652 2.34001 11.352 2.58801 11 2.58801C10.648 2.58801 10.348 2.34001 9.74799 1.84501L9.49599 1.63801C9.00899 1.23701 8.76599 1.03601 8.47499 1.00401C8.18299 0.973011 7.90499 1.11601 7.34699 1.40401L7.04399 1.55901C6.76699 1.70201 6.62799 1.77401 6.55299 1.91901C6.47799 2.06401 6.49499 2.20301 6.52999 2.48101C6.80599 4.70301 8.49099 6.79701 10.61 7.42601C10.735 7.47216 10.8668 7.49717 11 7.50001C11.1332 7.49717 11.265 7.47216 11.39 7.42601C13.509 6.79701 15.194 4.70301 15.47 2.48101C15.505 2.20301 15.522 2.06401 15.447 1.91901C15.372 1.77401 15.233 1.70201 14.957 1.55901L14.653 1.40301C14.096 1.11601 13.817 0.973011 13.525 1.00401C13.234 1.03601 12.991 1.23701 12.505 1.63801M6.47799 3.00001C4.39199 3.40701 2.99599 4.15001 1.71899 5.23001C1.34599 5.54501 1.15899 5.70301 1.07099 5.95401C0.780994 6.78001 1.41499 9.62101 2.19699 9.94901C2.68399 10.153 3.36499 9.73501 4.72799 8.89901C5.96499 8.14001 7.48299 7.35501 8.99999 6.93201M15.522 3.00001C17.608 3.40701 19.004 4.15001 20.281 5.23001C20.654 5.54501 20.841 5.70301 20.929 5.95401C21.219 6.78001 20.585 9.62101 19.803 9.94901C19.316 10.153 18.635 9.73501 17.271 8.89901C16.036 8.14001 14.518 7.35501 13 6.93201M17 9.00001L16.395 14.442C16.207 16.134 16.113 16.98 15.544 17.49C14.974 18 14.123 18 12.42 18H9.57999C7.87699 18 7.02599 18 6.45599 17.49C5.88599 16.98 5.79299 16.134 5.60499 14.442L4.99999 9.00001M14.385 18H7.61499C7.35999 18 7.23299 18 7.11199 18.014C6.67794 18.064 6.271 18.2506 5.94999 18.547C5.86099 18.628 5.77999 18.724 5.61699 18.915C5.29699 19.291 5.13599 19.479 5.07699 19.622C5.02428 19.7507 4.9991 19.8889 5.00309 20.0279C5.00709 20.1669 5.04016 20.3034 5.10017 20.4288C5.16019 20.5543 5.24582 20.6657 5.35155 20.756C5.45727 20.8462 5.58074 20.9134 5.71399 20.953C5.86399 21 6.11499 21 6.61599 21H15.384C15.884 21 16.135 21 16.286 20.953C16.4193 20.9136 16.5429 20.8467 16.6487 20.7565C16.7545 20.6664 16.8403 20.5551 16.9004 20.4297C16.9605 20.3044 16.9937 20.1678 16.9977 20.0289C17.0018 19.8899 16.9767 19.7516 16.924 19.623C16.864 19.479 16.704 19.291 16.384 18.915C16.22 18.724 16.139 18.628 16.05 18.547C15.7286 18.2512 15.3219 18.0646 14.888 18.014C14.767 18 14.64 18 14.385 18Z" stroke="#666666" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
3
public/assets/icons/profile/helmet.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="20" height="22" viewBox="0 0 20 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8.00001 19H12M10 7H10.009M2.53801 14.094C0.888011 7.61 3.99601 3.564 10 1C16.005 3.563 19.112 7.61 17.462 14.094C17.342 14.564 17.358 15.063 17.534 15.514L18.91 19.038C19.445 20.409 17.466 20.735 16.625 20.912C14.311 21.399 12.067 19.811 11.693 17.42C11.429 15.733 12.383 15.456 13.631 14.817C13.631 14.817 15.187 14.286 15.187 12.691C15.187 11.517 14.257 10.566 13.112 10.566C12.394 10.566 11.762 10.774 11.135 11.452C10.527 12.109 10.223 12.437 10 12.437C9.77701 12.437 9.47301 12.109 8.86601 11.452C8.23801 10.774 7.60601 10.566 6.88801 10.566C5.74201 10.566 4.81301 11.517 4.81301 12.691C4.81301 14.286 6.36901 14.817 6.36901 14.817C7.61701 15.456 8.57101 15.733 8.30701 17.42C7.93301 19.81 5.68901 21.4 3.37501 20.912C2.53401 20.735 0.555011 20.409 1.09101 19.038L2.46601 15.514C2.64201 15.063 2.65701 14.564 2.53801 14.094Z" stroke="#666666" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1010 B |
3
public/assets/icons/profile/legs.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="20" height="22" viewBox="0 0 20 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M16.16 4V3C16.16 2.057 16.16 1.586 15.86 1.293C15.559 1 15.075 1 14.107 1H5.89301C4.92501 1 4.44101 1 4.14001 1.293C3.83901 1.586 3.84001 2.057 3.84001 3V4M16.16 4H3.84001M16.16 4L17.6096 11M3.84001 4L2.39047 11M2.39047 11L1.78774 14.3265M2.39047 11C3.48635 11.3237 4.13173 12.9685 4.99991 13C6.07854 13.0392 7.37081 11.3877 8.76949 11M8.76949 11C9.1675 9.93337 9.99991 9 9.99991 9C9.99991 9 10.8327 9.93306 11.2308 11M8.76949 11C8.55307 11.58 8.31985 12.3176 8.03701 13.213L7.68545 14.3265M11.2308 11C11.4472 11.5799 11.6802 12.3175 11.963 13.213L12.3148 14.3265M11.2308 11C12.6566 11.3875 13.9154 13.041 14.9999 13C15.8598 12.9675 16.5386 11.3237 17.6096 11M17.6096 11L18.2123 14.326M1.78774 14.3265L1.18501 18.653C0.987006 19.741 0.889006 20.285 1.19701 20.643C1.50401 21 2.07201 21 3.20701 21H4.37601C5.10701 21 5.47401 21 5.74601 20.804C6.01801 20.608 6.12501 20.268 6.34001 19.588L7.68545 14.3265M1.78774 14.3265C2.76898 14.5762 3.5689 16.9715 4.37601 17C5.35607 17.0346 6.34671 14.6477 7.68545 14.3265M12.3148 14.3265L13.661 19.588C13.875 20.269 13.983 20.609 14.255 20.804C14.527 21 14.893 21 15.623 21H16.793C17.928 21 18.496 21 18.803 20.642C19.111 20.285 19.012 19.742 18.815 18.652L18.2123 14.326M12.3148 14.3265C13.6343 14.7183 14.621 16.5451 15.623 16.5C16.4184 16.4642 17.2235 14.6375 18.2123 14.326" stroke="#666666" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.8 KiB |
23
public/assets/ui-elements/profile-ui-box-inner.svg
Normal file
After Width: | Height: | Size: 335 KiB |
23
public/assets/ui-elements/profile-ui-box-outer.svg
Normal file
After Width: | Height: | Size: 453 KiB |
18
public/assets/ui-elements/ui-border-2-corners-bottom.svg
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<svg width="190" height="202" viewBox="0 0 190 202" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g filter="url(#filter0_i_598_514)">
|
||||||
|
<path d="M0 3.60002C0 1.61179 1.61177 0 3.6 0H186.4C188.388 0 190 1.61177 190 3.6V193.658C190 195.646 188.388 197.258 186.4 197.258H184.894C183.584 197.258 182.523 198.32 182.523 199.629C182.523 200.938 181.461 202 180.152 202H9.84847C8.53901 202 7.47748 200.938 7.47748 199.629C7.47748 198.32 6.41596 197.258 5.1065 197.258H3.6C1.61178 197.258 0 195.646 0 193.658V3.60002Z" fill="#181818"/>
|
||||||
|
</g>
|
||||||
|
<path d="M0.3 3.60002C0.3 1.77747 1.77746 0.3 3.6 0.3H186.4C188.223 0.3 189.7 1.77746 189.7 3.6V193.658C189.7 195.481 188.223 196.958 186.4 196.958H184.894C183.418 196.958 182.223 198.154 182.223 199.629C182.223 200.773 181.295 201.7 180.152 201.7H9.84847C8.7047 201.7 7.77748 200.773 7.77748 199.629C7.77748 198.154 6.58164 196.958 5.1065 196.958H3.6C1.77746 196.958 0.3 195.481 0.3 193.658V3.60002Z" stroke="#454442" stroke-width="0.6"/>
|
||||||
|
<defs>
|
||||||
|
<filter id="filter0_i_598_514" x="0" y="0" width="190" height="204.4" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feOffset dy="2.4"/>
|
||||||
|
<feGaussianBlur stdDeviation="2.34"/>
|
||||||
|
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.13 0"/>
|
||||||
|
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_598_514"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 598 KiB After Width: | Height: | Size: 598 KiB |
23
public/assets/ui-elements/ui-border-4-corners-small.svg
Normal file
After Width: | Height: | Size: 470 KiB |
Before Width: | Height: | Size: 471 KiB After Width: | Height: | Size: 471 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 |
Before Width: | Height: | Size: 470 KiB After Width: | Height: | Size: 470 KiB |
46
src/App.vue
@ -1,8 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<Notifications />
|
<Notifications />
|
||||||
<GmTools v-if="gameStore.character?.role === 'gm'" />
|
<BackgroundImageLoader />
|
||||||
<GmPanel v-if="gameStore.character?.role === 'gm'" />
|
<GmPanel v-if="gameStore.character?.role === 'gm'" />
|
||||||
|
|
||||||
<component :is="currentScreen" />
|
<component :is="currentScreen" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -10,24 +9,51 @@
|
|||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
import Notifications from '@/components/utilities/Notifications.vue'
|
import Notifications from '@/components/utilities/Notifications.vue'
|
||||||
import GmTools from '@/components/gameMaster/GmTools.vue'
|
import BackgroundImageLoader from '@/components/utilities/BackgroundImageLoader.vue'
|
||||||
import GmPanel from '@/components/gameMaster/GmPanel.vue'
|
import GmPanel from '@/components/gameMaster/GmPanel.vue'
|
||||||
import Login from '@/screens/Login.vue'
|
import Login from '@/components/screens/Login.vue'
|
||||||
import Characters from '@/screens/Characters.vue'
|
import Characters from '@/components/screens/Characters.vue'
|
||||||
import Game from '@/screens/Game.vue'
|
import Game from '@/components/screens/Game.vue'
|
||||||
// import Loading from '@/screens/Loading.vue'
|
import ZoneEditor from '@/components/screens/ZoneEditor.vue'
|
||||||
import ZoneEditor from '@/screens/ZoneEditor.vue'
|
import { computed, watch } from 'vue'
|
||||||
import { computed } from 'vue'
|
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
|
|
||||||
const currentScreen = computed(() => {
|
const currentScreen = computed(() => {
|
||||||
// if (!gameStore.isAssetsLoaded) return Loading
|
|
||||||
if (!gameStore.connection) return Login
|
if (!gameStore.connection) return Login
|
||||||
if (!gameStore.token) return Login
|
if (!gameStore.token) return Login
|
||||||
if (!gameStore.character) return Characters
|
if (!gameStore.character) return Characters
|
||||||
if (zoneEditorStore.active) return ZoneEditor
|
if (zoneEditorStore.active) return ZoneEditor
|
||||||
return Game
|
return Game
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Watch zoneEditorStore.active and empty gameStore.game.loadedAssets
|
||||||
|
watch(
|
||||||
|
() => zoneEditorStore.active,
|
||||||
|
() => {
|
||||||
|
gameStore.game.loadedAssets = []
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// #209: Play sound when a button is pressed
|
||||||
|
addEventListener('click', (event) => {
|
||||||
|
if (!(event.target instanceof HTMLButtonElement)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const audio = new Audio('/assets/music/click-btn.mp3')
|
||||||
|
audio.play()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Watch for "G" key press and toggle the gm panel
|
||||||
|
addEventListener('keydown', (event) => {
|
||||||
|
if (gameStore.character?.role !== 'gm') return // Only allow toggling the gm panel if the character is a gm
|
||||||
|
|
||||||
|
// Check if no input is active
|
||||||
|
if (event.repeat || event.isComposing || event.defaultPrevented) return
|
||||||
|
|
||||||
|
if (event.key === 'G') {
|
||||||
|
gameStore.toggleGmPanel()
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -125,6 +125,12 @@ button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.character {
|
||||||
|
&.active {
|
||||||
|
@apply pr-px border-r-0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.text-pixel {
|
.text-pixel {
|
||||||
@apply text-white font-ui drop-shadow-pixel-black;
|
@apply text-white font-ui drop-shadow-pixel-black;
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,41 @@
|
|||||||
<template>
|
<template>
|
||||||
<Scene name="effects" @preload="preloadScene" @create="createScene" @update="updateScene"> </Scene>
|
<Scene name="effects" @preload="preloadScene" @create="createScene" @update="updateScene" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Scene } from 'phavuer'
|
import { Scene } from 'phavuer'
|
||||||
import { useZoneStore } from '@/stores/zoneStore'
|
import { useZoneStore } from '@/stores/zoneStore'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { onBeforeUnmount, ref, watch } from 'vue'
|
import { onBeforeUnmount, ref, watch } from 'vue'
|
||||||
|
import type { WeatherState } from '@/types'
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const SUNRISE_HOUR = 6
|
||||||
|
const SUNSET_HOUR = 20
|
||||||
|
const DAY_STRENGTH = 100
|
||||||
|
const NIGHT_STRENGTH = 30
|
||||||
|
|
||||||
|
// Stores
|
||||||
|
const gameStore = useGameStore()
|
||||||
const zoneStore = useZoneStore()
|
const zoneStore = useZoneStore()
|
||||||
|
|
||||||
|
// Scene ref
|
||||||
const sceneRef = ref<Phaser.Scene | null>(null)
|
const sceneRef = ref<Phaser.Scene | null>(null)
|
||||||
|
|
||||||
// Effect-related refs
|
// Effect refs
|
||||||
const lightEffect = ref<Phaser.GameObjects.Graphics | null>(null)
|
const lightEffect = ref<Phaser.GameObjects.Graphics | null>(null)
|
||||||
const rainEmitter = ref<Phaser.GameObjects.Particles.ParticleEmitter | null>(null)
|
const rainEmitter = ref<Phaser.GameObjects.Particles.ParticleEmitter | null>(null)
|
||||||
const fogSprite = ref<Phaser.GameObjects.Sprite | null>(null)
|
const fogSprite = ref<Phaser.GameObjects.Sprite | null>(null)
|
||||||
|
|
||||||
|
// State refs
|
||||||
|
const weatherState = ref<WeatherState>({
|
||||||
|
isRainEnabled: false,
|
||||||
|
rainPercentage: 0,
|
||||||
|
isFogEnabled: false,
|
||||||
|
fogDensity: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// Scene lifecycle methods
|
||||||
const preloadScene = async (scene: Phaser.Scene) => {
|
const preloadScene = async (scene: Phaser.Scene) => {
|
||||||
scene.load.image('raindrop', 'assets/raindrop.png')
|
scene.load.image('raindrop', 'assets/raindrop.png')
|
||||||
scene.load.image('fog', 'assets/fog.png')
|
scene.load.image('fog', 'assets/fog.png')
|
||||||
@ -23,15 +43,21 @@ const preloadScene = async (scene: Phaser.Scene) => {
|
|||||||
|
|
||||||
const createScene = async (scene: Phaser.Scene) => {
|
const createScene = async (scene: Phaser.Scene) => {
|
||||||
sceneRef.value = scene
|
sceneRef.value = scene
|
||||||
createLightEffect(scene)
|
setupEffects(scene)
|
||||||
createRainEffect(scene)
|
setupSocketListeners()
|
||||||
createFogEffect(scene)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateScene = () => {
|
const updateScene = () => {
|
||||||
updateEffects()
|
updateEffects()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Effect setup
|
||||||
|
const setupEffects = (scene: Phaser.Scene) => {
|
||||||
|
createLightEffect(scene)
|
||||||
|
createRainEffect(scene)
|
||||||
|
createFogEffect(scene)
|
||||||
|
}
|
||||||
|
|
||||||
const createLightEffect = (scene: Phaser.Scene) => {
|
const createLightEffect = (scene: Phaser.Scene) => {
|
||||||
lightEffect.value = scene.add.graphics()
|
lightEffect.value = scene.add.graphics()
|
||||||
lightEffect.value.setDepth(1000)
|
lightEffect.value.setDepth(1000)
|
||||||
@ -59,14 +85,55 @@ const createFogEffect = (scene: Phaser.Scene) => {
|
|||||||
fogSprite.value.setDepth(950)
|
fogSprite.value.setDepth(950)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lighting calculations
|
||||||
|
const calculateLightStrength = (time: Date): number => {
|
||||||
|
const hour = time.getHours()
|
||||||
|
const minute = time.getMinutes()
|
||||||
|
|
||||||
|
let strength = DAY_STRENGTH
|
||||||
|
|
||||||
|
// Night time (10 PM - 6 AM)
|
||||||
|
if (hour >= SUNSET_HOUR || hour < SUNRISE_HOUR) {
|
||||||
|
strength = NIGHT_STRENGTH
|
||||||
|
}
|
||||||
|
// Full daylight (7 AM - 7 PM)
|
||||||
|
else if (hour > SUNRISE_HOUR && hour < SUNSET_HOUR - 2) {
|
||||||
|
strength = DAY_STRENGTH
|
||||||
|
}
|
||||||
|
// Sunrise transition (6 AM - 7 AM)
|
||||||
|
else if (hour === SUNRISE_HOUR) {
|
||||||
|
strength = NIGHT_STRENGTH + ((DAY_STRENGTH - NIGHT_STRENGTH) * minute) / 60
|
||||||
|
}
|
||||||
|
// Sunset transition (8 PM - 10 PM)
|
||||||
|
else if (hour >= SUNSET_HOUR - 2 && hour < SUNSET_HOUR) {
|
||||||
|
const totalMinutes = (hour - (SUNSET_HOUR - 2)) * 60 + minute
|
||||||
|
const transitionProgress = totalMinutes / 120 // 2 hours = 120 minutes
|
||||||
|
strength = DAY_STRENGTH - (DAY_STRENGTH - NIGHT_STRENGTH) * transitionProgress
|
||||||
|
}
|
||||||
|
|
||||||
|
return strength
|
||||||
|
}
|
||||||
|
|
||||||
|
// Effect updates
|
||||||
const updateEffects = () => {
|
const updateEffects = () => {
|
||||||
const effects = zoneStore.zone?.zoneEffects || []
|
const effects = zoneStore.zone?.zoneEffects || []
|
||||||
|
|
||||||
|
if (effects.length > 0) {
|
||||||
|
updateZoneEffects(effects)
|
||||||
|
} else {
|
||||||
|
// Make sure we're getting the current time
|
||||||
|
const lightStrength = calculateLightStrength(gameStore.world.date)
|
||||||
|
updateLightEffect(lightStrength)
|
||||||
|
updateWeatherEffects()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateZoneEffects = (effects: any[]) => {
|
||||||
|
// Always update light based on time when zone effects are present
|
||||||
|
updateLightEffect(calculateLightStrength(gameStore.world.date))
|
||||||
|
|
||||||
effects.forEach((effect) => {
|
effects.forEach((effect) => {
|
||||||
switch (effect.effect) {
|
switch (effect.effect) {
|
||||||
case 'light':
|
|
||||||
updateLightEffect(effect.strength)
|
|
||||||
break
|
|
||||||
case 'rain':
|
case 'rain':
|
||||||
updateRainEffect(effect.strength)
|
updateRainEffect(effect.strength)
|
||||||
break
|
break
|
||||||
@ -77,6 +144,11 @@ const updateEffects = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateWeatherEffects = () => {
|
||||||
|
updateRainEffect(weatherState.value.isRainEnabled ? weatherState.value.rainPercentage : 0)
|
||||||
|
updateFogEffect(weatherState.value.isFogEnabled ? weatherState.value.fogDensity * 100 : 0)
|
||||||
|
}
|
||||||
|
|
||||||
const updateLightEffect = (strength: number) => {
|
const updateLightEffect = (strength: number) => {
|
||||||
if (!lightEffect.value) return
|
if (!lightEffect.value) return
|
||||||
const darkness = 1 - strength / 100
|
const darkness = 1 - strength / 100
|
||||||
@ -100,11 +172,34 @@ const updateFogEffect = (strength: number) => {
|
|||||||
fogSprite.value.setAlpha(strength / 100)
|
fogSprite.value.setAlpha(strength / 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Socket handlers
|
||||||
|
const setupSocketListeners = () => {
|
||||||
|
// Initial weather state
|
||||||
|
gameStore.connection?.emit('weather', (response: WeatherState) => {
|
||||||
|
if (zoneStore.zone?.zoneEffects) return
|
||||||
|
weatherState.value = response
|
||||||
|
updateEffects()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Weather updates
|
||||||
|
gameStore.connection?.on('weather', (data: WeatherState) => {
|
||||||
|
weatherState.value = data
|
||||||
|
updateEffects()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Time updates
|
||||||
|
gameStore.connection?.on('date', () => {
|
||||||
|
if (zoneStore.zone?.zoneEffects) return
|
||||||
|
updateEffects()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watchers
|
||||||
watch(() => zoneStore.zone?.zoneEffects, updateEffects, { deep: true })
|
watch(() => zoneStore.zone?.zoneEffects, updateEffects, { deep: true })
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (sceneRef.value) sceneRef.value.scene.remove('effects')
|
if (sceneRef.value) sceneRef.value.scene.remove('effects')
|
||||||
|
gameStore.connection?.off('weather')
|
||||||
})
|
})
|
||||||
|
|
||||||
// @TODO : Fix resize issue
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -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">Users</button>
|
||||||
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Chats</button>
|
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Chats</button>
|
||||||
<button @mousedown.stop class="btn-cyan active py-1.5 px-4 min-w-24">Asset manager</button>
|
<button @mousedown.stop class="btn-cyan active py-1.5 px-4 min-w-24">Asset manager</button>
|
||||||
|
<button class="btn-cyan py-1.5 px-4 min-w-24" type="button" @click="() => zoneEditorStore.toggleActive()">Zone manager</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #modalBody>
|
<template #modalBody>
|
||||||
@ -21,8 +22,10 @@ import { ref } from 'vue'
|
|||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import AssetManager from '@/components/gameMaster/assetManager/AssetManager.vue'
|
import AssetManager from '@/components/gameMaster/assetManager/AssetManager.vue'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
|
|
||||||
let toggle = ref('asset-manager')
|
let toggle = ref('asset-manager')
|
||||||
</script>
|
</script>
|
||||||
|
@ -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>
|
|
@ -118,7 +118,6 @@ function saveObject() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(objectTags.value)
|
|
||||||
gameStore.connection?.emit(
|
gameStore.connection?.emit(
|
||||||
'gm:object:update',
|
'gm:object:update',
|
||||||
{
|
{
|
||||||
|
@ -1,146 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Controls :layer="tiles" :depth="0" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import config from '@/config'
|
|
||||||
import { useScene } from 'phavuer'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
|
||||||
import { onBeforeMount, onBeforeUnmount } from 'vue'
|
|
||||||
import { createTileArray, getTile, placeTile, setLayerTiles } from '@/composables/zoneComposable'
|
|
||||||
import Controls from '@/components/utilities/Controls.vue'
|
|
||||||
|
|
||||||
const emit = defineEmits(['tilemap:create'])
|
|
||||||
|
|
||||||
const scene = useScene()
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
|
||||||
|
|
||||||
const zoneTilemap = createTilemap()
|
|
||||||
const tiles = createTileLayer()
|
|
||||||
|
|
||||||
function createTilemap() {
|
|
||||||
const zoneData = new Phaser.Tilemaps.MapData({
|
|
||||||
width: zoneEditorStore.zone?.width,
|
|
||||||
height: zoneEditorStore.zone?.height,
|
|
||||||
tileWidth: config.tile_size.x,
|
|
||||||
tileHeight: config.tile_size.y,
|
|
||||||
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
|
|
||||||
format: Phaser.Tilemaps.Formats.ARRAY_2D
|
|
||||||
})
|
|
||||||
const tilemap = new Phaser.Tilemaps.Tilemap(scene, zoneData)
|
|
||||||
emit('tilemap:create', tilemap)
|
|
||||||
return tilemap
|
|
||||||
}
|
|
||||||
|
|
||||||
function createTileLayer() {
|
|
||||||
const tilesetImages = gameStore.assets.filter((asset) => asset.group === 'tiles').map((asset, index) => zoneTilemap.addTilesetImage(asset.key, asset.key, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y }))
|
|
||||||
tilesetImages.push(zoneTilemap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 1, 2, 0, { x: 0, y: -config.tile_size.y }))
|
|
||||||
|
|
||||||
const layer = zoneTilemap.createBlankLayer('tiles', tilesetImages as any, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
|
|
||||||
|
|
||||||
layer.setDepth(0)
|
|
||||||
layer.setCullPadding(2, 2)
|
|
||||||
|
|
||||||
return layer
|
|
||||||
}
|
|
||||||
|
|
||||||
function pencil(pointer: Phaser.Input.Pointer) {
|
|
||||||
// Check if zone is set
|
|
||||||
if (!zoneEditorStore.zone) return
|
|
||||||
|
|
||||||
// Check if tool is pencil
|
|
||||||
if (zoneEditorStore.tool !== 'pencil') return
|
|
||||||
|
|
||||||
// Check if draw mode is tile
|
|
||||||
if (zoneEditorStore.drawMode !== 'tile') return
|
|
||||||
|
|
||||||
// Check if there is a selected tile
|
|
||||||
if (!zoneEditorStore.selectedTile) return
|
|
||||||
|
|
||||||
// Check if left mouse button is pressed
|
|
||||||
if (!pointer.isDown) return
|
|
||||||
|
|
||||||
// Check if shift is not pressed, this means we are moving the camera
|
|
||||||
if (pointer.event.shiftKey) return
|
|
||||||
|
|
||||||
// Check if there is a tile
|
|
||||||
const tile = getTile(tiles, pointer.worldX, pointer.worldY)
|
|
||||||
if (!tile) return
|
|
||||||
|
|
||||||
// Place tile
|
|
||||||
placeTile(zoneTilemap, tiles, tile.x, tile.y, zoneEditorStore.selectedTile.id)
|
|
||||||
|
|
||||||
// Adjust zoneEditorStore.zone.tiles
|
|
||||||
zoneEditorStore.zone.tiles[tile.y][tile.x] = zoneEditorStore.selectedTile.id
|
|
||||||
}
|
|
||||||
|
|
||||||
function eraser(pointer: Phaser.Input.Pointer) {
|
|
||||||
// Check if zone is set
|
|
||||||
if (!zoneEditorStore.zone) return
|
|
||||||
|
|
||||||
// Check if tool is pencil
|
|
||||||
if (zoneEditorStore.tool !== 'eraser') return
|
|
||||||
|
|
||||||
// Check if draw mode is tile
|
|
||||||
if (zoneEditorStore.eraserMode !== 'tile') return
|
|
||||||
|
|
||||||
// Check if left mouse button is pressed
|
|
||||||
if (!pointer.isDown) return
|
|
||||||
|
|
||||||
// Check if shift is not pressed, this means we are moving the camera
|
|
||||||
if (pointer.event.shiftKey) return
|
|
||||||
|
|
||||||
// Check if there is a tile
|
|
||||||
const tile = getTile(tiles, pointer.worldX, pointer.worldY)
|
|
||||||
if (!tile) return
|
|
||||||
|
|
||||||
// Place tile
|
|
||||||
placeTile(zoneTilemap, tiles, tile.x, tile.y, 'blank_tile')
|
|
||||||
|
|
||||||
// Adjust zoneEditorStore.zone.tiles
|
|
||||||
zoneEditorStore.zone.tiles[tile.y][tile.x] = 'blank_tile'
|
|
||||||
}
|
|
||||||
|
|
||||||
function paint(pointer: Phaser.Input.Pointer) {
|
|
||||||
// Check if zone is set
|
|
||||||
if (!zoneEditorStore.zone) return
|
|
||||||
|
|
||||||
// Check if tool is pencil
|
|
||||||
if (zoneEditorStore.tool !== 'paint') return
|
|
||||||
|
|
||||||
// Check if there is a selected tile
|
|
||||||
if (!zoneEditorStore.selectedTile) return
|
|
||||||
|
|
||||||
// Check if left mouse button is pressed
|
|
||||||
if (!pointer.isDown) return
|
|
||||||
|
|
||||||
// Set new tileArray with selected tile
|
|
||||||
setLayerTiles(zoneTilemap, tiles, createTileArray(zoneTilemap.width, zoneTilemap.height, zoneEditorStore.selectedTile.id))
|
|
||||||
|
|
||||||
// Adjust zoneEditorStore.zone.tiles
|
|
||||||
zoneEditorStore.zone.tiles = createTileArray(zoneTilemap.width, zoneTilemap.height, zoneEditorStore.selectedTile.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
onBeforeMount(() => {
|
|
||||||
if (!zoneEditorStore.zone?.tiles) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setLayerTiles(zoneTilemap, tiles, zoneEditorStore.zone.tiles)
|
|
||||||
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_DOWN, paint)
|
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
|
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
|
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_DOWN, paint)
|
|
||||||
|
|
||||||
zoneTilemap.destroyLayer('tiles')
|
|
||||||
zoneTilemap.removeAllLayers()
|
|
||||||
zoneTilemap.destroy()
|
|
||||||
})
|
|
||||||
</script>
|
|
@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<Tiles @tilemap:create="tileMap = $event" />
|
<ZoneTiles @tileMap:create="tileMap = $event" />
|
||||||
<Objects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
<ZoneObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
||||||
<EventTiles v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
<ZoneEventTiles v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
||||||
|
|
||||||
<Toolbar @save="save" />
|
<Toolbar @save="save" @clear="clear" />
|
||||||
|
|
||||||
<ZoneList />
|
<ZoneList />
|
||||||
<TileList />
|
<TileList />
|
||||||
@ -18,7 +18,6 @@ import { onUnmounted, ref } from 'vue'
|
|||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
import { type Zone } from '@/types'
|
import { type Zone } from '@/types'
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import Toolbar from '@/components/gameMaster/zoneEditor/partials/Toolbar.vue'
|
import Toolbar from '@/components/gameMaster/zoneEditor/partials/Toolbar.vue'
|
||||||
import TileList from '@/components/gameMaster/zoneEditor/partials/TileList.vue'
|
import TileList from '@/components/gameMaster/zoneEditor/partials/TileList.vue'
|
||||||
@ -26,15 +25,25 @@ import ObjectList from '@/components/gameMaster/zoneEditor/partials/ObjectList.v
|
|||||||
import ZoneSettings from '@/components/gameMaster/zoneEditor/partials/ZoneSettings.vue'
|
import ZoneSettings from '@/components/gameMaster/zoneEditor/partials/ZoneSettings.vue'
|
||||||
import ZoneList from '@/components/gameMaster/zoneEditor/partials/ZoneList.vue'
|
import ZoneList from '@/components/gameMaster/zoneEditor/partials/ZoneList.vue'
|
||||||
import TeleportModal from '@/components/gameMaster/zoneEditor/partials/TeleportModal.vue'
|
import TeleportModal from '@/components/gameMaster/zoneEditor/partials/TeleportModal.vue'
|
||||||
import Tiles from '@/components/gameMaster/zoneEditor/Tiles.vue'
|
import ZoneTiles from '@/components/gameMaster/zoneEditor/zonePartials/ZoneTiles.vue'
|
||||||
import Objects from '@/components/gameMaster/zoneEditor/Objects.vue'
|
import ZoneObjects from '@/components/gameMaster/zoneEditor/zonePartials/ZoneObjects.vue'
|
||||||
import EventTiles from '@/components/gameMaster/zoneEditor/EventTiles.vue'
|
import ZoneEventTiles from '@/components/gameMaster/zoneEditor/zonePartials/ZoneEventTiles.vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
|
console.log(zoneEditorStore.zone)
|
||||||
|
|
||||||
const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null)
|
const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null)
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
if (!zoneEditorStore.zone) return
|
||||||
|
|
||||||
|
// Clear objects, event tiles and tiles
|
||||||
|
zoneEditorStore.zone.zoneObjects = []
|
||||||
|
zoneEditorStore.zone.zoneEventTiles = []
|
||||||
|
zoneEditorStore.triggerClearTiles()
|
||||||
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
if (!zoneEditorStore.zone) return
|
if (!zoneEditorStore.zone) return
|
||||||
|
|
||||||
@ -55,6 +64,7 @@ function save() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:zone_editor:zone:update', data, (response: Zone) => {
|
gameStore.connection?.emit('gm:zone_editor:zone:update', data, (response: Zone) => {
|
||||||
|
console.log(response.updatedAt)
|
||||||
zoneEditorStore.setZone(response)
|
zoneEditorStore.setZone(response)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -42,23 +42,18 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/config'
|
import config from '@/config'
|
||||||
import { ref, onMounted, computed, watch } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import type { Object } from '@/types'
|
import type { Object, ZoneObject } from '@/types'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const isModalOpen = ref(false)
|
const isModalOpen = ref(false)
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
// const objectDepth = ref(0)
|
|
||||||
const selectedTags = ref<string[]>([])
|
const selectedTags = ref<string[]>([])
|
||||||
|
|
||||||
// watch(objectDepth, (depth) => {
|
|
||||||
// zoneEditorStore.setObjectDepth(depth)
|
|
||||||
// })
|
|
||||||
|
|
||||||
const uniqueTags = computed(() => {
|
const uniqueTags = computed(() => {
|
||||||
const allTags = zoneEditorStore.objectList.flatMap((obj) => obj.tags || [])
|
const allTags = zoneEditorStore.objectList.flatMap((obj) => obj.tags || [])
|
||||||
return Array.from(new Set(allTags))
|
return Array.from(new Set(allTags))
|
||||||
@ -81,8 +76,6 @@ const toggleTag = (tag: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
zoneEditorStore.setObjectDepth(0)
|
|
||||||
|
|
||||||
isModalOpen.value = true
|
isModalOpen.value = true
|
||||||
gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => {
|
gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => {
|
||||||
zoneEditorStore.setObjectList(response)
|
zoneEditorStore.setObjectList(response)
|
||||||
|
@ -52,7 +52,7 @@
|
|||||||
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
|
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
|
||||||
:src="`${config.server_endpoint}/assets/tiles/${selectedGroup.parent.id}.png`"
|
:src="`${config.server_endpoint}/assets/tiles/${selectedGroup.parent.id}.png`"
|
||||||
:alt="selectedGroup.parent.name"
|
:alt="selectedGroup.parent.name"
|
||||||
@click="selectTile(selectedGroup.parent)"
|
@click="selectTile(selectedGroup.parent.id)"
|
||||||
:class="{
|
:class="{
|
||||||
'border-cyan shadow-lg scale-105': isActiveTile(selectedGroup.parent),
|
'border-cyan shadow-lg scale-105': isActiveTile(selectedGroup.parent),
|
||||||
'border-transparent hover:border-gray-300': !isActiveTile(selectedGroup.parent)
|
'border-transparent hover:border-gray-300': !isActiveTile(selectedGroup.parent)
|
||||||
@ -65,7 +65,7 @@
|
|||||||
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
|
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
|
||||||
:src="`${config.server_endpoint}/assets/tiles/${childTile.id}.png`"
|
:src="`${config.server_endpoint}/assets/tiles/${childTile.id}.png`"
|
||||||
:alt="childTile.name"
|
:alt="childTile.name"
|
||||||
@click="selectTile(childTile)"
|
@click="selectTile(childTile.id)"
|
||||||
:class="{
|
:class="{
|
||||||
'border-cyan shadow-lg scale-105': isActiveTile(childTile),
|
'border-cyan shadow-lg scale-105': isActiveTile(childTile),
|
||||||
'border-transparent hover:border-gray-300': !isActiveTile(childTile)
|
'border-transparent hover:border-gray-300': !isActiveTile(childTile)
|
||||||
@ -218,12 +218,12 @@ function closeGroup() {
|
|||||||
selectedGroup.value = null
|
selectedGroup.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectTile(tile: Tile) {
|
function selectTile(tile: string) {
|
||||||
zoneEditorStore.setSelectedTile(tile)
|
zoneEditorStore.setSelectedTile(tile)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isActiveTile(tile: Tile): boolean {
|
function isActiveTile(tile: Tile): boolean {
|
||||||
return zoneEditorStore.selectedTile?.id === tile.id
|
return zoneEditorStore.selectedTile === tile.id
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
@ -8,7 +8,7 @@ import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
|||||||
import { Image, useScene } from 'phavuer'
|
import { Image, useScene } from 'phavuer'
|
||||||
import { getTile, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
import { getTile, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||||
import { uuidv4 } from '@/utilities'
|
import { uuidv4 } from '@/utilities'
|
||||||
import { onBeforeMount, onBeforeUnmount } from 'vue'
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
|
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
@ -102,14 +102,14 @@ function eraser(pointer: Phaser.Input.Pointer) {
|
|||||||
zoneEditorStore.zone.zoneEventTiles = zoneEditorStore.zone.zoneEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id)
|
zoneEditorStore.zone.zoneEventTiles = zoneEditorStore.zone.zoneEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onMounted(() => {
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_DOWN, pencil)
|
scene.input.on(Phaser.Input.Events.POINTER_DOWN, pencil)
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
|
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_DOWN, eraser)
|
scene.input.on(Phaser.Input.Events.POINTER_DOWN, eraser)
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
|
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onUnmounted(() => {
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_DOWN, pencil)
|
scene.input.off(Phaser.Input.Events.POINTER_DOWN, pencil)
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
|
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_DOWN, eraser)
|
scene.input.off(Phaser.Input.Events.POINTER_DOWN, eraser)
|
@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<Image v-if="gameStore.getLoadedAsset(props.zoneObject.object.id)" v-bind="imageProps" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { Image, useScene } from 'phavuer'
|
||||||
|
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||||
|
import { loadTexture } from '@/composables/gameComposable'
|
||||||
|
import type { AssetDataT, ZoneObject } from '@/types'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
tilemap: Phaser.Tilemaps.Tilemap
|
||||||
|
zoneObject: ZoneObject
|
||||||
|
selectedZoneObject: ZoneObject | null
|
||||||
|
movingZoneObject: ZoneObject | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const scene = useScene()
|
||||||
|
|
||||||
|
const imageProps = computed(() => ({
|
||||||
|
alpha: props.movingZoneObject?.id === props.zoneObject.id ? 0.5 : 1,
|
||||||
|
tint: props.selectedZoneObject?.id === props.zoneObject.id ? 0x00ff00 : 0xffffff,
|
||||||
|
depth: calculateIsometricDepth(props.zoneObject.positionX, props.zoneObject.positionY, props.zoneObject.object.frameWidth, props.zoneObject.object.frameHeight),
|
||||||
|
x: tileToWorldX(props.tilemap, props.zoneObject.positionX, props.zoneObject.positionY),
|
||||||
|
y: tileToWorldY(props.tilemap, props.zoneObject.positionX, props.zoneObject.positionY),
|
||||||
|
flipX: props.zoneObject.isRotated,
|
||||||
|
texture: props.zoneObject.object.id,
|
||||||
|
originY: Number(props.zoneObject.object.originX),
|
||||||
|
originX: Number(props.zoneObject.object.originY)
|
||||||
|
}))
|
||||||
|
|
||||||
|
loadTexture(scene, {
|
||||||
|
key: props.zoneObject.object.id,
|
||||||
|
data: '/assets/objects/' + props.zoneObject.object.id + '.png',
|
||||||
|
group: 'objects',
|
||||||
|
updatedAt: props.zoneObject.object.updatedAt,
|
||||||
|
frameWidth: props.zoneObject.object.frameWidth,
|
||||||
|
frameHeight: props.zoneObject.object.frameHeight
|
||||||
|
} as AssetDataT).catch((error) => {
|
||||||
|
console.error('Error loading texture:', error)
|
||||||
|
})
|
||||||
|
</script>
|
@ -1,40 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<SelectedZoneObject v-if="selectedZoneObject" :zoneObject="selectedZoneObject" @move="moveZoneObject" @rotate="rotateZoneObject" @delete="deleteZoneObject" />
|
<SelectedZoneObject v-if="selectedZoneObject" :zoneObject="selectedZoneObject" :movingZoneObject="movingZoneObject" @move="moveZoneObject" @rotate="rotateZoneObject" @delete="deleteZoneObject" />
|
||||||
<Image v-for="object in zoneEditorStore.zone?.zoneObjects" v-bind="getImageProps(object)" @pointerup="() => (selectedZoneObject = object)" />
|
<ZoneObject v-for="zoneObject in zoneEditorStore.zone?.zoneObjects" :tilemap="tilemap" :zoneObject :selectedZoneObject :movingZoneObject @pointerup="clickZoneObject(zoneObject)" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { uuidv4 } from '@/utilities'
|
import { uuidv4 } from '@/utilities'
|
||||||
import { calculateIsometricDepth, getTile, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
import { getTile } from '@/composables/zoneComposable'
|
||||||
import { Image, useScene } from 'phavuer'
|
import { useScene } from 'phavuer'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
import type { ZoneObject } from '@/types'
|
|
||||||
import SelectedZoneObject from '@/components/gameMaster/zoneEditor/partials/SelectedZoneObject.vue'
|
import SelectedZoneObject from '@/components/gameMaster/zoneEditor/partials/SelectedZoneObject.vue'
|
||||||
import { onBeforeMount, onBeforeUnmount, ref, watch } from 'vue'
|
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
|
import ZoneObject from '@/components/gameMaster/zoneEditor/zonePartials/ZoneObject.vue'
|
||||||
|
import type { ZoneObject as ZoneObjectT } from '@/types'
|
||||||
|
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
const selectedZoneObject = ref<ZoneObject | null>(null)
|
const selectedZoneObject = ref<ZoneObjectT | null>(null)
|
||||||
const movingZoneObject = ref<ZoneObject | null>(null)
|
const movingZoneObject = ref<ZoneObjectT | null>(null)
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
tilemap: Phaser.Tilemaps.Tilemap
|
tilemap: Phaser.Tilemaps.Tilemap
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
function getImageProps(zoneObject: ZoneObject) {
|
|
||||||
return {
|
|
||||||
alpha: zoneObject.id === movingZoneObject.value?.id ? 0.5 : 1,
|
|
||||||
depth: calculateIsometricDepth(zoneObject.positionX, zoneObject.positionY, zoneObject.object.frameWidth, zoneObject.object.frameHeight),
|
|
||||||
tint: selectedZoneObject.value?.id === zoneObject.id ? 0x00ff00 : 0xffffff,
|
|
||||||
x: tileToWorldX(props.tilemap, zoneObject.positionX, zoneObject.positionY),
|
|
||||||
y: tileToWorldY(props.tilemap, zoneObject.positionX, zoneObject.positionY),
|
|
||||||
flipX: zoneObject.isRotated,
|
|
||||||
texture: zoneObject.object.id,
|
|
||||||
originY: Number(zoneObject.object.originX),
|
|
||||||
originX: Number(zoneObject.object.originY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function pencil(pointer: Phaser.Input.Pointer) {
|
function pencil(pointer: Phaser.Input.Pointer) {
|
||||||
// Check if zone is set
|
// Check if zone is set
|
||||||
if (!zoneEditorStore.zone) return
|
if (!zoneEditorStore.zone) return
|
||||||
@ -54,6 +41,9 @@ function pencil(pointer: Phaser.Input.Pointer) {
|
|||||||
// Check if shift is not pressed, this means we are moving the camera
|
// Check if shift is not pressed, this means we are moving the camera
|
||||||
if (pointer.event.shiftKey) return
|
if (pointer.event.shiftKey) return
|
||||||
|
|
||||||
|
// Check if alt is pressed, this means we are selecting the object
|
||||||
|
if (pointer.event.altKey) return
|
||||||
|
|
||||||
// Check if there is a tile
|
// Check if there is a tile
|
||||||
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
|
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
|
||||||
if (!tile) return
|
if (!tile) return
|
||||||
@ -66,7 +56,7 @@ function pencil(pointer: Phaser.Input.Pointer) {
|
|||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
zoneId: zoneEditorStore.zone.id,
|
zoneId: zoneEditorStore.zone.id,
|
||||||
zone: zoneEditorStore.zone,
|
zone: zoneEditorStore.zone,
|
||||||
objectId: zoneEditorStore.selectedObject.id,
|
objectId: zoneEditorStore.selectedObject,
|
||||||
object: zoneEditorStore.selectedObject,
|
object: zoneEditorStore.selectedObject,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
isRotated: false,
|
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
|
// Check if shift is not pressed, this means we are moving the camera
|
||||||
if (pointer.event.shiftKey) return
|
if (pointer.event.shiftKey) return
|
||||||
|
|
||||||
|
// Check if alt is pressed, this means we are selecting the object
|
||||||
|
if (pointer.event.altKey) return
|
||||||
|
|
||||||
// Check if there is a tile
|
// Check if there is a tile
|
||||||
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
|
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
|
||||||
if (!tile) return
|
if (!tile) return
|
||||||
@ -106,6 +99,37 @@ function eraser(pointer: Phaser.Input.Pointer) {
|
|||||||
zoneEditorStore.zone.zoneObjects = zoneEditorStore.zone.zoneObjects.filter((object) => object.id !== existingObject.id)
|
zoneEditorStore.zone.zoneObjects = zoneEditorStore.zone.zoneObjects.filter((object) => object.id !== existingObject.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function objectPicker(pointer: Phaser.Input.Pointer) {
|
||||||
|
// Check if zone is set
|
||||||
|
if (!zoneEditorStore.zone) return
|
||||||
|
|
||||||
|
// Check if tool is pencil
|
||||||
|
if (zoneEditorStore.tool !== 'pencil') return
|
||||||
|
|
||||||
|
// Check if draw mode is object
|
||||||
|
if (zoneEditorStore.drawMode !== 'object') return
|
||||||
|
|
||||||
|
// Check if left mouse button is pressed
|
||||||
|
if (!pointer.isDown) return
|
||||||
|
|
||||||
|
// Check if shift is not pressed, this means we are moving the camera
|
||||||
|
if (pointer.event.shiftKey) return
|
||||||
|
|
||||||
|
// If alt is not pressed, return
|
||||||
|
if (!pointer.event.altKey) return
|
||||||
|
|
||||||
|
// Check if there is a tile
|
||||||
|
const tile = getTile(props.tilemap, pointer.worldX, pointer.worldY)
|
||||||
|
if (!tile) return
|
||||||
|
|
||||||
|
// Check if object already exists on position
|
||||||
|
const existingObject = zoneEditorStore.zone.zoneObjects.find((object) => object.positionX === tile.x && object.positionY === tile.y)
|
||||||
|
if (!existingObject) return
|
||||||
|
|
||||||
|
// Select the object
|
||||||
|
zoneEditorStore.setSelectedObject(existingObject)
|
||||||
|
}
|
||||||
|
|
||||||
function moveZoneObject(id: string) {
|
function moveZoneObject(id: string) {
|
||||||
// Check if zone is set
|
// Check if zone is set
|
||||||
if (!zoneEditorStore.zone) return
|
if (!zoneEditorStore.zone) return
|
||||||
@ -155,18 +179,29 @@ function deleteZoneObject(id: string) {
|
|||||||
selectedZoneObject.value = null
|
selectedZoneObject.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeMount(() => {
|
function clickZoneObject(zoneObject: ZoneObjectT) {
|
||||||
|
selectedZoneObject.value = zoneObject
|
||||||
|
|
||||||
|
// If alt is pressed, select the object
|
||||||
|
if (scene.input.activePointer.event.altKey) {
|
||||||
|
zoneEditorStore.setSelectedObject(zoneObject.object)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_DOWN, pencil)
|
scene.input.on(Phaser.Input.Events.POINTER_DOWN, pencil)
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
|
scene.input.on(Phaser.Input.Events.POINTER_MOVE, pencil)
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_DOWN, eraser)
|
scene.input.on(Phaser.Input.Events.POINTER_DOWN, eraser)
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
|
scene.input.on(Phaser.Input.Events.POINTER_MOVE, eraser)
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_DOWN, objectPicker)
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onUnmounted(() => {
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_DOWN, pencil)
|
scene.input.off(Phaser.Input.Events.POINTER_DOWN, pencil)
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
|
scene.input.off(Phaser.Input.Events.POINTER_MOVE, pencil)
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_DOWN, eraser)
|
scene.input.off(Phaser.Input.Events.POINTER_DOWN, eraser)
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
|
scene.input.off(Phaser.Input.Events.POINTER_MOVE, eraser)
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_DOWN, objectPicker)
|
||||||
})
|
})
|
||||||
|
|
||||||
// watch zoneEditorStore.objectList and update originX and originY of objects in zoneObjects
|
// watch zoneEditorStore.objectList and update originX and originY of objects in zoneObjects
|
227
src/components/gameMaster/zoneEditor/zonePartials/ZoneTiles.vue
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
<template>
|
||||||
|
<Controls :layer="tileLayer" :depth="0" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import config from '@/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>
|
243
src/components/gui/CharacterProfile.vue
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative" v-if="gameStore.uiSettings.isCharacterProfileOpen" :style="modalStyle">
|
||||||
|
<img src="/assets/ui-elements/profile-ui-box-outer.svg" class="absolute w-full h-full" />
|
||||||
|
<img src="/assets/ui-elements/profile-ui-box-inner.svg" class="absolute left-2 bottom-2 w-[calc(100%_-_16px)] h-[calc(100%_-_40px)]" />
|
||||||
|
<div @mousedown="startDrag" class="cursor-move px-5 py-2.5 flex justify-between items-center relative">
|
||||||
|
<span class="text-xs text-white font-thin">Character Profile [Alt+C]</span>
|
||||||
|
<button @click="gameStore.uiSettings.isCharacterProfileOpen = false" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
|
||||||
|
<img alt="close" draggable="false" src="/assets/icons/close-button-white.svg" class="w-full h-full" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="py-4 px-6 flex flex-col gap-7 relative z-10">
|
||||||
|
<div class="flex flex-col gap-2.5">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm m-0 font-bold text-white tracking-wide">{{ gameStore.character?.name }}</p>
|
||||||
|
<span class="text-xs">{{ gameStore.character?.experience }} / 18.600XP</span>
|
||||||
|
</div>
|
||||||
|
<a class="hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-small.svg')] bg-no-repeat block w-8 h-8 relative mx-[3px]">
|
||||||
|
<img class="hover:drop-shadow-default w-3.5 h-3.5 m-[9px] object-contain" draggable="false" src="/assets/icons/plus-green-icon.svg" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div class="flex flex-col gap-0.5">
|
||||||
|
<div class="w-9 h-9 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600">
|
||||||
|
<span class="absolute w-full top-1/2 -translate-y-1/2 text-[6px] text-center">CROWN</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-9 h-9 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600">
|
||||||
|
<span class="absolute w-full top-1/2 -translate-y-1/2 text-[6px] text-center">R-HAND</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-0.5 items-end">
|
||||||
|
<div class="w-9 h-9 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600">
|
||||||
|
<span class="absolute w-full top-1/2 -translate-y-1/2 text-[6px] text-center">L-HAND</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-6 h-6 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600">
|
||||||
|
<span class="absolute w-full top-1/2 -translate-y-1/2 text-[6px] text-center">RING</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<img src="/assets/placeholders/inventory_player.png" class="w-8 h-auto" />
|
||||||
|
<div class="flex flex-col items-end gap-0.5">
|
||||||
|
<div class="w-9 h-9 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600">
|
||||||
|
<img class="absolute w-6 h-6 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/profile/helmet.svg" />
|
||||||
|
</div>
|
||||||
|
<div class="w-9 h-9 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600">
|
||||||
|
<img class="absolute w-6 h-6 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/profile/chestplate.svg" />
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-0.5 items-end">
|
||||||
|
<div class="w-6 h-6 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600">
|
||||||
|
<img class="absolute w-4 h-4 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/profile/boots.svg" />
|
||||||
|
</div>
|
||||||
|
<div class="w-9 h-9 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600">
|
||||||
|
<img class="absolute w-6 h-6 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/profile/legs.svg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="w-[105px] h-px mb-[3px] flex justify-between">
|
||||||
|
<div class="w-px h-full bg-gray-300"></div>
|
||||||
|
<div class="w-[101px] h-full bg-gray-300"></div>
|
||||||
|
<div class="w-px h-full bg-gray-300"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-11">
|
||||||
|
<p class="m-0 text-xs text-white tracking-wide">Health</p>
|
||||||
|
<span class="m-0 text-xs text-white tracking-wide">+ 15</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-[105px] h-px my-[3px] flex justify-between">
|
||||||
|
<div class="w-px h-full bg-gray-300"></div>
|
||||||
|
<div class="w-[101px] h-full bg-gray-300"></div>
|
||||||
|
<div class="w-px h-full bg-gray-300"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-11">
|
||||||
|
<p class="m-0 text-xs text-white tracking-wide">Health</p>
|
||||||
|
<span class="m-0 text-xs text-white tracking-wide">+ 15</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-[105px] h-px mt-[3px] flex justify-between">
|
||||||
|
<div class="w-px h-full bg-gray-300"></div>
|
||||||
|
<div class="w-[101px] h-full bg-gray-300"></div>
|
||||||
|
<div class="w-px h-full bg-gray-300"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="w-[105px] h-px mb-[3px] flex justify-between">
|
||||||
|
<div class="w-px h-full bg-gray-300"></div>
|
||||||
|
<div class="w-[101px] h-full bg-gray-300"></div>
|
||||||
|
<div class="w-px h-full bg-gray-300"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-11">
|
||||||
|
<p class="m-0 text-xs text-white tracking-wide">Health</p>
|
||||||
|
<span class="m-0 text-xs text-white tracking-wide">+ 15</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-[105px] h-px my-[3px] flex justify-between">
|
||||||
|
<div class="w-px h-full bg-gray-300"></div>
|
||||||
|
<div class="w-[101px] h-full bg-gray-300"></div>
|
||||||
|
<div class="w-px h-full bg-gray-300"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-11">
|
||||||
|
<p class="m-0 text-xs text-white tracking-wide">Health</p>
|
||||||
|
<span class="m-0 text-xs text-white tracking-wide">+ 15</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-[105px] h-px mt-[3px] flex justify-between">
|
||||||
|
<div class="w-px h-full bg-gray-300"></div>
|
||||||
|
<div class="w-[101px] h-full bg-gray-300"></div>
|
||||||
|
<div class="w-px h-full bg-gray-300"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-rows-4 grid-cols-6 gap-0.5">
|
||||||
|
<div v-for="n in 24" class="w-9 h-9 border border-solid border-gray-500 rounded-sm bg-gray relative hover:bg-gray-600"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, onUnmounted, ref, watch, computed } from 'vue'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
|
||||||
|
let startX = 0
|
||||||
|
let startY = 0
|
||||||
|
let initialX = 0
|
||||||
|
let initialY = 0
|
||||||
|
let modalPositionX = 0
|
||||||
|
let modalPositionY = 0
|
||||||
|
let modalWidth = 286
|
||||||
|
let modalHeight = 483
|
||||||
|
|
||||||
|
const width = ref(modalWidth)
|
||||||
|
const height = ref(modalHeight)
|
||||||
|
const x = ref(0)
|
||||||
|
const y = ref(0)
|
||||||
|
const isDragging = ref(false)
|
||||||
|
|
||||||
|
const modalStyle = computed(() => ({
|
||||||
|
top: `${y.value}px`,
|
||||||
|
left: `${x.value}px`,
|
||||||
|
width: `${width.value}px`,
|
||||||
|
height: `${height.value}px`,
|
||||||
|
maxWidth: '100vw',
|
||||||
|
maxHeight: '100vh'
|
||||||
|
}))
|
||||||
|
|
||||||
|
function startDrag(event: MouseEvent) {
|
||||||
|
isDragging.value = true
|
||||||
|
startX = event.clientX
|
||||||
|
startY = event.clientY
|
||||||
|
initialX = x.value
|
||||||
|
initialY = y.value
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
function drag(event: MouseEvent) {
|
||||||
|
if (!isDragging.value) return
|
||||||
|
const dx = event.clientX - startX
|
||||||
|
const dy = event.clientY - startY
|
||||||
|
x.value = initialX + dx
|
||||||
|
y.value = initialY + dy
|
||||||
|
adjustPosition()
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopDrag() {
|
||||||
|
isDragging.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function adjustPosition() {
|
||||||
|
x.value = Math.min(x.value, window.innerWidth - width.value)
|
||||||
|
y.value = Math.min(y.value, window.innerHeight - height.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializePosition() {
|
||||||
|
width.value = Math.min(modalWidth, window.innerWidth)
|
||||||
|
height.value = Math.min(modalHeight, window.innerHeight)
|
||||||
|
if (modalPositionX !== 0 && modalPositionY !== 0) {
|
||||||
|
x.value = modalPositionX
|
||||||
|
y.value = modalPositionY
|
||||||
|
} else {
|
||||||
|
x.value = (window.innerWidth - width.value) / 2
|
||||||
|
y.value = (window.innerHeight - height.value) / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => gameStore.uiSettings.isCharacterProfileOpen,
|
||||||
|
(value) => {
|
||||||
|
gameStore.uiSettings.isCharacterProfileOpen = value
|
||||||
|
if (value) {
|
||||||
|
initializePosition()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => modalWidth,
|
||||||
|
(value) => {
|
||||||
|
width.value = Math.min(value, window.innerWidth)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => modalHeight,
|
||||||
|
(value) => {
|
||||||
|
height.value = Math.min(value, window.innerHeight)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => modalPositionX,
|
||||||
|
(value) => {
|
||||||
|
x.value = value
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => modalPositionY,
|
||||||
|
(value) => {
|
||||||
|
y.value = value
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function keyPress(event: KeyboardEvent) {
|
||||||
|
if (event.altKey && event.key === 'c') {
|
||||||
|
gameStore.toggleCharacterProfile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
addEventListener('keydown', keyPress)
|
||||||
|
addEventListener('mousemove', drag)
|
||||||
|
addEventListener('mouseup', stopDrag)
|
||||||
|
initializePosition()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
removeEventListener('keydown', keyPress)
|
||||||
|
removeEventListener('mousemove', drag)
|
||||||
|
removeEventListener('mouseup', stopDrag)
|
||||||
|
})
|
||||||
|
</script>
|
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,42 +2,42 @@
|
|||||||
<div class="absolute top-4 left-[300px] w-[422px]">
|
<div class="absolute top-4 left-[300px] w-[422px]">
|
||||||
<div class="flex gap-2.5">
|
<div class="flex gap-2.5">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-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/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||||
<span class="z-10 text-pixel absolute top-1 left-2">F1</span>
|
<span class="z-10 text-pixel absolute top-1 left-2">F1</span>
|
||||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f1-icon.png')] bg-no-repeat"></div>
|
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f1-icon.png')] bg-no-repeat"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-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/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||||
<span class="z-10 text-pixel absolute top-1 left-2">F2</span>
|
<span class="z-10 text-pixel absolute top-1 left-2">F2</span>
|
||||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f2-icon.png')] bg-no-repeat"></div>
|
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f2-icon.png')] bg-no-repeat"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-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/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||||
<span class="z-10 text-pixel absolute top-1 left-2">F3</span>
|
<span class="z-10 text-pixel absolute top-1 left-2">F3</span>
|
||||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f3-icon.png')] bg-no-repeat"></div>
|
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f3-icon.png')] bg-no-repeat"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-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/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||||
<span class="z-10 text-pixel absolute top-1 left-2">F4</span>
|
<span class="z-10 text-pixel absolute top-1 left-2">F4</span>
|
||||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f4-icon.png')] bg-no-repeat"></div>
|
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f4-icon.png')] bg-no-repeat"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-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/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||||
<span class="z-10 text-pixel absolute top-1 left-2">F5</span>
|
<span class="z-10 text-pixel absolute top-1 left-2">F5</span>
|
||||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f5-icon.png')] bg-no-repeat"></div>
|
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f5-icon.png')] bg-no-repeat"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-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/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||||
<span class="z-10 text-pixel absolute top-1 left-2">F6</span>
|
<span class="z-10 text-pixel absolute top-1 left-2">F6</span>
|
||||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f6-icon.png')] bg-no-repeat"></div>
|
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f6-icon.png')] bg-no-repeat"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-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/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||||
<span class="z-10 text-pixel absolute top-1 left-2">F7</span>
|
<span class="z-10 text-pixel absolute top-1 left-2">F7</span>
|
||||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f7-icon.png')] bg-no-repeat"></div>
|
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f7-icon.png')] bg-no-repeat"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-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/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||||
<span class="z-10 text-pixel absolute top-1 left-2">F8</span>
|
<span class="z-10 text-pixel absolute top-1 left-2">F8</span>
|
||||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f8-icon.png')] bg-no-repeat"></div>
|
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f8-icon.png')] bg-no-repeat"></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="absolute left-[66px] top-4 bg-[url('/assets/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/ui-rect-border-4-corners.svg')] bg-no-repeat px-4 py-2 w-[181px] h-[26px] flex flex-col justify-between">
|
||||||
<div class="w-full flex items-center gap-2">
|
<div class="w-full flex items-center gap-2">
|
||||||
<label class="text-xs leading-3 text-pixel" for="hp">HP</label>
|
<label class="text-xs leading-3 text-pixel" for="hp">HP</label>
|
||||||
<progress class="h-2 rounded-sm w-full max-w-44 appearance-none accent-green" id="hp" :value="gameStore.character?.hitpoints" max="100">{{ gameStore.character?.hitpoints }}%</progress>
|
<progress class="h-2 rounded-sm w-full max-w-44 appearance-none accent-green" id="hp" :value="gameStore.character?.hitpoints" max="100">{{ gameStore.character?.hitpoints }}%</progress>
|
||||||
|
@ -5,16 +5,16 @@
|
|||||||
<p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">Open menu</p>
|
<p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">Open menu</p>
|
||||||
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
<a class="group-hover:cursor-pointer bg-[url('/assets/ui-border-4-corners.svg')] bg-no-repeat block w-[42px] h-[42px] relative">
|
<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" />
|
<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>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="menu-item group relative" @click="gameStore.toggleUserPanel">
|
<li class="menu-item group relative" @click="gameStore.toggleCharacterProfile">
|
||||||
<div class="group-hover:block absolute top-1/2 left-14 -translate-y-1/2 w-20 h-6 text-center bg-gray-800 border-2 border-solid border-gray-500 rounded-3xl hidden">
|
<div class="group-hover:block absolute top-1/2 left-14 -translate-y-1/2 w-20 h-6 text-center bg-gray-800 border-2 border-solid border-gray-500 rounded-3xl hidden">
|
||||||
<p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">User Profile</p>
|
<p class="absolute w-full bottom-0 m-0 text-xs leading-6 text-white">User Profile</p>
|
||||||
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
<a class="group-hover:cursor-pointer bg-[url('/assets/ui-border-4-corners.svg')] bg-no-repeat block w-[42px] h-[42px] relative">
|
<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-8 h-8 m-[5px] object-contain" draggable="false" src="/assets/avatar/default/head.png" />
|
<img class="group-hover:drop-shadow-default w-8 h-8 m-[5px] object-contain" draggable="false" src="/assets/avatar/default/head.png" />
|
||||||
<p class="absolute bottom-0 -right-1.5 m-0 max-w-4 font-ui z-10 text-white text-[12px] leading-[6px] drop-shadow-pixel"><span class="font-ui text-white text-[8px] ml-0.5">LVL</span> {{ characterLevel }}</p>
|
<p class="absolute bottom-0 -right-1.5 m-0 max-w-4 font-ui z-10 text-white text-[12px] leading-[6px] drop-shadow-pixel"><span class="font-ui text-white text-[8px] ml-0.5">LVL</span> {{ characterLevel }}</p>
|
||||||
</a>
|
</a>
|
||||||
|
@ -6,11 +6,11 @@
|
|||||||
<div class="absolute -bottom-3 left-1/2 -translate-x-1/2 flex gap-1">
|
<div class="absolute -bottom-3 left-1/2 -translate-x-1/2 flex gap-1">
|
||||||
<button class="w-6 h-6 relative p-0">
|
<button class="w-6 h-6 relative p-0">
|
||||||
<img class="absolute w-3 h-3 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/plus-icon.svg" />
|
<img class="absolute w-3 h-3 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/plus-icon.svg" />
|
||||||
<img class="w-full h-full" src="/assets/ui-border-4-corners.svg" />
|
<img class="w-full h-full" src="/assets/ui-elements/ui-border-4-corners.svg" />
|
||||||
</button>
|
</button>
|
||||||
<button class="w-6 h-6 relative p-0">
|
<button class="w-6 h-6 relative p-0">
|
||||||
<img class="absolute w-3 h-3 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/minus-icon.svg" />
|
<img class="absolute w-3 h-3 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" src="/assets/icons/minus-icon.svg" />
|
||||||
<img class="w-full h-full" src="/assets/ui-border-4-corners.svg" />
|
<img class="w-full h-full" src="/assets/ui-elements/ui-border-4-corners.svg" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="absolute z-50 w-full h-dvh top-0 left-0 bg-black/60" v-show="gameStore.uiSettings.isUserPanelOpen">
|
<div class="absolute z-50 w-full h-dvh top-0 left-0 bg-black/60" v-show="false">
|
||||||
<div class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 max-w-[875px] max-h-[600px] h-full w-[80%] bg-gray-700 border-solid border-2 border-gray-500 rounded-md z-50 flex flex-col backdrop-blur-sm shadow-lg">
|
<div class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 max-w-[875px] max-h-[600px] h-full w-[80%] bg-gray-700 border-solid border-2 border-gray-500 rounded-md z-50 flex flex-col backdrop-blur-sm shadow-lg">
|
||||||
<div class="p-2.5 flex max-sm:flex-wrap justify-between items-center gap-5 border-solid border-0 border-b border-gray-500">
|
<div class="p-2.5 flex max-sm:flex-wrap justify-between items-center gap-5 border-solid border-0 border-b border-gray-500">
|
||||||
<h3 class="m-0 font-medium shrink-0">Game menu</h3>
|
<h3 class="m-0 font-medium shrink-0">Game menu</h3>
|
||||||
@ -10,7 +10,7 @@
|
|||||||
<button @click.stop="userPanelScreen = 'settings'" :class="{ active: userPanelScreen === 'settings' }" class="btn-cyan py-1.5 px-4 min-w-24">Settings</button>
|
<button @click.stop="userPanelScreen = 'settings'" :class="{ active: userPanelScreen === 'settings' }" class="btn-cyan py-1.5 px-4 min-w-24">Settings</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2.5">
|
<div class="flex gap-2.5">
|
||||||
<button class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out" @click="gameStore.toggleUserPanel">
|
<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/close-button-white.svg" class="w-full h-full" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
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>
|
172
src/components/screens/Characters.vue
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
<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 selected 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/ui-border-2-corners-bottom.svg')] w-[190px] h-52 bg-no-repeat bg-center flex items-center justify-center">
|
||||||
|
<img class="w-12 object-contain mb-3.5" src="/assets/avatar/default/0.png" alt="Player avatar"/>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between w-[190px]">
|
||||||
|
<!-- TODO: replace with color swatches -->
|
||||||
|
<div v-for="n in 9" class="w-4 h-4 rounded-sm bg-white"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between w-[190px]">
|
||||||
|
<button class="btn-empty flex gap-2">
|
||||||
|
<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">
|
||||||
|
<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>
|
||||||
|
</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', { 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>
|
@ -5,12 +5,12 @@
|
|||||||
<Menu />
|
<Menu />
|
||||||
<Hud />
|
<Hud />
|
||||||
<Hotkeys />
|
<Hotkeys />
|
||||||
<Minimap />
|
<Clock />
|
||||||
<Zone />
|
<Zone />
|
||||||
<Chat />
|
<Chat />
|
||||||
<ExpBar />
|
<ExpBar />
|
||||||
|
|
||||||
<Inventory />
|
<CharacterProfile />
|
||||||
<Effects />
|
<Effects />
|
||||||
</Scene>
|
</Scene>
|
||||||
</Game>
|
</Game>
|
||||||
@ -20,7 +20,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/config'
|
import config from '@/config'
|
||||||
import 'phaser'
|
import 'phaser'
|
||||||
import { onBeforeUnmount } from 'vue'
|
|
||||||
import { Game, Scene } from 'phavuer'
|
import { Game, Scene } from 'phavuer'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import Menu from '@/components/gui/Menu.vue'
|
import Menu from '@/components/gui/Menu.vue'
|
||||||
@ -29,14 +28,13 @@ import Hud from '@/components/gui/Hud.vue'
|
|||||||
import Zone from '@/components/zone/Zone.vue'
|
import Zone from '@/components/zone/Zone.vue'
|
||||||
import Hotkeys from '@/components/gui/Hotkeys.vue'
|
import Hotkeys from '@/components/gui/Hotkeys.vue'
|
||||||
import Chat from '@/components/gui/Chat.vue'
|
import Chat from '@/components/gui/Chat.vue'
|
||||||
import Inventory from '@/components/gui/UserPanel.vue'
|
import CharacterProfile from '@/components/gui/CharacterProfile.vue'
|
||||||
import Effects from '@/components/Effects.vue'
|
import Effects from '@/components/Effects.vue'
|
||||||
import Minimap from '@/components/gui/Minimap.vue'
|
// import Minimap from '@/components/gui/Minimap.vue'
|
||||||
|
import Clock from '@/components/gui/Clock.vue'
|
||||||
import AwaitLoaderPlugin from 'phaser3-rex-plugins/plugins/awaitloader-plugin'
|
import AwaitLoaderPlugin from 'phaser3-rex-plugins/plugins/awaitloader-plugin'
|
||||||
import { useAssetManager } from '@/utilities/assetManager'
|
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const assetManager = useAssetManager
|
|
||||||
|
|
||||||
const gameConfig = {
|
const gameConfig = {
|
||||||
name: config.name,
|
name: config.name,
|
||||||
@ -79,43 +77,7 @@ function preloadScene(scene: Phaser.Scene) {
|
|||||||
*/
|
*/
|
||||||
scene.load.image('blank_tile', '/assets/zone/blank_tile.png')
|
scene.load.image('blank_tile', '/assets/zone/blank_tile.png')
|
||||||
scene.load.image('waypoint', '/assets/waypoint.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) {
|
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
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {})
|
|
||||||
</script>
|
</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/ui-box-outer.svg" class="absolute w-full h-full" alt="UI box outer" />
|
||||||
|
<img src="/assets/ui-elements/ui-box-inner.svg" class="absolute left-2 top-2 w-[calc(100%_-_16px)] h-[calc(100%_-_16px)] max-lg:hidden" alt="UI box inner" />
|
||||||
|
|
||||||
|
<!-- 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">
|
<div class="flex justify-center items-center h-dvh relative">
|
||||||
<Game :config="gameConfig" @create="createGame">
|
<Game :config="gameConfig" @create="createGame">
|
||||||
<Scene name="main" @preload="preloadScene" @create="createScene">
|
<Scene name="main" @preload="preloadScene" @create="createScene">
|
||||||
<ZoneEditor 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>
|
</Scene>
|
||||||
</Game>
|
</Game>
|
||||||
</div>
|
</div>
|
||||||
@ -11,22 +11,32 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/config'
|
import config from '@/config'
|
||||||
import 'phaser'
|
import 'phaser'
|
||||||
import { ref, onBeforeUnmount } from 'vue'
|
|
||||||
import { Game, Scene } from 'phavuer'
|
import { Game, Scene } from 'phavuer'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
import ZoneEditor from '@/components/gameMaster/zoneEditor/ZoneEditor.vue'
|
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 gameStore = useGameStore()
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
const isLoaded = ref(false)
|
|
||||||
|
|
||||||
const gameConfig = {
|
const gameConfig = {
|
||||||
name: config.name,
|
name: config.name,
|
||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
height: window.innerHeight,
|
height: window.innerHeight,
|
||||||
type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS
|
type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS
|
||||||
resolution: 5
|
resolution: 5,
|
||||||
|
plugins: {
|
||||||
|
global: [
|
||||||
|
{
|
||||||
|
key: 'rexAwaitLoader',
|
||||||
|
plugin: AwaitLoaderPlugin,
|
||||||
|
start: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createGame = (game: Phaser.Game) => {
|
const createGame = (game: Phaser.Game) => {
|
||||||
@ -48,43 +58,6 @@ const createGame = (game: Phaser.Game) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const preloadScene = async (scene: Phaser.Scene) => {
|
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
|
* 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('TELEPORT', '/assets/zone/tp_tile.png')
|
||||||
scene.load.image('blank_tile', '/assets/zone/blank_tile.png')
|
scene.load.image('blank_tile', '/assets/zone/blank_tile.png')
|
||||||
scene.load.image('waypoint', '/assets/waypoint.png')
|
scene.load.image('waypoint', '/assets/waypoint.png')
|
||||||
}
|
|
||||||
|
|
||||||
const createScene = async (scene: Phaser.Scene) => {
|
|
||||||
/**
|
/**
|
||||||
* Create sprite animations
|
* Because Phaser can't load tiles after the scene with map in it is created,
|
||||||
* This is done here because phaser forces us to
|
* we need to load and cache all the tiles first.
|
||||||
|
* Then load them into the scene.
|
||||||
*/
|
*/
|
||||||
gameStore.assets.forEach((asset) => {
|
scene.load.rexAwait(async function (successCallback: any) {
|
||||||
if (asset.group !== 'sprite_animations') return
|
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({
|
successCallback()
|
||||||
key: asset.key,
|
|
||||||
frameRate: 7,
|
|
||||||
frames: scene.anims.generateFrameNumbers(asset.key, { start: 0, end: asset.frameCount! - 1 }),
|
|
||||||
repeat: -1
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
const createScene = async (scene: Phaser.Scene) => {}
|
||||||
isLoaded.value = false
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
@ -18,12 +18,13 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import config from '@/config'
|
import config from '@/config'
|
||||||
import { type ExtendedCharacter } from '@/types'
|
import { type ExtendedCharacter, type Sprite as SpriteT } from '@/types'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useZoneStore } from '@/stores/zoneStore'
|
import { useZoneStore } from '@/stores/zoneStore'
|
||||||
import { watch, computed, ref, onMounted, onUnmounted } from 'vue'
|
import { watch, computed, ref, onMounted, onUnmounted } from 'vue'
|
||||||
import { Container, refObj, RoundRectangle, Sprite, Text, useGame, useScene } from 'phavuer'
|
import { Container, refObj, RoundRectangle, Sprite, Text, useGame, useScene } from 'phavuer'
|
||||||
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||||
|
import { loadSpriteTextures } from '@/composables/gameComposable'
|
||||||
|
|
||||||
enum Direction {
|
enum Direction {
|
||||||
POSITIVE,
|
POSITIVE,
|
||||||
@ -181,6 +182,15 @@ watch(
|
|||||||
watch(() => props.character.isMoving, updateSprite)
|
watch(() => props.character.isMoving, updateSprite)
|
||||||
watch(() => props.character.rotation, updateSprite)
|
watch(() => props.character.rotation, updateSprite)
|
||||||
|
|
||||||
|
loadSpriteTextures(scene, props.character.characterType?.sprite as SpriteT)
|
||||||
|
.then(() => {
|
||||||
|
charSprite.value!.setTexture(charTexture.value)
|
||||||
|
charSprite.value!.setFlipX(isFlippedX.value)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error loading texture:', error)
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
charChatContainer.value!.setName(`${props.character!.name}_chatContainer`)
|
charChatContainer.value!.setName(`${props.character!.name}_chatContainer`)
|
||||||
charChatContainer.value!.setVisible(false)
|
charChatContainer.value!.setVisible(false)
|
||||||
@ -194,10 +204,6 @@ onMounted(() => {
|
|||||||
scene.cameras.main.stopFollow()
|
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.character.positionX, props.character.positionY, props.character.rotation)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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/ui-border-4-corners.svg', '/assets/ui-elements/ui-border-4-corners-light.svg', '/assets/ui-elements/ui-border-4-corners-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,22 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<div v-if="isModalOpenRef" class="fixed border-solid border-2 border-gray-500 z-50 flex flex-col backdrop-blur-sm shadow-lg" :style="modalStyle">
|
<div v-if="isModalOpenRef" class="fixed border-solid border-2 border-gray-500 z-50 flex flex-col backdrop-blur-sm shadow-lg" :style="modalStyle">
|
||||||
|
<!-- Header -->
|
||||||
<div @mousedown="startDrag" class="cursor-move p-2.5 flex justify-between items-center border-solid border-0 border-b border-gray-500 relative">
|
<div @mousedown="startDrag" class="cursor-move p-2.5 flex justify-between items-center border-solid border-0 border-b border-gray-500 relative">
|
||||||
<div class="rounded-t-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">
|
<div class="relative z-10">
|
||||||
<slot name="modalHeader" />
|
<slot name="modalHeader" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2.5">
|
<div class="flex gap-2.5">
|
||||||
<button @click="toggleFullScreen" class="w-5 h-5 m-0 p-0 relative hover:scale-110 transition-transform duration-300 ease-in-out" v-if="canFullScreen">
|
<button v-if="canFullScreen" @click="toggleFullScreen" class="w-5 h-5 m-0 p-0 relative hover:scale-110 transition-transform duration-300 ease-in-out">
|
||||||
<img :alt="isFullScreen ? 'exit full-screen' : 'full-screen'" draggable="false" :src="isFullScreen ? '/assets/icons/minimize.svg' : '/assets/icons/increase-size-option.svg'" class="w-3.5 h-3.5 invert" />
|
<img :alt="isFullScreen ? 'exit full-screen' : 'full-screen'" :src="isFullScreen ? '/assets/icons/minimize.svg' : '/assets/icons/increase-size-option.svg'" class="w-3.5 h-3.5 invert" draggable="false" />
|
||||||
</button>
|
</button>
|
||||||
<button @click="close" v-if="closable" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
|
<button v-if="closable" @click="emit('modal:close')" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
|
||||||
<img alt="close" draggable="false" src="/assets/icons/close-button-white.svg" class="w-full h-full" />
|
<img alt="close" src="/assets/icons/close-button-white.svg" class="w-full h-full" draggable="false" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
<div class="overflow-hidden grow relative">
|
<div class="overflow-hidden grow relative">
|
||||||
<div class="rounded-b-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">
|
<div class="relative z-10 h-full">
|
||||||
<slot name="modalBody" />
|
<slot name="modalBody" />
|
||||||
</div>
|
</div>
|
||||||
@ -27,221 +30,187 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineEmits, onMounted, onUnmounted, ref, watch, computed } from 'vue'
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
interface ModalProps {
|
||||||
isModalOpen: {
|
isModalOpen: boolean
|
||||||
type: Boolean,
|
closable?: boolean
|
||||||
default: false
|
isResizable?: boolean
|
||||||
},
|
canFullScreen?: boolean
|
||||||
closable: {
|
modalPositionX?: number
|
||||||
type: Boolean,
|
modalPositionY?: number
|
||||||
default: true
|
modalWidth?: number
|
||||||
},
|
modalHeight?: number
|
||||||
isResizable: {
|
}
|
||||||
type: Boolean,
|
|
||||||
default: true
|
interface Position {
|
||||||
},
|
x: number
|
||||||
canFullScreen: {
|
y: number
|
||||||
type: Boolean,
|
width: number
|
||||||
default: false
|
height: number
|
||||||
},
|
}
|
||||||
modalPositionX: {
|
|
||||||
type: Number,
|
const props = withDefaults(defineProps<ModalProps>(), {
|
||||||
default: 0
|
isModalOpen: false,
|
||||||
},
|
closable: true,
|
||||||
modalPositionY: {
|
isResizable: true,
|
||||||
type: Number,
|
canFullScreen: false,
|
||||||
default: 0
|
modalPositionX: 0,
|
||||||
},
|
modalPositionY: 0,
|
||||||
modalWidth: {
|
modalWidth: 500,
|
||||||
type: Number,
|
modalHeight: 280
|
||||||
default: 500
|
|
||||||
},
|
|
||||||
modalHeight: {
|
|
||||||
type: Number,
|
|
||||||
default: 280
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const isModalOpenRef = ref(props.isModalOpen)
|
const emit = defineEmits<{
|
||||||
const emit = defineEmits(['modal:close', 'character:create'])
|
'modal:close': []
|
||||||
|
'character:create': []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isModalOpenRef = ref(props.isModalOpen)
|
||||||
const width = ref(props.modalWidth)
|
const width = ref(props.modalWidth)
|
||||||
const height = ref(props.modalHeight)
|
const height = ref(props.modalHeight)
|
||||||
const x = ref(0)
|
const x = ref(0)
|
||||||
const y = ref(0)
|
const y = ref(0)
|
||||||
|
|
||||||
const minWidth = ref(200)
|
|
||||||
const minHeight = ref(100)
|
|
||||||
const isResizing = ref(false)
|
const isResizing = ref(false)
|
||||||
const isDragging = ref(false)
|
const isDragging = ref(false)
|
||||||
const isFullScreen = ref(false)
|
const isFullScreen = ref(false)
|
||||||
|
|
||||||
let startX = 0
|
const minDimensions = {
|
||||||
let startY = 0
|
width: 200,
|
||||||
let initialX = 0
|
height: 100
|
||||||
let initialY = 0
|
}
|
||||||
let startWidth = 0
|
|
||||||
let startHeight = 0
|
let dragState = {
|
||||||
let preFullScreenState = { x: 0, y: 0, width: 0, height: 0 }
|
startX: 0,
|
||||||
|
startY: 0,
|
||||||
|
initialX: 0,
|
||||||
|
initialY: 0,
|
||||||
|
startWidth: 0,
|
||||||
|
startHeight: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
let preFullScreenState: Position = { x: 0, y: 0, width: 0, height: 0 }
|
||||||
|
|
||||||
const modalStyle = computed(() => ({
|
const modalStyle = computed(() => ({
|
||||||
borderRadius: isFullScreen.value ? '0' : '6px',
|
borderRadius: isFullScreen.value ? '0' : '6px',
|
||||||
top: isFullScreen.value ? '0' : `${y.value}px`,
|
top: isFullScreen.value ? '0' : `${y.value}px`,
|
||||||
left: isFullScreen.value ? '0' : `${x.value}px`,
|
left: isFullScreen.value ? '0' : `${x.value}px`,
|
||||||
width: isFullScreen.value ? '100vw' : `${width.value}px`,
|
width: isFullScreen.value ? '100vw' : `${width.value}px`,
|
||||||
height: isFullScreen.value ? '100vh' : `${height.value}px`,
|
height: isFullScreen.value ? '100vh' : `${height.value}px`
|
||||||
maxWidth: '100vw',
|
|
||||||
maxHeight: '100vh'
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
function close() {
|
|
||||||
emit('modal:close')
|
|
||||||
}
|
|
||||||
|
|
||||||
function startResize(event: MouseEvent) {
|
function startResize(event: MouseEvent) {
|
||||||
if (isFullScreen.value) return
|
if (isFullScreen.value) return
|
||||||
isResizing.value = true
|
isResizing.value = true
|
||||||
startWidth = width.value - event.clientX
|
dragState.startWidth = width.value - event.clientX
|
||||||
startHeight = height.value - event.clientY
|
dragState.startHeight = height.value - event.clientY
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
function resizeModal(event: MouseEvent) {
|
function resizeModal(event: MouseEvent) {
|
||||||
if (!isResizing.value || isFullScreen.value) return
|
if (!isResizing.value || isFullScreen.value) return
|
||||||
const newWidth = Math.min(startWidth + event.clientX, window.innerWidth)
|
width.value = Math.max(dragState.startWidth + event.clientX, minDimensions.width)
|
||||||
const newHeight = Math.min(startHeight + event.clientY, window.innerHeight)
|
height.value = Math.max(dragState.startHeight + event.clientY, minDimensions.height)
|
||||||
width.value = Math.max(newWidth, minWidth.value)
|
|
||||||
height.value = Math.max(newHeight, minHeight.value)
|
|
||||||
adjustPosition()
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopResize() {
|
|
||||||
isResizing.value = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function startDrag(event: MouseEvent) {
|
function startDrag(event: MouseEvent) {
|
||||||
if (isFullScreen.value) return
|
if (isFullScreen.value) return
|
||||||
isDragging.value = true
|
isDragging.value = true
|
||||||
startX = event.clientX
|
dragState = {
|
||||||
startY = event.clientY
|
startX: event.clientX,
|
||||||
initialX = x.value
|
startY: event.clientY,
|
||||||
initialY = y.value
|
initialX: x.value,
|
||||||
|
initialY: y.value,
|
||||||
|
startWidth: width.value,
|
||||||
|
startHeight: height.value
|
||||||
|
}
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
function drag(event: MouseEvent) {
|
function drag(event: MouseEvent) {
|
||||||
if (!isDragging.value || isFullScreen.value) return
|
if (!isDragging.value || isFullScreen.value) return
|
||||||
const dx = event.clientX - startX
|
x.value = dragState.initialX + (event.clientX - dragState.startX)
|
||||||
const dy = event.clientY - startY
|
y.value = dragState.initialY + (event.clientY - dragState.startY)
|
||||||
x.value = initialX + dx
|
|
||||||
y.value = initialY + dy
|
|
||||||
adjustPosition()
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopDrag() {
|
|
||||||
isDragging.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function adjustPosition() {
|
|
||||||
if (isFullScreen.value) return
|
|
||||||
x.value = Math.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) {
|
|
||||||
console.log(props.modalPositionX)
|
|
||||||
console.log(props.modalPositionY)
|
|
||||||
x.value = props.modalPositionX
|
|
||||||
y.value = props.modalPositionY
|
|
||||||
} else {
|
|
||||||
x.value = (window.innerWidth - width.value) / 2
|
|
||||||
y.value = (window.innerHeight - height.value) / 2
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleFullScreen() {
|
function toggleFullScreen() {
|
||||||
if (isFullScreen.value) {
|
if (isFullScreen.value) {
|
||||||
// Exit full-screen
|
Object.assign({ x, y, width, height }, preFullScreenState)
|
||||||
x.value = preFullScreenState.x
|
|
||||||
y.value = preFullScreenState.y
|
|
||||||
width.value = preFullScreenState.width
|
|
||||||
height.value = preFullScreenState.height
|
|
||||||
isFullScreen.value = false
|
|
||||||
} else {
|
} else {
|
||||||
// Enter full-screen
|
|
||||||
preFullScreenState = { x: x.value, y: y.value, width: width.value, height: height.value }
|
preFullScreenState = { x: x.value, y: y.value, width: width.value, height: height.value }
|
||||||
isFullScreen.value = true
|
|
||||||
}
|
}
|
||||||
|
isFullScreen.value = !isFullScreen.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initializePosition() {
|
||||||
|
width.value = props.modalWidth
|
||||||
|
height.value = props.modalHeight
|
||||||
|
x.value = props.modalPositionX || (window.innerWidth - width.value) / 2
|
||||||
|
y.value = props.modalPositionY || (window.innerHeight - height.value) / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watchers
|
||||||
watch(
|
watch(
|
||||||
() => props.isModalOpen,
|
() => props.isModalOpen,
|
||||||
(value) => {
|
(value) => {
|
||||||
isModalOpenRef.value = value
|
isModalOpenRef.value = value
|
||||||
if (value) {
|
if (value) initializePosition()
|
||||||
initializePosition()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modalWidth,
|
() => props.modalWidth,
|
||||||
(value) => {
|
(value) => (width.value = value)
|
||||||
width.value = Math.min(value, window.innerWidth)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modalHeight,
|
() => props.modalHeight,
|
||||||
(value) => {
|
(value) => (height.value = value)
|
||||||
height.value = Math.min(value, window.innerHeight)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modalPositionX,
|
() => props.modalPositionX,
|
||||||
(value) => {
|
(value) => (x.value = value)
|
||||||
x.value = value
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modalPositionY,
|
() => props.modalPositionY,
|
||||||
(value) => {
|
(value) => (y.value = value)
|
||||||
y.value = value
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Lifecycle hooks
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener('mousemove', drag)
|
const handlers: Record<string, EventListener[]> = {
|
||||||
window.addEventListener('mouseup', stopDrag)
|
mousemove: [(e: Event) => drag(e as MouseEvent), (e: Event) => resizeModal(e as MouseEvent)],
|
||||||
window.addEventListener('mousemove', resizeModal)
|
mouseup: [
|
||||||
window.addEventListener('mouseup', stopResize)
|
() => {
|
||||||
if (props.modalPositionX !== 0 && props.modalPositionY !== 0) {
|
isDragging.value = false
|
||||||
window.addEventListener('resize', handleResize)
|
},
|
||||||
|
() => {
|
||||||
|
isResizing.value = false
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Object.entries(handlers).forEach(([event, fns]) => {
|
||||||
|
fns.forEach((fn) => window.addEventListener(event, fn))
|
||||||
|
})
|
||||||
|
|
||||||
initializePosition()
|
initializePosition()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('mousemove', drag)
|
const handlers: Record<string, EventListener[]> = {
|
||||||
window.removeEventListener('mouseup', stopDrag)
|
mousemove: [(e: Event) => drag(e as MouseEvent), (e: Event) => resizeModal(e as MouseEvent)],
|
||||||
window.removeEventListener('mousemove', resizeModal)
|
mouseup: [
|
||||||
window.removeEventListener('mouseup', stopResize)
|
() => {
|
||||||
if (props.modalPositionX !== 0 && props.modalPositionY !== 0) {
|
isDragging.value = false
|
||||||
window.removeEventListener('resize', handleResize)
|
},
|
||||||
|
() => {
|
||||||
|
isResizing.value = false
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Object.entries(handlers).forEach(([event, fns]) => {
|
||||||
|
fns.forEach((fn) => window.removeEventListener(event, fn))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import { onBeforeMount, onBeforeUnmount, watch } from 'vue'
|
import { onBeforeMount, onBeforeUnmount, onMounted, onUnmounted, watch } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ function setupNotificationListener(connection: any) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onMounted(() => {
|
||||||
const connection = gameStore.connection
|
const connection = gameStore.connection
|
||||||
if (connection) {
|
if (connection) {
|
||||||
setupNotificationListener(connection)
|
setupNotificationListener(connection)
|
||||||
@ -49,7 +49,7 @@ onBeforeMount(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onUnmounted(() => {
|
||||||
const connection = gameStore.connection
|
const connection = gameStore.connection
|
||||||
if (connection) {
|
if (connection) {
|
||||||
connection.off('notification')
|
connection.off('notification')
|
||||||
|
@ -1,18 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<ZoneTiles :key="zoneStore.zone?.id ?? 0" @tilemap:create="tileMap = $event" />
|
<ZoneTiles :key="zoneStore.zone?.id ?? 0" @tileMap:create="tileMap = $event" />
|
||||||
<ZoneObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
<ZoneObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
||||||
<Characters v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
<Characters v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref, onUnmounted } from 'vue'
|
||||||
|
import { useScene } from 'phavuer'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useZoneStore } from '@/stores/zoneStore'
|
import { useZoneStore } from '@/stores/zoneStore'
|
||||||
import { onBeforeUnmount, ref, onBeforeMount } from 'vue'
|
import { loadZoneTilesIntoScene } from '@/composables/zoneComposable'
|
||||||
import type { Character as CharacterT, Zone as ZoneT, ExtendedCharacter as ExtendedCharacterT } from '@/types'
|
import type { Character as CharacterT, Zone as ZoneT, ExtendedCharacter as ExtendedCharacterT } from '@/types'
|
||||||
import ZoneTiles from '@/components/zone/ZoneTiles.vue'
|
import ZoneTiles from '@/components/zone/ZoneTiles.vue'
|
||||||
import ZoneObjects from '@/components/zone/ZoneObjects.vue'
|
import ZoneObjects from '@/components/zone/ZoneObjects.vue'
|
||||||
import Characters from '@/components/zone/Characters.vue'
|
import Characters from '@/components/zone/Characters.vue'
|
||||||
|
|
||||||
|
const scene = useScene()
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const zoneStore = useZoneStore()
|
const zoneStore = useZoneStore()
|
||||||
|
|
||||||
@ -33,6 +36,7 @@ gameStore.connection!.on('zone:character:teleport', async (data: zoneLoadData) =
|
|||||||
zoneId: data.zone.id
|
zoneId: data.zone.id
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await loadZoneTilesIntoScene(data.zone, scene)
|
||||||
zoneStore.setZone(data.zone)
|
zoneStore.setZone(data.zone)
|
||||||
zoneStore.setCharacters(data.characters)
|
zoneStore.setCharacters(data.characters)
|
||||||
})
|
})
|
||||||
@ -51,15 +55,14 @@ gameStore.connection!.on('character:move', (data: ExtendedCharacterT) => {
|
|||||||
zoneStore.updateCharacter(data)
|
zoneStore.updateCharacter(data)
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeMount(async () => {
|
// 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) => {
|
gameStore!.connection!.emit('zone:character:join', async (response: zoneLoadData) => {
|
||||||
// Set zone and characters
|
await loadZoneTilesIntoScene(response.zone, scene)
|
||||||
zoneStore.setZone(response.zone)
|
zoneStore.setZone(response.zone)
|
||||||
zoneStore.setCharacters(response.characters)
|
zoneStore.setCharacters(response.characters)
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onUnmounted(() => {
|
||||||
zoneStore.reset()
|
zoneStore.reset()
|
||||||
gameStore.connection!.off('zone:character:teleport')
|
gameStore.connection!.off('zone:character:teleport')
|
||||||
gameStore.connection!.off('zone:character:join')
|
gameStore.connection!.off('zone:character:join')
|
||||||
|
@ -1,23 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<Controls :layer="tiles" :depth="0" />
|
<Controls :layer="tileLayer" :depth="0" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/config'
|
import config from '@/config'
|
||||||
import { useScene } from 'phavuer'
|
import { useScene } from 'phavuer'
|
||||||
import { useZoneStore } from '@/stores/zoneStore'
|
import { useZoneStore } from '@/stores/zoneStore'
|
||||||
import { onBeforeMount, onBeforeUnmount } from 'vue'
|
import { onBeforeUnmount } from 'vue'
|
||||||
import { setLayerTiles } from '@/composables/zoneComposable'
|
import { FlattenZoneArray, setLayerTiles } from '@/composables/zoneComposable'
|
||||||
import Controls from '@/components/utilities/Controls.vue'
|
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 scene = useScene()
|
||||||
const zoneTilemap = createTilemap()
|
const zoneStore = useZoneStore()
|
||||||
const tiles = createTileLayer()
|
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({
|
const zoneData = new Phaser.Tilemaps.MapData({
|
||||||
width: zoneStore.zone?.width,
|
width: zoneStore.zone?.width,
|
||||||
height: zoneStore.zone?.height,
|
height: zoneStore.zone?.height,
|
||||||
@ -26,22 +32,26 @@ function createTilemap() {
|
|||||||
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
|
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
|
||||||
format: Phaser.Tilemaps.Formats.ARRAY_2D
|
format: Phaser.Tilemaps.Formats.ARRAY_2D
|
||||||
})
|
})
|
||||||
const tilemap = new Phaser.Tilemaps.Tilemap(scene, zoneData)
|
|
||||||
emit('tilemap:create', tilemap)
|
const newTileMap = new Phaser.Tilemaps.Tilemap(scene, zoneData)
|
||||||
return tilemap
|
emit('tileMap:create', newTileMap)
|
||||||
|
|
||||||
|
return newTileMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Tileset is a combination of a single image containing the tiles and a container for data about each tile.
|
||||||
|
*/
|
||||||
function createTileLayer() {
|
function createTileLayer() {
|
||||||
const tilesFromZone = zoneStore.zone?.tiles || []
|
const tilesArray = unduplicateArray(FlattenZoneArray(zoneStore.zone?.tiles ?? []))
|
||||||
const uniqueTiles = new Set(tilesFromZone.flat().filter(Boolean))
|
|
||||||
|
|
||||||
const tilesetImages = Array.from(uniqueTiles).map((tile, index) => {
|
const tilesetImages = Array.from(tilesArray).map((tile: any, index: number) => {
|
||||||
return zoneTilemap.addTilesetImage(tile, tile, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y })
|
return tileMap.addTilesetImage(tile, tile, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y })
|
||||||
}) as any
|
}) as any
|
||||||
|
|
||||||
// Add blank tile
|
// Add blank tile
|
||||||
tilesetImages.push(zoneTilemap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 1, 2, 0, { x: 0, y: -config.tile_size.y }))
|
tilesetImages.push(tileMap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 1, 2, 0, { x: 0, y: -config.tile_size.y }))
|
||||||
const layer = zoneTilemap.createBlankLayer('tiles', tilesetImages, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
|
const layer = tileMap.createBlankLayer('tiles', tilesetImages, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
|
||||||
|
|
||||||
layer.setDepth(0)
|
layer.setDepth(0)
|
||||||
layer.setCullPadding(2, 2)
|
layer.setCullPadding(2, 2)
|
||||||
@ -49,16 +59,11 @@ function createTileLayer() {
|
|||||||
return layer
|
return layer
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeMount(() => {
|
setLayerTiles(tileMap, tileLayer, zoneStore.zone?.tiles)
|
||||||
if (!zoneStore.zone?.tiles) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setLayerTiles(zoneTilemap, tiles, zoneStore.zone.tiles)
|
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
zoneTilemap.destroyLayer('tiles')
|
tileMap.destroyLayer('tiles')
|
||||||
zoneTilemap.removeAllLayers()
|
tileMap.removeAllLayers()
|
||||||
zoneTilemap.destroy()
|
tileMap.destroy()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,22 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<Image v-if="isTextureLoaded" v-bind="imageProps" />
|
<Image v-if="gameStore.getLoadedAsset(props.zoneObject.object.id)" v-bind="imageProps" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { computed, onMounted } from 'vue'
|
||||||
import { Image, useScene } from 'phavuer'
|
import { Image, useScene } from 'phavuer'
|
||||||
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||||
import { useAssetManager } from '@/utilities/assetManager'
|
import { loadTexture } from '@/composables/gameComposable'
|
||||||
import type { ZoneObject } from '@/types'
|
import type { AssetDataT, ZoneObject } from '@/types'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
tilemap: Phaser.Tilemaps.Tilemap
|
tilemap: Phaser.Tilemaps.Tilemap
|
||||||
zoneObject: ZoneObject
|
zoneObject: ZoneObject
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
const assetManager = useAssetManager
|
|
||||||
const isTextureLoaded = ref(false)
|
|
||||||
|
|
||||||
const imageProps = computed(() => ({
|
const imageProps = computed(() => ({
|
||||||
depth: calculateIsometricDepth(props.zoneObject.positionX, props.zoneObject.positionY, props.zoneObject.object.frameWidth, props.zoneObject.object.frameHeight),
|
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)
|
originX: Number(props.zoneObject.object.originY)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const loadTexture = async () => {
|
loadTexture(scene, {
|
||||||
const textureId = props.zoneObject.object.id
|
key: props.zoneObject.object.id,
|
||||||
|
data: '/assets/objects/' + props.zoneObject.object.id + '.png',
|
||||||
// Check if the texture is already loaded in Phaser
|
group: 'objects',
|
||||||
if (scene.textures.exists(textureId)) {
|
updatedAt: props.zoneObject.object.updatedAt,
|
||||||
isTextureLoaded.value = true
|
frameWidth: props.zoneObject.object.frameWidth,
|
||||||
return
|
frameHeight: props.zoneObject.object.frameHeight
|
||||||
}
|
} as AssetDataT).catch((error) => {
|
||||||
|
console.error('Error loading texture:', error)
|
||||||
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) => {
|
|
||||||
console.error('Error loading texture:', error)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
</script>
|
</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,86 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
// 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
|
const { worldX, worldY } = pointer
|
||||||
updateWaypoint(worldX, worldY)
|
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)
|
const distance = Phaser.Math.Distance.Between(pointerStartPosition.value.x, pointerStartPosition.value.y, pointer.x, pointer.y)
|
||||||
|
|
||||||
if (distance > dragThreshold) {
|
if (distance > dragThreshold) {
|
||||||
|
@ -26,7 +26,7 @@ export function useZoneEditorPointerHandlers(scene: Phaser.Scene, layer: Phaser.
|
|||||||
}
|
}
|
||||||
|
|
||||||
function dragZone(pointer: Phaser.Input.Pointer) {
|
function dragZone(pointer: Phaser.Input.Pointer) {
|
||||||
if (gameStore.isPlayerDraggingCamera) {
|
if (gameStore.game.isPlayerDraggingCamera) {
|
||||||
const { x, y, prevPosition } = pointer
|
const { x, y, prevPosition } = pointer
|
||||||
const { scrollX, scrollY, zoom } = camera
|
const { scrollX, scrollY, zoom } = camera
|
||||||
camera.setScroll(scrollX - (x - prevPosition.x) / zoom, scrollY - (y - prevPosition.y) / zoom)
|
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 TilemapLayer = Phaser.Tilemaps.TilemapLayer
|
||||||
import Tileset = Phaser.Tilemaps.Tileset
|
import Tileset = Phaser.Tilemaps.Tileset
|
||||||
import Tile = Phaser.Tilemaps.Tile
|
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 {
|
export function getTile(layer: TilemapLayer | Tilemap, x: number, y: number): Tile | undefined {
|
||||||
const tile = layer.getTileAtWorldXY(x, y)
|
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) {
|
export function placeTile(zone: Tilemap, layer: TilemapLayer, x: number, y: number, tileName: string) {
|
||||||
let tileImg = zone.getTileset(tileName) as Tileset
|
let tileImg = zone.getTileset(tileName) as Tileset
|
||||||
if (!tileImg) {
|
if (!tileImg) {
|
||||||
|
console.log('tile not found:', tileName)
|
||||||
tileImg = zone.getTileset('blank_tile') as Tileset
|
tileImg = zone.getTileset('blank_tile') as Tileset
|
||||||
}
|
}
|
||||||
layer.putTileAt(tileImg.firstgid, x, y)
|
layer.putTileAt(tileImg.firstgid, x, y)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setLayerTiles(zone: Tilemap, layer: TilemapLayer, tiles: string[][]) {
|
export function setLayerTiles(zone: Tilemap, layer: TilemapLayer, tiles: string[][]) {
|
||||||
|
if (!tiles) return
|
||||||
|
|
||||||
tiles.forEach((row: string[], y: number) => {
|
tiles.forEach((row: string[], y: number) => {
|
||||||
row.forEach((tile: string, x: number) => {
|
row.forEach((tile: string, x: number) => {
|
||||||
placeTile(zone, layer, x, y, tile)
|
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)
|
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-box-outer.svg" class="absolute w-full h-full" />
|
|
||||||
<img src="/assets/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-box-outer.svg" class="absolute w-full h-full" />
|
|
||||||
<img src="/assets/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 axios from 'axios'
|
||||||
import config from '@/config'
|
import config from '@/config'
|
||||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
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 {
|
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)
|
useCookies().set('token', response.data.token as string)
|
||||||
return { success: true, token: response.data.token }
|
return { success: true, token: response.data.token }
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
if (typeof error.response.data === 'undefined') {
|
||||||
|
return { error: 'Could not connect to server' }
|
||||||
|
}
|
||||||
return { error: error.response.data.message }
|
return { error: error.response.data.message }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -16,12 +20,34 @@ export async function login(username: string, password: string) {
|
|||||||
try {
|
try {
|
||||||
const response = await axios.post(`${config.server_endpoint}/login`, { username, password })
|
const response = await axios.post(`${config.server_endpoint}/login`, { username, password })
|
||||||
useCookies().set('token', response.data.token as string, {
|
useCookies().set('token', response.data.token as string, {
|
||||||
// for whole domain
|
domain: getDomain()
|
||||||
// @TODO : #190
|
|
||||||
// domain: window.location.hostname.split('.').slice(-2).join('.')
|
|
||||||
})
|
})
|
||||||
return { success: true, token: response.data.token }
|
return { success: true, token: response.data.token }
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return { error: error.response.data.message }
|
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,36 +1,49 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { io, Socket } from 'socket.io-client'
|
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 config from '@/config'
|
||||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||||
|
import { getDomain } from '@/utilities'
|
||||||
|
|
||||||
export const useGameStore = defineStore('game', {
|
export const useGameStore = defineStore('game', {
|
||||||
state: () => {
|
state: () => {
|
||||||
return {
|
return {
|
||||||
notifications: [] as Notification[],
|
notifications: [] as Notification[],
|
||||||
isAssetsLoaded: false,
|
|
||||||
loadedAssets: [] as string[],
|
|
||||||
token: '' as string | null,
|
token: '' as string | null,
|
||||||
connection: null as Socket | null,
|
connection: null as Socket | null,
|
||||||
user: null as User | null,
|
user: null as User | null,
|
||||||
character: null as Character | null,
|
character: null as Character | null,
|
||||||
isPlayerDraggingCamera: false,
|
|
||||||
world: {
|
world: {
|
||||||
date: new Date(),
|
date: new Date(),
|
||||||
isRainEnabled: false,
|
isRainEnabled: false,
|
||||||
isFogEnabled: false,
|
isFogEnabled: false,
|
||||||
fogDensity: 0.5
|
fogDensity: 0.5
|
||||||
} as WorldSettings,
|
} 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
|
isCameraFollowingCharacter: false
|
||||||
},
|
},
|
||||||
uiSettings: {
|
uiSettings: {
|
||||||
isChatOpen: false,
|
isChatOpen: false,
|
||||||
isUserPanelOpen: false,
|
isCharacterProfileOpen: false,
|
||||||
isGmPanelOpen: false
|
isGmPanelOpen: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
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: {
|
actions: {
|
||||||
addNotification(notification: Notification) {
|
addNotification(notification: Notification) {
|
||||||
if (!notification.id) {
|
if (!notification.id) {
|
||||||
@ -53,23 +66,14 @@ export const useGameStore = defineStore('game', {
|
|||||||
toggleGmPanel() {
|
toggleGmPanel() {
|
||||||
this.uiSettings.isGmPanelOpen = !this.uiSettings.isGmPanelOpen
|
this.uiSettings.isGmPanelOpen = !this.uiSettings.isGmPanelOpen
|
||||||
},
|
},
|
||||||
togglePlayerDraggingCamera() {
|
|
||||||
this.isPlayerDraggingCamera = !this.isPlayerDraggingCamera
|
|
||||||
},
|
|
||||||
setPlayerDraggingCamera(moving: boolean) {
|
setPlayerDraggingCamera(moving: boolean) {
|
||||||
this.isPlayerDraggingCamera = moving
|
this.game.isPlayerDraggingCamera = moving
|
||||||
},
|
|
||||||
toggleCameraFollowingCharacter() {
|
|
||||||
this.gameSettings.isCameraFollowingCharacter = !this.gameSettings.isCameraFollowingCharacter
|
|
||||||
},
|
|
||||||
setCameraFollowingCharacter(following: boolean) {
|
|
||||||
this.gameSettings.isCameraFollowingCharacter = following
|
|
||||||
},
|
},
|
||||||
toggleChat() {
|
toggleChat() {
|
||||||
this.uiSettings.isChatOpen = !this.uiSettings.isChatOpen
|
this.uiSettings.isChatOpen = !this.uiSettings.isChatOpen
|
||||||
},
|
},
|
||||||
toggleUserPanel() {
|
toggleCharacterProfile() {
|
||||||
this.uiSettings.isUserPanelOpen = !this.uiSettings.isUserPanelOpen
|
this.uiSettings.isCharacterProfileOpen = !this.uiSettings.isCharacterProfileOpen
|
||||||
},
|
},
|
||||||
initConnection() {
|
initConnection() {
|
||||||
this.connection = io(config.server_endpoint, {
|
this.connection = io(config.server_endpoint, {
|
||||||
@ -101,21 +105,22 @@ export const useGameStore = defineStore('game', {
|
|||||||
this.connection?.disconnect()
|
this.connection?.disconnect()
|
||||||
|
|
||||||
useCookies().remove('token', {
|
useCookies().remove('token', {
|
||||||
// for whole domain
|
domain: getDomain()
|
||||||
// @TODO : #190
|
|
||||||
// domain: window.location.hostname.split('.').slice(-2).join('.')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
this.isAssetsLoaded = false
|
|
||||||
this.connection = null
|
this.connection = null
|
||||||
this.token = null
|
this.token = null
|
||||||
this.user = null
|
this.user = null
|
||||||
this.character = null
|
this.character = null
|
||||||
this.uiSettings.isGmPanelOpen = false
|
|
||||||
this.isPlayerDraggingCamera = false
|
this.game.isLoaded = false
|
||||||
this.gameSettings.isCameraFollowingCharacter = false
|
this.game.loadedAssets = []
|
||||||
|
this.game.isPlayerDraggingCamera = false
|
||||||
|
this.game.isCameraFollowingCharacter = false
|
||||||
|
|
||||||
this.uiSettings.isChatOpen = false
|
this.uiSettings.isChatOpen = false
|
||||||
this.uiSettings.isUserPanelOpen = false
|
this.uiSettings.isCharacterProfileOpen = false
|
||||||
|
this.uiSettings.isGmPanelOpen = false
|
||||||
|
|
||||||
this.world.date = new Date()
|
this.world.date = new Date()
|
||||||
this.world.isRainEnabled = false
|
this.world.isRainEnabled = false
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
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 = {
|
export type TeleportSettings = {
|
||||||
toZoneId: number
|
toZoneId: number
|
||||||
@ -20,14 +20,14 @@ export const useZoneEditorStore = defineStore('zoneEditor', {
|
|||||||
zoneList: [] as Zone[],
|
zoneList: [] as Zone[],
|
||||||
tileList: [] as Tile[],
|
tileList: [] as Tile[],
|
||||||
objectList: [] as Object[],
|
objectList: [] as Object[],
|
||||||
selectedTile: null as Tile | null,
|
selectedTile: '',
|
||||||
selectedObject: null as Object | null,
|
selectedObject: null as Object | null,
|
||||||
objectDepth: 0,
|
|
||||||
isTileListModalShown: false,
|
isTileListModalShown: false,
|
||||||
isObjectListModalShown: false,
|
isObjectListModalShown: false,
|
||||||
isZoneListModalShown: false,
|
isZoneListModalShown: false,
|
||||||
isCreateZoneModalShown: false,
|
isCreateZoneModalShown: false,
|
||||||
isSettingsModalShown: false,
|
isSettingsModalShown: false,
|
||||||
|
shouldClearTiles: false,
|
||||||
zoneSettings: {
|
zoneSettings: {
|
||||||
name: '',
|
name: '',
|
||||||
width: 0,
|
width: 0,
|
||||||
@ -88,15 +88,12 @@ export const useZoneEditorStore = defineStore('zoneEditor', {
|
|||||||
setObjectList(objects: Object[]) {
|
setObjectList(objects: Object[]) {
|
||||||
this.objectList = objects
|
this.objectList = objects
|
||||||
},
|
},
|
||||||
setSelectedTile(tile: Tile) {
|
setSelectedTile(tile: string) {
|
||||||
this.selectedTile = tile
|
this.selectedTile = tile
|
||||||
},
|
},
|
||||||
setSelectedObject(object: any) {
|
setSelectedObject(object: Object) {
|
||||||
this.selectedObject = object
|
this.selectedObject = object
|
||||||
},
|
},
|
||||||
setObjectDepth(depth: number) {
|
|
||||||
this.objectDepth = depth
|
|
||||||
},
|
|
||||||
toggleSettingsModal() {
|
toggleSettingsModal() {
|
||||||
this.isSettingsModalShown = !this.isSettingsModalShown
|
this.isSettingsModalShown = !this.isSettingsModalShown
|
||||||
},
|
},
|
||||||
@ -110,6 +107,13 @@ export const useZoneEditorStore = defineStore('zoneEditor', {
|
|||||||
setTeleportSettings(teleportSettings: TeleportSettings) {
|
setTeleportSettings(teleportSettings: TeleportSettings) {
|
||||||
this.teleportSettings = teleportSettings
|
this.teleportSettings = teleportSettings
|
||||||
},
|
},
|
||||||
|
triggerClearTiles() {
|
||||||
|
this.shouldClearTiles = true
|
||||||
|
},
|
||||||
|
|
||||||
|
resetClearTilesFlag() {
|
||||||
|
this.shouldClearTiles = false
|
||||||
|
},
|
||||||
reset(resetZone = false) {
|
reset(resetZone = false) {
|
||||||
if (resetZone) this.zone = null
|
if (resetZone) this.zone = null
|
||||||
this.zoneList = []
|
this.zoneList = []
|
||||||
@ -117,12 +121,14 @@ export const useZoneEditorStore = defineStore('zoneEditor', {
|
|||||||
this.objectList = []
|
this.objectList = []
|
||||||
this.tool = 'move'
|
this.tool = 'move'
|
||||||
this.drawMode = 'tile'
|
this.drawMode = 'tile'
|
||||||
this.selectedTile = null
|
this.selectedTile = ''
|
||||||
this.selectedObject = null
|
this.selectedObject = null
|
||||||
this.objectDepth = 0
|
this.isTileListModalShown = false
|
||||||
|
this.isObjectListModalShown = false
|
||||||
this.isSettingsModalShown = false
|
this.isSettingsModalShown = false
|
||||||
this.isZoneListModalShown = false
|
this.isZoneListModalShown = false
|
||||||
this.isCreateZoneModalShown = false
|
this.isCreateZoneModalShown = false
|
||||||
|
this.shouldClearTiles = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
12
src/types.ts
@ -4,11 +4,12 @@ export type Notification = {
|
|||||||
message?: string
|
message?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AssetT = {
|
export type AssetDataT = {
|
||||||
key: string
|
key: string
|
||||||
url: string
|
data: string
|
||||||
group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
|
group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
|
isAnimated?: boolean
|
||||||
frameCount?: number
|
frameCount?: number
|
||||||
frameWidth?: number
|
frameWidth?: number
|
||||||
frameHeight?: number
|
frameHeight?: number
|
||||||
@ -223,3 +224,10 @@ export type WorldSettings = {
|
|||||||
isFogEnabled: boolean
|
isFogEnabled: boolean
|
||||||
fogDensity: number
|
fogDensity: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type WeatherState = {
|
||||||
|
isRainEnabled: boolean
|
||||||
|
rainPercentage: number
|
||||||
|
isFogEnabled: boolean
|
||||||
|
fogDensity: number
|
||||||
|
}
|
||||||
|
@ -1,3 +1,25 @@
|
|||||||
export function uuidv4() {
|
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))
|
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 { defineConfig } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue';
|
import vue from '@vitejs/plugin-vue';
|
||||||
import VueDevTools from 'vite-plugin-vue-devtools'
|
import VueDevTools from 'vite-plugin-vue-devtools'
|
||||||
|
import viteCompression from 'vite-plugin-compression';
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
VueDevTools(),
|
VueDevTools(),
|
||||||
|
viteCompression()
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|