Compare commits
187 Commits
feature/ch
...
feature/ca
Author | SHA1 | Date | |
---|---|---|---|
9de7af961e | |||
4067ec2585 | |||
fb18841c91 | |||
7b1dcf7ce3 | |||
7546116878 | |||
03fef60621 | |||
574777da80 | |||
c2db9b5469 | |||
6e30a8530a | |||
41f82897a8 | |||
37b97b0aac | |||
c1d9cc3a11 | |||
b54b825422 | |||
0142850983 | |||
2d09715dc4 | |||
ef807982a5 | |||
ae0841889b | |||
bdd2f93175 | |||
10f6dc3802 | |||
700bd57e67 | |||
145143cdc5 | |||
201853a3ec | |||
40c87f0ee3 | |||
736ddddc54 | |||
63758e67b3 | |||
b51aa29bd8 | |||
f9bfbdf735 | |||
2abce7a7e7 | |||
6ec9f8a7bc | |||
8191a039c9 | |||
540425ca44 | |||
1c2e642fe3 | |||
8355c83dc8 | |||
5fcb336835 | |||
90bdf43b64 | |||
e9dfcf7870 | |||
d0c08c25fd | |||
7bb7af9476 | |||
e4186a1bf5 | |||
8c664d7774 | |||
744df2e2dc | |||
b4f9b11143 | |||
18b07d2f46 | |||
9d0f810ab3 | |||
cf3f17dfef | |||
6be1134c8c | |||
6dad7bc9dd | |||
231f19a30f | |||
9c105d6df6 | |||
179ceb0ca0 | |||
680661f07c | |||
c54d2a2da8 | |||
85f0fca2ae | |||
420e63b724 | |||
5d9b4fd19a | |||
b3d68ef562 | |||
baae737d6b | |||
03f8b327c5 | |||
b9a1ce5ab5 | |||
1b650bd733 | |||
b867250580 | |||
2c7a1e27be | |||
0e455f8ffc | |||
8005bc1318 | |||
11e978121f | |||
727ca99b73 | |||
97080d7380 | |||
1a3a53a229 | |||
a926de8466 | |||
5eabb39ec8 | |||
03313cb092 | |||
d58cfa668d | |||
e3e40dd083 | |||
facdd2d1b4 | |||
7d6bd39f29 | |||
608932300f | |||
b5c1c92b04 | |||
c68b129da8 | |||
963c593a1f | |||
a299e22f88 | |||
2007bfd7c5 | |||
4cae045d0d | |||
1fa8b8f06e | |||
4095184b27 | |||
857d56a878 | |||
8087f754b0 | |||
1479d96162 | |||
606e220a9f | |||
6988565484 | |||
fbc4a3dcdb | |||
924d5bdd13 | |||
25a2fd24f3 | |||
64f5ac45dd | |||
937ce939d1 | |||
7d89364104 | |||
f7b8c235d8 | |||
89d83efca4 | |||
ab97e27f27 | |||
ee3e1b55cb | |||
5e109e2a39 | |||
a8e50c993a | |||
ba8af589a7 | |||
301340327a | |||
1e4c58c79e | |||
f87cd063ee | |||
9593298389 | |||
ad4651844d | |||
3748c459f8 | |||
50ea3ecdab | |||
8910390f7b | |||
d820490b2b | |||
2c96caee4f | |||
84939a7d32 | |||
1e3fc2b0f8 | |||
7c8b5f3e82 | |||
570d315bf5 | |||
7871b34c60 | |||
85d64f23eb | |||
bdb6dd0d54 | |||
faf887163a | |||
dd5baa530d | |||
d9947e29cf | |||
1888521762 | |||
48fef2313b | |||
0a99d2c430 | |||
ed6f592606 | |||
46ebfaec01 | |||
1384f50406 | |||
d71f4e7b59 | |||
58929290ab | |||
63146106c0 | |||
7c5602f204 | |||
e711e124ce | |||
e1b39c42ec | |||
d81c889426 | |||
afb0edacf6 | |||
6d7d568746 | |||
8df5b6eb76 | |||
270d12821a | |||
9c244e980c | |||
25ba54c8ac | |||
9c4bef864b | |||
bdc566e30f | |||
a653b61b51 | |||
7b61f71fa9 | |||
42539cc73d | |||
864369860c | |||
89938b7f93 | |||
f076661d4a | |||
0b018f53a7 | |||
32a13f0f3c | |||
f203bf9534 | |||
67b6339ffc | |||
6233da0044 | |||
56f455a08e | |||
9d541cc46a | |||
adf86d369b | |||
bbcb84ed03 | |||
6f40c774ea | |||
e3b70df46f | |||
584262a59b | |||
7e4e613405 | |||
e2c60bfd5a | |||
e38c402266 | |||
a299d5dc55 | |||
43c0f0ab1e | |||
ed17e7f16e | |||
7d723530e6 | |||
a3532a5940 | |||
74cbf3f2c8 | |||
d402744955 | |||
39b65b3884 | |||
c62ff2efc1 | |||
08f55c9680 | |||
1afc50ea6a | |||
7c259f455c | |||
be854a79b8 | |||
a71890ab68 | |||
dc2b6b9851 | |||
d091aabeb3 | |||
c261937cf5 | |||
4aa1309797 | |||
4f8517a50c | |||
446e049e6e | |||
7db2ba322c | |||
70fb732051 | |||
5128aa83f9 |
@ -1,5 +1,5 @@
|
|||||||
VITE_NAME=New Quest
|
VITE_NAME=Noxious
|
||||||
VITE_DEVELOPMENT=true
|
VITE_DEVELOPMENT=true
|
||||||
VITE_SERVER_ENDPOINT=http://localhost:4000
|
VITE_SERVER_ENDPOINT=http://localhost:4000
|
||||||
VITE_TILE_SIZE_X=64
|
VITE_TILE_SIZE_WIDTH=64
|
||||||
VITE_TILE_SIZE_Y=32
|
VITE_TILE_SIZE_HEIGHT=32
|
@ -4,5 +4,8 @@
|
|||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"printWidth": 300,
|
"printWidth": 300,
|
||||||
"trailingComma": "none"
|
"trailingComma": "none",
|
||||||
|
"plugins": ["@ianvs/prettier-plugin-sort-imports"],
|
||||||
|
"importOrderParserPlugins": ["typescript", "jsx", "decorators-legacy", "classProperties"],
|
||||||
|
"importOrderCaseSensitive": false
|
||||||
}
|
}
|
@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/favicon.ico">
|
<link rel="icon" href="/favicon.ico">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<title>Sylvan Quest - Play</title>
|
<title>Noxious - Play</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
2682
package-lock.json
generated
@ -27,6 +27,7 @@
|
|||||||
"zod": "^3.22.2"
|
"zod": "^3.22.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@ianvs/prettier-plugin-sort-imports": "^4.4.0",
|
||||||
"@rushstack/eslint-patch": "^1.10.3",
|
"@rushstack/eslint-patch": "^1.10.3",
|
||||||
"@tsconfig/node20": "^20.1.4",
|
"@tsconfig/node20": "^20.1.4",
|
||||||
"@types/jsdom": "^21.1.7",
|
"@types/jsdom": "^21.1.7",
|
||||||
@ -37,7 +38,6 @@
|
|||||||
"@vue/test-utils": "^2.4.6",
|
"@vue/test-utils": "^2.4.6",
|
||||||
"@vue/tsconfig": "^0.5.1",
|
"@vue/tsconfig": "^0.5.1",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"easystarjs": "^0.4.4",
|
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-plugin-vue": "^9.27.0",
|
"eslint-plugin-vue": "^9.27.0",
|
||||||
"jsdom": "^24.1.1",
|
"jsdom": "^24.1.1",
|
||||||
@ -50,7 +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-vue-devtools": "^7.5.2",
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vitest": "^2.0.3",
|
"vitest": "^2.0.3",
|
||||||
"vue-tsc": "^1.6.5"
|
"vue-tsc": "^1.6.5"
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 162 KiB |
@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M11 16L15 12M15 12L11 8M15 12H3M4.51555 17C6.13007 19.412 8.87958 21 12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3C8.87958 3 6.13007 4.58803 4.51555 7" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 453 B |
Before Width: | Height: | Size: 597 KiB After Width: | Height: | Size: 597 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 600 KiB After Width: | Height: | Size: 600 KiB |
Before Width: | Height: | Size: 600 KiB After Width: | Height: | Size: 600 KiB |
Before Width: | Height: | Size: 599 KiB After Width: | Height: | Size: 599 KiB |
10
public/assets/icons/male-icon.svg
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_598_541)">
|
||||||
|
<path d="M10.0327 5.69111L12.3905 3.33333H9.33333V2H14.6667V7.33333H13.3333V4.27614L10.9755 6.63392C11.6183 7.47513 12 8.52633 12 9.66667C12 12.4281 9.7614 14.6667 7 14.6667C4.23857 14.6667 2 12.4281 2 9.66667C2 6.90527 4.23857 4.66667 7 4.66667C8.14033 4.66667 9.19153 5.04843 10.0327 5.69111ZM7 13.3333C9.02507 13.3333 10.6667 11.6917 10.6667 9.66667C10.6667 7.6416 9.02507 6 7 6C4.97495 6 3.33333 7.6416 3.33333 9.66667C3.33333 11.6917 4.97495 13.3333 7 13.3333Z" fill="#999999"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_598_541">
|
||||||
|
<rect width="16" height="16" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 729 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 325 B After Width: | Height: | Size: 325 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 847 B After Width: | Height: | Size: 847 B |
Before Width: | Height: | Size: 745 B After Width: | Height: | Size: 745 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 915 B After Width: | Height: | Size: 918 B |
Before Width: | Height: | Size: 768 B After Width: | Height: | Size: 768 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 375 B After Width: | Height: | Size: 375 B |
Before Width: | Height: | Size: 652 B After Width: | Height: | Size: 652 B |
Before Width: | Height: | Size: 346 B After Width: | Height: | Size: 346 B |
@ -1 +0,0 @@
|
|||||||
<svg width="48" xmlns="http://www.w3.org/2000/svg" height="48" id="screenshot-e9346f42-72c8-800c-8004-507b356b7f18" viewBox="0 0 48 48" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-e9346f42-72c8-800c-8004-507b356b7f18" width="1em" height="1em" rx="0" ry="0" style="fill: rgb(0, 0, 0);"><g id="shape-e9346f42-72c8-800c-8004-507b356b7f1b"><defs style="fill: rgb(0, 0, 0);"/></g><g id="shape-e9346f42-72c8-800c-8004-507b356b7f1c"><defs><mask width="1.2" height="1.2" x="-0.1" id="render-3-ipSEnterKey0" data-old-y="-0.1" data-old-width="1.2" data-old-x="-0.1" y="-0.1" data-old-height="1.2"><g class="svg-mask-wrapper" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"><path fill="#fff" stroke="#fff" d="M44 44V4H24v16H4v24z"/><path stroke="#000" d="m21 28l-4 4l4 4"/><path stroke="#000" d="M34 23v9H17"/></g></g></mask></defs><g class="fills" id="fills-e9346f42-72c8-800c-8004-507b356b7f1c"><path d="M0.000,0.000L48.000,0.000L48.000,48.000L0.000,48.000ZZ" mask="url(#render-3-ipSEnterKey0)" style="fill: #696969; fill-opacity: 1;"/></g></g></g></svg>
|
|
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 2.1 KiB |
3
public/assets/icons/triangle-icon.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="13" height="16" viewBox="0 0 13 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M1.00044 7.42339L12.0005 1.07252C12.3193 0.888385 12.7271 0.997653 12.9111 1.31652C12.9697 1.41785 13.0005 1.53285 13.0005 1.64985L13.0005 14.3516C13.0005 14.7198 12.702 15.0182 12.3338 15.0182C12.2167 15.0182 12.1018 14.9874 12.0005 14.9289L1.00044 8.57805C0.681573 8.39399 0.572326 7.98625 0.756419 7.66739C0.814932 7.56605 0.899092 7.48185 1.00044 7.42339Z" fill="#4D4D4D"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 490 B |
3
public/assets/icons/x-button-gray.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8.5859 10.0001L0.792969 2.20718L2.20718 0.792969L10.0001 8.58582L17.793 0.792969L19.2072 2.20718L11.4143 10.0001L19.2072 17.7929L17.793 19.2072L10.0001 11.4143L2.20718 19.2072L0.792969 17.7929L8.5859 10.0001Z" fill="#999999"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 340 B |
Before Width: | Height: | Size: 109 B After Width: | Height: | Size: 109 B |
Before Width: | Height: | Size: 696 B After Width: | Height: | Size: 696 B |
Before Width: | Height: | Size: 708 B After Width: | Height: | Size: 708 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 274 B |
@ -1 +0,0 @@
|
|||||||
<svg width="170" xmlns="http://www.w3.org/2000/svg" height="275" id="screenshot-c7008730-586b-8052-8004-79a2f5807f5c" viewBox="0 0 170 275" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-c7008730-586b-8052-8004-79a2f5807f5c"><defs><linearGradient id="fill-color-gradient-render-1-0" x1="0.4238862507773219" y1="0.26897966364321624" x2="0.7513711107948178" y2="0.9146156628521559" gradientTransform=""><stop offset="0" stop-color="#ffffff" stop-opacity="0.4"/><stop offset="1" stop-color="#ffffff" stop-opacity="0"/></linearGradient><pattern patternUnits="userSpaceOnUse" x="0" y="0" width="170" height="275" patternTransform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)" id="fill-0-render-1"><g><rect width="170" height="275" style="fill: url("#fill-color-gradient-render-1-0");"/></g></pattern></defs><g class="fills" id="fills-c7008730-586b-8052-8004-79a2f5807f5c"><path d="M170.000,20.000L170.000,255.000C170.000,266.038,161.038,275.000,150.000,275.000L20.000,275.000C8.962,275.000,0.000,266.038,0.000,255.000L0.000,20.000C0.000,8.962,8.962,0.000,20.000,0.000L150.000,0.000C150.000,0.000,150.000,0.000,150.000,0.000C150.000,11.038,158.962,20.000,170.000,20.000Z" fill="url(#fill-0-render-1)"/></g><g id="strokes-c7008730-586b-8052-8004-79a2f5807f5c" class="strokes"><g class="inner-stroke-shape"><defs><clipPath id="inner-stroke-render-1-c7008730-586b-8052-8004-79a2f5807f5c-0"><use href="#stroke-shape-render-1-c7008730-586b-8052-8004-79a2f5807f5c-0"/></clipPath><path d="M170.000,20.000L170.000,255.000C170.000,266.038,161.038,275.000,150.000,275.000L20.000,275.000C8.962,275.000,0.000,266.038,0.000,255.000L0.000,20.000C0.000,8.962,8.962,0.000,20.000,0.000L150.000,0.000C150.000,0.000,150.000,0.000,150.000,0.000C150.000,11.038,158.962,20.000,170.000,20.000Z" id="stroke-shape-render-1-c7008730-586b-8052-8004-79a2f5807f5c-0" style="fill: none; stroke-width: 4; stroke: rgb(255, 255, 255); stroke-opacity: 1;"/></defs><use href="#stroke-shape-render-1-c7008730-586b-8052-8004-79a2f5807f5c-0" clip-path="url('#inner-stroke-render-1-c7008730-586b-8052-8004-79a2f5807f5c-0')"/></g></g></g></svg>
|
|
Before Width: | Height: | Size: 2.1 KiB |
@ -1 +0,0 @@
|
|||||||
<svg width="290" xmlns="http://www.w3.org/2000/svg" height="87" id="screenshot-e9942e24-155b-8096-8004-7eaff9882cd6" viewBox="0 0 290 87" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-e9942e24-155b-8096-8004-7eaff9882cd6"><g class="fills" id="fills-e9942e24-155b-8096-8004-7eaff9882cd6"><path d="M3.485,8.722C7.089,3.457,13.144,0.000,20.000,0.000L270.000,0.000C281.038,0.000,290.000,8.962,290.000,20.000L290.000,67.000C290.000,78.038,281.038,87.000,270.000,87.000L20.000,87.000C13.166,87.000,7.128,83.565,3.520,78.329C21.157,76.589,35.000,61.648,35.000,43.500C35.000,25.390,21.216,10.475,3.485,8.722ZM0.000,67.000L0.000,20.000"/></g><g id="strokes-e9942e24-155b-8096-8004-7eaff9882cd6" class="strokes"><g class="inner-stroke-shape"><defs><clipPath id="inner-stroke-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0"><use href="#stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0"/></clipPath><path d="M3.485,8.722C7.089,3.457,13.144,0.000,20.000,0.000L270.000,0.000C281.038,0.000,290.000,8.962,290.000,20.000L290.000,67.000C290.000,78.038,281.038,87.000,270.000,87.000L20.000,87.000C13.166,87.000,7.128,83.565,3.520,78.329C21.157,76.589,35.000,61.648,35.000,43.500C35.000,25.390,21.216,10.475,3.485,8.722ZM0.000,67.000L0.000,20.000" id="stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0" style="fill: none; stroke-width: 6; stroke: rgb(77 77 77); stroke-opacity: 1;"/></defs><use href="#stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0" clip-path="url('#inner-stroke-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0')"/></g></g></g></svg>
|
|
Before Width: | Height: | Size: 1.6 KiB |
@ -1 +0,0 @@
|
|||||||
<svg width="290" xmlns="http://www.w3.org/2000/svg" height="87" id="screenshot-e9942e24-155b-8096-8004-7eaff9882cd6" viewBox="0 0 290 87" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-e9942e24-155b-8096-8004-7eaff9882cd6"><g class="fills" id="fills-e9942e24-155b-8096-8004-7eaff9882cd6"><path d="M3.485,8.722C7.089,3.457,13.144,0.000,20.000,0.000L270.000,0.000C281.038,0.000,290.000,8.962,290.000,20.000L290.000,67.000C290.000,78.038,281.038,87.000,270.000,87.000L20.000,87.000C13.166,87.000,7.128,83.565,3.520,78.329C21.157,76.589,35.000,61.648,35.000,43.500C35.000,25.390,21.216,10.475,3.485,8.722ZM0.000,67.000L0.000,20.000" style="fill: rgb(127, 127, 127); fill-opacity: 0.7;"/></g><g id="strokes-e9942e24-155b-8096-8004-7eaff9882cd6" class="strokes"><g class="inner-stroke-shape"><defs><clipPath id="inner-stroke-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0"><use href="#stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0"/></clipPath><path d="M3.485,8.722C7.089,3.457,13.144,0.000,20.000,0.000L270.000,0.000C281.038,0.000,290.000,8.962,290.000,20.000L290.000,67.000C290.000,78.038,281.038,87.000,270.000,87.000L20.000,87.000C13.166,87.000,7.128,83.565,3.520,78.329C21.157,76.589,35.000,61.648,35.000,43.500C35.000,25.390,21.216,10.475,3.485,8.722ZM0.000,67.000L0.000,20.000" id="stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0" style="fill: none; stroke-width: 6; stroke: rgb(255, 255, 255); stroke-opacity: 1;"/></defs><use href="#stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0" clip-path="url('#inner-stroke-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0')"/></g></g></g></svg>
|
|
Before Width: | Height: | Size: 1.6 KiB |
@ -1 +0,0 @@
|
|||||||
<svg width="1508.086" xmlns="http://www.w3.org/2000/svg" height="1511.251" id="screenshot-0d120e2a-8725-8061-8004-79728483f7ea" viewBox="-201.784 -208.012 1508.086 1511.251" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-0d120e2a-8725-8061-8004-79728483f7ea" width="800px" height="800px" rx="0" ry="0" style="opacity: 0.3; fill: rgb(0, 0, 0);"><g id="shape-0d120e2a-8725-8061-8004-79728484b3fe"><g class="fills" id="fills-0d120e2a-8725-8061-8004-79728484b3fe"><path d="M1190.359,745.690L1043.367,575.945L1133.504,630.603C1099.180,585.722,1047.978,532.622,975.722,469.519L780.783,401.898L896.468,404.538C851.234,371.382,797.068,339.090,738.130,311.073L601.350,337.338L689.349,289.039C627.425,263.088,562.143,241.922,497.713,229.160C430.172,215.674,363.453,211.534,303.512,221.641L314.753,151.382C271.664,177.012,239.130,209.992,214.226,251.017L204.390,177.710C166.181,212.950,148.095,250.172,143.131,301.343C69.092,307.974,-2.300,327.925,-73.861,347.005L-63.628,384.898C361.675,238.903,753.109,407.667,987.467,615.054L960.305,643.506C749.259,458.743,490.332,358.712,193.406,380.541C209.110,415.226,228.858,447.126,251.288,474.785L371.998,430.671L277.417,504.449C294.635,521.771,312.591,536.670,331.111,548.765L396.081,470.998L358.366,564.253C377.625,573.924,397.183,580.217,416.674,582.837C534.164,599.232,652.310,618.566,782.785,703.535L773.618,601.955L831.163,737.792C852.261,754.489,876.370,771.191,897.168,782.701L861.169,684.190L960.409,811.519C976.589,817.512,992.991,822.477,1008.953,826.486C1066.083,840.287,1120.015,842.594,1157.256,819.002C1175.975,807.393,1189.205,786.963,1190.791,762.853C1191.080,756.933,1190.907,750.762,1190.359,745.690ZZ" style="fill: rgb(13 109 105);"/></g></g></g></svg>
|
|
Before Width: | Height: | Size: 1.7 KiB |
BIN
public/assets/tlogo.png
Normal file
After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 470 KiB After Width: | Height: | Size: 470 KiB |
Before Width: | Height: | Size: 471 KiB After Width: | Height: | Size: 471 KiB |
Before Width: | Height: | Size: 598 KiB After Width: | Height: | Size: 598 KiB |
25
public/assets/ui-elements/character-select-ui-shape.svg
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<svg width="190" height="202" viewBox="0 0 190 202" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0 8H8V0C7.40741 4.14815 4.14815 7.40741 0 8Z" fill="#1A1A1A"/>
|
||||||
|
<path d="M7.55 0V0C7.55 4.16975 4.16975 7.55 0 7.55V7.55" stroke="#4D4D4D"/>
|
||||||
|
<mask id="path-3-inside-1_632_705" fill="white">
|
||||||
|
<path d="M0 8H8V194H0V8Z"/>
|
||||||
|
</mask>
|
||||||
|
<path d="M0 8H8V194H0V8Z" fill="#1A1A1A"/>
|
||||||
|
<path d="M1 194V8H-1V194H1Z" fill="#4D4D4D" mask="url(#path-3-inside-1_632_705)"/>
|
||||||
|
<path d="M0 194H8V202C7.40741 197.852 4.14815 194.593 0 194Z" fill="#1A1A1A"/>
|
||||||
|
<path d="M7.55 202V202C7.55 197.83 4.16975 194.45 0 194.45V194.45" stroke="#4D4D4D"/>
|
||||||
|
<mask id="path-7-inside-2_632_705" fill="white">
|
||||||
|
<path d="M8 0H182V202H8V0Z"/>
|
||||||
|
</mask>
|
||||||
|
<path d="M8 0H182V202H8V0Z" fill="#1A1A1A"/>
|
||||||
|
<path d="M8 1H182V-1H8V1ZM182 201H8V203H182V201Z" fill="#4D4D4D" mask="url(#path-7-inside-2_632_705)"/>
|
||||||
|
<path d="M190 8H182V0C182.593 4.14815 185.852 7.40741 190 8Z" fill="#1A1A1A"/>
|
||||||
|
<path d="M182.45 0V0C182.45 4.16975 185.83 7.55 190 7.55V7.55" stroke="#4D4D4D"/>
|
||||||
|
<mask id="path-11-inside-3_632_705" fill="white">
|
||||||
|
<path d="M190 8H182V194H190V8Z"/>
|
||||||
|
</mask>
|
||||||
|
<path d="M190 8H182V194H190V8Z" fill="#1A1A1A"/>
|
||||||
|
<path d="M189 194V8H191V194H189Z" fill="#4D4D4D" mask="url(#path-11-inside-3_632_705)"/>
|
||||||
|
<path d="M190 194H182V202C182.593 197.852 185.852 194.593 190 194Z" fill="#1A1A1A"/>
|
||||||
|
<path d="M182.45 202V202C182.45 197.83 185.83 194.45 190 194.45V194.45" stroke="#4D4D4D"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 470 KiB After Width: | Height: | Size: 470 KiB |
23
public/assets/ui-elements/login-ui-box-inner.svg
Normal file
After Width: | Height: | Size: 302 KiB |
23
public/assets/ui-elements/login-ui-box-outer.svg
Normal file
After Width: | Height: | Size: 400 KiB |
Before Width: | Height: | Size: 453 KiB After Width: | Height: | Size: 453 KiB |
Before Width: | Height: | Size: 301 KiB |
Before Width: | Height: | Size: 400 KiB |
Before Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 2.1 MiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 1.1 MiB |
63
src/App.vue
@ -1,33 +1,66 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<Debug />
|
||||||
<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>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
|
||||||
import Notifications from '@/components/utilities/Notifications.vue'
|
|
||||||
import GmTools from '@/components/gameMaster/GmTools.vue'
|
|
||||||
import GmPanel from '@/components/gameMaster/GmPanel.vue'
|
import GmPanel from '@/components/gameMaster/GmPanel.vue'
|
||||||
import Login from '@/screens/Login.vue'
|
import Characters from '@/components/screens/Characters.vue'
|
||||||
import Characters from '@/screens/Characters.vue'
|
import Game from '@/components/screens/Game.vue'
|
||||||
import Game from '@/screens/Game.vue'
|
import Loading from '@/components/screens/Loading.vue'
|
||||||
// import Loading from '@/screens/Loading.vue'
|
import Login from '@/components/screens/Login.vue'
|
||||||
import ZoneEditor from '@/screens/ZoneEditor.vue'
|
import MapEditor from '@/components/screens/MapEditor.vue'
|
||||||
import { computed } from 'vue'
|
import BackgroundImageLoader from '@/components/utilities/BackgroundImageLoader.vue'
|
||||||
|
import Debug from '@/components/utilities/Debug.vue'
|
||||||
|
import Notifications from '@/components/utilities/Notifications.vue'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useMapEditorStore } from '@/stores/mapEditorStore'
|
||||||
|
import { computed, onMounted, onUnmounted, watch } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const mapEditorStore = useMapEditorStore()
|
||||||
|
|
||||||
const currentScreen = computed(() => {
|
const currentScreen = computed(() => {
|
||||||
// if (!gameStore.isAssetsLoaded) return Loading
|
if (!gameStore.game.isLoaded) 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 (mapEditorStore.active) return MapEditor
|
||||||
return Game
|
return Game
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Watch mapEditorStore.active and empty gameStore.game.loadedAssets
|
||||||
|
watch(
|
||||||
|
() => mapEditorStore.active,
|
||||||
|
() => {
|
||||||
|
gameStore.game.loadedTextures = []
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// #209: Play sound when a button is pressed
|
||||||
|
/**
|
||||||
|
* @TODO: Not all button-like elements will actually be a button, so we need to find a better way to do this
|
||||||
|
*/
|
||||||
|
addEventListener('click', (event) => {
|
||||||
|
if (!(event.target instanceof HTMLButtonElement)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const audio = new Audio('/assets/music/click-btn.mp3')
|
||||||
|
audio.play()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Watch for "G" key press and toggle the gm panel
|
||||||
|
addEventListener('keydown', (event) => {
|
||||||
|
if (gameStore.character?.role !== 'gm') return // Only allow toggling the gm panel if the character is a gm
|
||||||
|
|
||||||
|
// Check if no input is active or focus is on an input
|
||||||
|
if (event.repeat || event.isComposing || event.defaultPrevented || document.activeElement?.tagName.toUpperCase() === 'INPUT' || document.activeElement?.tagName.toUpperCase() === 'TEXTAREA') return
|
||||||
|
|
||||||
|
if (event.key === 'G') {
|
||||||
|
gameStore.toggleGmPanel()
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -3,7 +3,7 @@ export default {
|
|||||||
development: import.meta.env.VITE_DEVELOPMENT === 'true',
|
development: import.meta.env.VITE_DEVELOPMENT === 'true',
|
||||||
server_endpoint: import.meta.env.VITE_SERVER_ENDPOINT,
|
server_endpoint: import.meta.env.VITE_SERVER_ENDPOINT,
|
||||||
tile_size: {
|
tile_size: {
|
||||||
x: Number(import.meta.env.VITE_TILE_SIZE_X),
|
width: Number(import.meta.env.VITE_TILE_SIZE_WIDTH),
|
||||||
y: Number(import.meta.env.VITE_TILE_SIZE_Y)
|
height: Number(import.meta.env.VITE_TILE_SIZE_HEIGHT)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,118 +1,128 @@
|
|||||||
|
export type UUID = `${string}-${string}-${string}-${string}-${string}`
|
||||||
|
|
||||||
export type Notification = {
|
export type Notification = {
|
||||||
id?: string
|
id?: string
|
||||||
title?: string
|
title?: string
|
||||||
message?: string
|
message?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AssetT = {
|
export type HttpResponse<T> = {
|
||||||
|
success: boolean
|
||||||
|
message?: string
|
||||||
|
data?: T
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TextureData = {
|
||||||
key: string
|
key: string
|
||||||
url: string
|
data: string // URL or Base64 encoded blob
|
||||||
group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
|
group: 'tiles' | 'map_objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
frameCount?: number
|
originX?: number
|
||||||
|
originY?: number
|
||||||
|
isAnimated?: boolean
|
||||||
|
frameRate?: number
|
||||||
frameWidth?: number
|
frameWidth?: number
|
||||||
frameHeight?: number
|
frameHeight?: number
|
||||||
|
frameCount?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Tile = {
|
export type Tile = {
|
||||||
id: string
|
id: UUID
|
||||||
name: string
|
name: string
|
||||||
tags: any | null
|
tags: any | null
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Object = {
|
export type MapObject = {
|
||||||
id: string
|
id: UUID
|
||||||
name: string
|
name: string
|
||||||
tags: any | null
|
tags: any | null
|
||||||
originX: number
|
originX: number
|
||||||
originY: number
|
originY: number
|
||||||
isAnimated: boolean
|
isAnimated: boolean
|
||||||
frameSpeed: number
|
frameRate: number
|
||||||
frameWidth: number
|
frameWidth: number
|
||||||
frameHeight: number
|
frameHeight: number
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
ZoneObject: ZoneObject[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Item = {
|
export type Item = {
|
||||||
id: string
|
id: UUID
|
||||||
name: string
|
name: string
|
||||||
description: string | null
|
description: string | null
|
||||||
|
itemType: ItemType
|
||||||
stackable: boolean
|
stackable: boolean
|
||||||
|
rarity: ItemRarity
|
||||||
|
sprite?: Sprite
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
characters: CharacterItem[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Zone = {
|
export type ItemType = 'WEAPON' | 'HELMET' | 'CHEST' | 'LEGS' | 'BOOTS' | 'GLOVES' | 'RING' | 'NECKLACE'
|
||||||
id: number
|
export type ItemRarity = 'COMMON' | 'UNCOMMON' | 'RARE' | 'EPIC' | 'LEGENDARY'
|
||||||
|
|
||||||
|
export type Map = {
|
||||||
|
id: UUID
|
||||||
name: string
|
name: string
|
||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
tiles: any | null
|
tiles: any | null
|
||||||
pvp: boolean
|
pvp: boolean
|
||||||
zoneEffects: ZoneEffect[]
|
mapEffects: MapEffect[]
|
||||||
zoneEventTiles: ZoneEventTile[]
|
mapEventTiles: MapEventTile[]
|
||||||
zoneObjects: ZoneObject[]
|
placedMapObjects: PlacedMapObject[]
|
||||||
characters: Character[]
|
characters: Character[]
|
||||||
chats: Chat[]
|
chats: Chat[]
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ZoneEffect = {
|
export type MapEffect = {
|
||||||
id: string
|
id: UUID
|
||||||
zoneId: number
|
map: Map
|
||||||
zone: Zone
|
|
||||||
effect: string
|
effect: string
|
||||||
strength: number
|
strength: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ZoneObject = {
|
export type PlacedMapObject = {
|
||||||
id: string
|
id: UUID
|
||||||
zoneId: number
|
map: Map
|
||||||
zone: Zone
|
mapObject: MapObject
|
||||||
objectId: string
|
|
||||||
object: Object
|
|
||||||
depth: number
|
depth: number
|
||||||
isRotated: boolean
|
isRotated: boolean
|
||||||
positionX: number
|
positionX: number
|
||||||
positionY: number
|
positionY: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ZoneEventTileType {
|
export enum MapEventTileType {
|
||||||
BLOCK = 'BLOCK',
|
BLOCK = 'BLOCK',
|
||||||
TELEPORT = 'TELEPORT',
|
TELEPORT = 'TELEPORT',
|
||||||
NPC = 'NPC',
|
NPC = 'NPC',
|
||||||
ITEM = 'ITEM'
|
ITEM = 'ITEM'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ZoneEventTile = {
|
export type MapEventTile = {
|
||||||
id: string
|
id: UUID
|
||||||
zoneId: number
|
map: Map
|
||||||
zone: Zone
|
type: MapEventTileType
|
||||||
type: ZoneEventTileType
|
|
||||||
positionX: number
|
positionX: number
|
||||||
positionY: number
|
positionY: number
|
||||||
teleport?: ZoneEventTileTeleport
|
teleport?: MapEventTileTeleport
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ZoneEventTileTeleport = {
|
export type MapEventTileTeleport = {
|
||||||
id: string
|
id: UUID
|
||||||
zoneEventTileId: string
|
mapEventTile: MapEventTile
|
||||||
zoneEventTile: ZoneEventTile
|
toMap: Map
|
||||||
toZoneId: number
|
|
||||||
toZone: Zone
|
|
||||||
toPositionX: number
|
toPositionX: number
|
||||||
toPositionY: number
|
toPositionY: number
|
||||||
toRotation: number
|
toRotation: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
id: number
|
id: UUID
|
||||||
username: string
|
username: string
|
||||||
password: string
|
password: string
|
||||||
characters: Character[]
|
characters: Character[]
|
||||||
@ -132,20 +142,27 @@ export enum CharacterRace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type CharacterType = {
|
export type CharacterType = {
|
||||||
id: number
|
id: UUID
|
||||||
name: string
|
name: string
|
||||||
gender: CharacterGender
|
gender: CharacterGender
|
||||||
race: CharacterRace
|
race: CharacterRace
|
||||||
characters: Character[]
|
isSelectable: boolean
|
||||||
spriteId?: string
|
|
||||||
sprite?: Sprite
|
sprite?: Sprite
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CharacterHair = {
|
||||||
|
id: UUID
|
||||||
|
name: string
|
||||||
|
sprite?: Sprite
|
||||||
|
gender: CharacterGender
|
||||||
|
isSelectable: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export type Character = {
|
export type Character = {
|
||||||
id: number
|
id: UUID
|
||||||
userId: number
|
userId: UUID
|
||||||
user: User
|
user: User
|
||||||
name: string
|
name: string
|
||||||
hitpoints: number
|
hitpoints: number
|
||||||
@ -157,29 +174,43 @@ export type Character = {
|
|||||||
positionX: number
|
positionX: number
|
||||||
positionY: number
|
positionY: number
|
||||||
rotation: number
|
rotation: number
|
||||||
zoneId: number
|
characterType: UUID | null
|
||||||
zone: Zone
|
characterHair: UUID | null
|
||||||
characterTypeId: number | null
|
map: UUID
|
||||||
characterType: CharacterType | null
|
|
||||||
chats: Chat[]
|
chats: Chat[]
|
||||||
items: CharacterItem[]
|
items: CharacterItem[]
|
||||||
|
equipment: CharacterEquipment[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ExtendedCharacter = Character & {
|
export type MapCharacter = {
|
||||||
isMoving?: boolean
|
character: Character
|
||||||
|
isMoving: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CharacterItem = {
|
export type CharacterItem = {
|
||||||
id: number
|
id: UUID
|
||||||
characterId: number
|
|
||||||
character: Character
|
character: Character
|
||||||
itemId: string
|
|
||||||
item: Item
|
item: Item
|
||||||
quantity: number
|
quantity: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CharacterEquipment = {
|
||||||
|
id: UUID
|
||||||
|
slot: CharacterEquipmentSlotType
|
||||||
|
characterItem: CharacterItem
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CharacterEquipmentSlotType {
|
||||||
|
HEAD = 'HEAD',
|
||||||
|
BODY = 'BODY',
|
||||||
|
ARMS = 'ARMS',
|
||||||
|
LEGS = 'LEGS',
|
||||||
|
NECK = 'NECK',
|
||||||
|
RING = 'RING'
|
||||||
|
}
|
||||||
|
|
||||||
export type Sprite = {
|
export type Sprite = {
|
||||||
id: string
|
id: UUID
|
||||||
name: string
|
name: string
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
@ -188,8 +219,7 @@ export type Sprite = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type SpriteAction = {
|
export type SpriteAction = {
|
||||||
id: string
|
id: UUID
|
||||||
spriteId: string
|
|
||||||
sprite: Sprite
|
sprite: Sprite
|
||||||
action: string
|
action: string
|
||||||
sprites: string[]
|
sprites: string[]
|
||||||
@ -199,27 +229,32 @@ export type SpriteAction = {
|
|||||||
isLooping: boolean
|
isLooping: boolean
|
||||||
frameWidth: number
|
frameWidth: number
|
||||||
frameHeight: number
|
frameHeight: number
|
||||||
frameSpeed: number
|
frameRate: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Chat = {
|
export type Chat = {
|
||||||
id: number
|
id: UUID
|
||||||
characterId: number
|
|
||||||
character: Character
|
character: Character
|
||||||
zoneId: number
|
map: Map
|
||||||
zone: Zone
|
|
||||||
message: string
|
message: string
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ChatMessage = {
|
|
||||||
character: Character
|
|
||||||
message: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type WorldSettings = {
|
export type WorldSettings = {
|
||||||
date: Date
|
date: Date
|
||||||
isRainEnabled: boolean
|
isRainEnabled: boolean
|
||||||
isFogEnabled: boolean
|
isFogEnabled: boolean
|
||||||
fogDensity: number
|
fogDensity: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type WeatherState = {
|
||||||
|
isRainEnabled: boolean
|
||||||
|
rainPercentage: number
|
||||||
|
isFogEnabled: boolean
|
||||||
|
fogDensity: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type mapLoadData = {
|
||||||
|
mapId: UUID
|
||||||
|
characters: MapCharacter[]
|
||||||
|
}
|
25
src/application/utilities.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
export function uuidv4() {
|
||||||
|
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) => (+c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))).toString(16))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unduplicateArray(array: any[]) {
|
||||||
|
return [...new Set(array.flat())]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDomain() {
|
||||||
|
// Check if not localhost
|
||||||
|
if (window.location.hostname !== 'localhost') {
|
||||||
|
return window.location.hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if not IP address
|
||||||
|
if (window.location.hostname.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) {
|
||||||
|
return window.location.hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.location.hostname.split('.').length < 3) {
|
||||||
|
return window.location.hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.location.hostname.split('.').slice(-2).join('.')
|
||||||
|
}
|
@ -23,6 +23,14 @@ body {
|
|||||||
|
|
||||||
// Disable pinch zoom
|
// Disable pinch zoom
|
||||||
touch-action: pan-x pan-y;
|
touch-action: pan-x pan-y;
|
||||||
|
|
||||||
|
// Add custom focus outline
|
||||||
|
*:focus-visible {
|
||||||
|
@apply outline-gray-300;
|
||||||
|
@apply outline;
|
||||||
|
@apply outline-offset-2;
|
||||||
|
@apply rounded-sm;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
@ -45,7 +53,7 @@ label {
|
|||||||
|
|
||||||
button,
|
button,
|
||||||
a {
|
a {
|
||||||
@apply font-medium drop-shadow-20;
|
@apply font-medium;
|
||||||
}
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
@ -58,13 +66,20 @@ input {
|
|||||||
appearance: textfield;
|
appearance: textfield;
|
||||||
}
|
}
|
||||||
&[type='number']::-webkit-inner-spin-button,
|
&[type='number']::-webkit-inner-spin-button,
|
||||||
&[type='number']::-webkit-outer-spin-button {
|
&[type='number']::-webkit-outer-spin-button,
|
||||||
|
&[type='radio'] {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-field {
|
.input-field {
|
||||||
@apply px-4 py-2.5 text-base leading-5 focus-visible:outline-none bg-gray border border-solid border-gray-500 rounded text-gray-300;
|
@apply px-4 py-2.5 text-base leading-5 bg-gray border border-solid border-gray-500 rounded text-gray-300;
|
||||||
|
&:focus-visible {
|
||||||
|
@apply outline-none border-cyan rounded bg-gray-900;
|
||||||
|
}
|
||||||
|
&::placeholder {
|
||||||
|
@apply focus-visible:text-gray-300/50;
|
||||||
|
}
|
||||||
&.inactive {
|
&.inactive {
|
||||||
@apply bg-gray-600/50 hover:cursor-not-allowed;
|
@apply bg-gray-600/50 hover:cursor-not-allowed;
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
@ -99,11 +114,11 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.btn-red {
|
&.btn-red {
|
||||||
@apply bg-red text-gray-50 text-base leading-5 rounded py-2.5;
|
@apply bg-red-300 text-gray-50 text-base leading-5 rounded py-2.5;
|
||||||
|
|
||||||
&.active,
|
&.active,
|
||||||
&:hover {
|
&:hover {
|
||||||
@apply bg-red-300;
|
@apply bg-red-400;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,8 +126,9 @@ button {
|
|||||||
@apply text-gray-50 border-2 border-solid border-gray-500 text-base leading-5 rounded py-2.5;
|
@apply text-gray-50 border-2 border-solid border-gray-500 text-base leading-5 rounded py-2.5;
|
||||||
|
|
||||||
&.active,
|
&.active,
|
||||||
|
&.selected,
|
||||||
&:hover {
|
&:hover {
|
||||||
@apply bg-gray-700 border-gray-700;
|
@apply bg-gray border-gray;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,6 +139,28 @@ button {
|
|||||||
&.eye-open {
|
&.eye-open {
|
||||||
@apply bg-[url('/assets/icons/eye-closed.svg')] w-5 h-4 right-2.5;
|
@apply bg-[url('/assets/icons/eye-closed.svg')] w-5 h-4 right-2.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
@apply bg-gray-500 border-gray-400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.character.active {
|
||||||
|
@apply bg-gray bg-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hair-deselect:has(:checked) {
|
||||||
|
img {
|
||||||
|
@apply brightness-200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.default-border {
|
||||||
|
@apply border border-solid border-gray-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-element {
|
||||||
|
@apply absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-pixel {
|
.text-pixel {
|
||||||
@ -133,6 +171,15 @@ button {
|
|||||||
@apply hidden;
|
@apply hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scrollbar {
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
@apply block w-0.5 bg-gray-300 rounded-sm;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
@apply bg-gray-175;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
canvas {
|
canvas {
|
||||||
image-rendering: -moz-crisp-edges;
|
image-rendering: -moz-crisp-edges;
|
||||||
image-rendering: -webkit-crisp-edges;
|
image-rendering: -webkit-crisp-edges;
|
||||||
|
@ -1,110 +1,179 @@
|
|||||||
<template>
|
<template>
|
||||||
<Scene name="effects" @preload="preloadScene" @create="createScene" @update="updateScene"> </Scene>
|
<Scene name="effects" @preload="preloadScene" @create="createScene" @update="updateScene" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { WeatherState } from '@/application/types'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useMapStore } from '@/stores/mapStore'
|
||||||
import { Scene } from 'phavuer'
|
import { Scene } from 'phavuer'
|
||||||
import { useZoneStore } from '@/stores/zoneStore'
|
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { onBeforeUnmount, ref, watch } from 'vue'
|
|
||||||
|
|
||||||
const zoneStore = useZoneStore()
|
// Constants
|
||||||
|
const LIGHT_CONFIG = {
|
||||||
|
SUNRISE_HOUR: 6,
|
||||||
|
SUNSET_HOUR: 20,
|
||||||
|
DAY_STRENGTH: 100,
|
||||||
|
NIGHT_STRENGTH: 30
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stores and refs
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const mapStore = useMapStore()
|
||||||
const sceneRef = ref<Phaser.Scene | null>(null)
|
const sceneRef = ref<Phaser.Scene | null>(null)
|
||||||
|
const mapEffectsReady = ref(false)
|
||||||
|
|
||||||
// Effect-related refs
|
// Effect objects
|
||||||
const lightEffect = ref<Phaser.GameObjects.Graphics | null>(null)
|
const effects = {
|
||||||
const rainEmitter = ref<Phaser.GameObjects.Particles.ParticleEmitter | null>(null)
|
light: ref<Phaser.GameObjects.Graphics | null>(null),
|
||||||
const fogSprite = ref<Phaser.GameObjects.Sprite | null>(null)
|
rain: ref<Phaser.GameObjects.Particles.ParticleEmitter | null>(null),
|
||||||
|
fog: ref<Phaser.GameObjects.Sprite | null>(null)
|
||||||
|
}
|
||||||
|
|
||||||
const preloadScene = async (scene: Phaser.Scene) => {
|
// Weather state
|
||||||
|
const weatherState = ref<WeatherState>({
|
||||||
|
isRainEnabled: false,
|
||||||
|
rainPercentage: 0,
|
||||||
|
isFogEnabled: false,
|
||||||
|
fogDensity: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// Scene setup
|
||||||
|
const preloadScene = (scene: Phaser.Scene) => {
|
||||||
scene.load.image('raindrop', 'assets/raindrop.png')
|
scene.load.image('raindrop', 'assets/raindrop.png')
|
||||||
scene.load.image('fog', 'assets/fog.png')
|
scene.load.image('fog', 'assets/fog.png')
|
||||||
}
|
}
|
||||||
|
|
||||||
const createScene = async (scene: Phaser.Scene) => {
|
const createScene = (scene: Phaser.Scene) => {
|
||||||
sceneRef.value = scene
|
sceneRef.value = scene
|
||||||
createLightEffect(scene)
|
initializeEffects(scene)
|
||||||
createRainEffect(scene)
|
setupSocketListeners()
|
||||||
createFogEffect(scene)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const initializeEffects = (scene: Phaser.Scene) => {
|
||||||
|
// Light
|
||||||
|
effects.light.value = scene.add.graphics().setDepth(1000)
|
||||||
|
|
||||||
|
// Rain
|
||||||
|
effects.rain.value = scene.add
|
||||||
|
.particles(0, 0, 'raindrop', {
|
||||||
|
x: { min: 0, max: window.innerWidth },
|
||||||
|
y: -50,
|
||||||
|
quantity: 5,
|
||||||
|
lifespan: 2000,
|
||||||
|
speedY: { min: 300, max: 500 },
|
||||||
|
scale: { start: 0.005, end: 0.005 },
|
||||||
|
alpha: { start: 0.5, end: 0 },
|
||||||
|
blendMode: 'ADD'
|
||||||
|
})
|
||||||
|
.setDepth(900)
|
||||||
|
effects.rain.value.stop()
|
||||||
|
|
||||||
|
// Fog
|
||||||
|
effects.fog.value = scene.add
|
||||||
|
.sprite(window.innerWidth / 2, window.innerHeight / 2, 'fog')
|
||||||
|
.setScale(2)
|
||||||
|
.setAlpha(0)
|
||||||
|
.setDepth(950)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Effect updates
|
||||||
const updateScene = () => {
|
const updateScene = () => {
|
||||||
updateEffects()
|
const timeBasedLight = calculateLightStrength(gameStore.world.date)
|
||||||
}
|
const mapEffects = mapStore.map?.mapEffects?.reduce(
|
||||||
|
(acc, curr) => ({
|
||||||
|
...acc,
|
||||||
|
[curr.effect]: curr.strength
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
) as { [key: string]: number }
|
||||||
|
|
||||||
const createLightEffect = (scene: Phaser.Scene) => {
|
// Only update effects once mapEffects are loaded
|
||||||
lightEffect.value = scene.add.graphics()
|
if (!mapEffectsReady.value) {
|
||||||
lightEffect.value.setDepth(1000)
|
if (mapEffects && Object.keys(mapEffects).length) {
|
||||||
}
|
mapEffectsReady.value = true
|
||||||
|
} else {
|
||||||
const createRainEffect = (scene: Phaser.Scene) => {
|
return
|
||||||
rainEmitter.value = scene.add.particles(0, 0, 'raindrop', {
|
|
||||||
x: { min: 0, max: window.innerWidth },
|
|
||||||
y: -50,
|
|
||||||
quantity: 5,
|
|
||||||
lifespan: 2000,
|
|
||||||
speedY: { min: 300, max: 500 },
|
|
||||||
scale: { start: 0.005, end: 0.005 },
|
|
||||||
alpha: { start: 0.5, end: 0 },
|
|
||||||
blendMode: 'ADD'
|
|
||||||
})
|
|
||||||
rainEmitter.value.setDepth(900)
|
|
||||||
rainEmitter.value.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
const createFogEffect = (scene: Phaser.Scene) => {
|
|
||||||
fogSprite.value = scene.add.sprite(window.innerWidth / 2, window.innerHeight / 2, 'fog')
|
|
||||||
fogSprite.value.setScale(2)
|
|
||||||
fogSprite.value.setAlpha(0)
|
|
||||||
fogSprite.value.setDepth(950)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateEffects = () => {
|
|
||||||
const effects = zoneStore.zone?.zoneEffects || []
|
|
||||||
|
|
||||||
effects.forEach((effect) => {
|
|
||||||
switch (effect.effect) {
|
|
||||||
case 'light':
|
|
||||||
updateLightEffect(effect.strength)
|
|
||||||
break
|
|
||||||
case 'rain':
|
|
||||||
updateRainEffect(effect.strength)
|
|
||||||
break
|
|
||||||
case 'fog':
|
|
||||||
updateFogEffect(effect.strength)
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
|
const finalEffects =
|
||||||
|
mapEffects && Object.keys(mapEffects).length
|
||||||
|
? mapEffects
|
||||||
|
: {
|
||||||
|
light: timeBasedLight,
|
||||||
|
rain: weatherState.value.isRainEnabled ? weatherState.value.rainPercentage : 0,
|
||||||
|
fog: weatherState.value.isFogEnabled ? weatherState.value.fogDensity * 100 : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
applyEffects(finalEffects)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateLightEffect = (strength: number) => {
|
const applyEffects = (effectValues: any) => {
|
||||||
if (!lightEffect.value) return
|
if (effects.light.value) {
|
||||||
const darkness = 1 - strength / 100
|
const darkness = 1 - (effectValues.light ?? 100) / 100
|
||||||
lightEffect.value.clear()
|
effects.light.value.clear().fillStyle(0x000000, darkness).fillRect(0, 0, window.innerWidth, window.innerHeight)
|
||||||
lightEffect.value.fillStyle(0x000000, darkness)
|
}
|
||||||
lightEffect.value.fillRect(0, 0, window.innerWidth, window.innerHeight)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateRainEffect = (strength: number) => {
|
if (effects.rain.value) {
|
||||||
if (!rainEmitter.value) return
|
const strength = effectValues.rain ?? 0
|
||||||
if (strength > 0) {
|
strength > 0 ? effects.rain.value.start().setQuantity(Math.floor((strength / 100) * 10)) : effects.rain.value.stop()
|
||||||
rainEmitter.value.start()
|
}
|
||||||
rainEmitter.value.setQuantity(Math.floor((strength / 100) * 10))
|
|
||||||
} else {
|
if (effects.fog.value) {
|
||||||
rainEmitter.value.stop()
|
effects.fog.value.setAlpha((effectValues.fog ?? 0) / 100)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateFogEffect = (strength: number) => {
|
const calculateLightStrength = (time: Date): number => {
|
||||||
if (!fogSprite.value) return
|
const hour = time.getHours()
|
||||||
fogSprite.value.setAlpha(strength / 100)
|
const minute = time.getMinutes()
|
||||||
|
|
||||||
|
if (hour >= LIGHT_CONFIG.SUNSET_HOUR || hour < LIGHT_CONFIG.SUNRISE_HOUR) return LIGHT_CONFIG.NIGHT_STRENGTH
|
||||||
|
|
||||||
|
if (hour > LIGHT_CONFIG.SUNRISE_HOUR && hour < LIGHT_CONFIG.SUNSET_HOUR - 2) return LIGHT_CONFIG.DAY_STRENGTH
|
||||||
|
|
||||||
|
if (hour === LIGHT_CONFIG.SUNRISE_HOUR) return LIGHT_CONFIG.NIGHT_STRENGTH + ((LIGHT_CONFIG.DAY_STRENGTH - LIGHT_CONFIG.NIGHT_STRENGTH) * minute) / 60
|
||||||
|
|
||||||
|
const totalMinutes = (hour - (LIGHT_CONFIG.SUNSET_HOUR - 2)) * 60 + minute
|
||||||
|
return LIGHT_CONFIG.DAY_STRENGTH - (LIGHT_CONFIG.DAY_STRENGTH - LIGHT_CONFIG.NIGHT_STRENGTH) * (totalMinutes / 120)
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => zoneStore.zone?.zoneEffects, updateEffects, { deep: true })
|
// Socket and window handlers
|
||||||
|
const setupSocketListeners = () => {
|
||||||
|
gameStore.connection?.emit('weather', (response: WeatherState) => {
|
||||||
|
weatherState.value = response
|
||||||
|
updateScene()
|
||||||
|
})
|
||||||
|
|
||||||
|
gameStore.connection?.on('weather', (data: WeatherState) => {
|
||||||
|
weatherState.value = data
|
||||||
|
updateScene()
|
||||||
|
})
|
||||||
|
|
||||||
|
gameStore.connection?.on('date', updateScene)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
if (effects.rain.value) effects.rain.value.updateConfig({ x: { min: 0, max: window.innerWidth } })
|
||||||
|
if (effects.fog.value) effects.fog.value.setPosition(window.innerWidth / 2, window.innerHeight / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
watch(
|
||||||
|
() => mapStore.map,
|
||||||
|
() => {
|
||||||
|
mapEffectsReady.value = false
|
||||||
|
updateScene()
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(() => window.addEventListener('resize', handleResize))
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
if (sceneRef.value) sceneRef.value.scene.remove('effects')
|
if (sceneRef.value) sceneRef.value.scene.remove('effects')
|
||||||
|
gameStore.connection?.off('weather')
|
||||||
})
|
})
|
||||||
|
|
||||||
// @TODO : Fix resize issue
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-wrap items-center input-field gap-1">
|
<div class="flex flex-wrap items-center input-field gap-1" @click="focusInput">
|
||||||
<div v-for="(chip, i) in internalValue" :key="i" class="flex gap-2.5 items-center bg-cyan rounded py-1 px-2">
|
<div v-for="(chip, i) in internalValue" :key="i" class="flex gap-2.5 items-center bg-cyan rounded py-1 px-2" role="listitem">
|
||||||
<span class="text-xs text-white">{{ chip }}</span>
|
<span class="text-xs text-white">{{ chip }}</span>
|
||||||
<button type="button" class="text-xs cursor-pointer text-white font-light font-default not-italic hover:text-gray-50" @click="deleteChip(i)" aria-label="Remove chip">×</button>
|
<button type="button" class="text-xs cursor-pointer text-white font-light font-default not-italic hover:text-gray-50" @click.stop="deleteChip(i)" aria-label="Remove tag">×</button>
|
||||||
</div>
|
</div>
|
||||||
<input class="outline-none border-none p-1 text-gray-300" placeholder="Tag name" v-model="currentInput" @keypress.enter.prevent="addChip" @keydown.backspace="handleBackspace" />
|
<input ref="inputRef" class="outline-none border-none p-1 text-gray-300 min-w-[60px] flex-grow" :placeholder="placeholder" v-model.trim="currentInput" @keydown="handleKeydown" @paste="handlePaste" :maxlength="maxChipLength" aria-label="Add new tag" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -14,20 +14,29 @@ import type { Ref } from 'vue'
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue?: string[]
|
modelValue?: string[]
|
||||||
|
maxChips?: number
|
||||||
|
maxChipLength?: number
|
||||||
|
placeholder?: string
|
||||||
|
allowDuplicates?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
modelValue: () => []
|
modelValue: () => [],
|
||||||
|
maxChips: 10,
|
||||||
|
maxChipLength: 20,
|
||||||
|
placeholder: 'Add tag',
|
||||||
|
allowDuplicates: false
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: string[]): void
|
(e: 'update:modelValue', value: string[]): void
|
||||||
|
(e: 'error', message: string): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const currentInput: Ref<string> = ref('')
|
const currentInput: Ref<string> = ref('')
|
||||||
const internalValue = ref<string[]>([])
|
const internalValue = ref<string[]>([])
|
||||||
|
const inputRef = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
// Initialize internalValue with props.modelValue
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
@ -36,9 +45,27 @@ watch(
|
|||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const validateChip = (chip: string): boolean => {
|
||||||
|
if (!chip) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.allowDuplicates && internalValue.value.includes(chip)) {
|
||||||
|
emit('error', 'Duplicate tags are not allowed')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (internalValue.value.length >= props.maxChips) {
|
||||||
|
emit('error', `Maximum ${props.maxChips} tags allowed`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
const addChip = () => {
|
const addChip = () => {
|
||||||
const trimmedInput = currentInput.value.trim()
|
const trimmedInput = currentInput.value.trim()
|
||||||
if (trimmedInput && !internalValue.value.includes(trimmedInput)) {
|
if (validateChip(trimmedInput)) {
|
||||||
internalValue.value.push(trimmedInput)
|
internalValue.value.push(trimmedInput)
|
||||||
emit('update:modelValue', internalValue.value)
|
emit('update:modelValue', internalValue.value)
|
||||||
currentInput.value = ''
|
currentInput.value = ''
|
||||||
@ -50,10 +77,36 @@ const deleteChip = (index: number) => {
|
|||||||
emit('update:modelValue', internalValue.value)
|
emit('update:modelValue', internalValue.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleBackspace = (event: KeyboardEvent) => {
|
const handleKeydown = (event: KeyboardEvent) => {
|
||||||
if (event.key === 'Backspace' && currentInput.value === '' && internalValue.value.length > 0) {
|
switch (event.key) {
|
||||||
internalValue.value.pop()
|
case 'Enter':
|
||||||
emit('update:modelValue', internalValue.value)
|
event.preventDefault()
|
||||||
|
addChip()
|
||||||
|
break
|
||||||
|
case 'Backspace':
|
||||||
|
if (currentInput.value === '' && internalValue.value.length > 0) {
|
||||||
|
deleteChip(internalValue.value.length - 1)
|
||||||
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handlePaste = (event: ClipboardEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
const pastedText = event.clipboardData?.getData('text')
|
||||||
|
if (pastedText) {
|
||||||
|
const chips = pastedText
|
||||||
|
.split(/[,\n]/)
|
||||||
|
.map((chip) => chip.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
chips.forEach((chip) => {
|
||||||
|
currentInput.value = chip
|
||||||
|
addChip()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const focusInput = () => {
|
||||||
|
inputRef.value?.focus()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
184
src/components/game/character/Character.vue
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
<template>
|
||||||
|
<ChatBubble :mapCharacter="props.mapCharacter" :currentX="currentPositionX" :currentY="currentPositionY" />
|
||||||
|
<Healthbar :mapCharacter="props.mapCharacter" :currentX="currentPositionX" :currentY="currentPositionY" />
|
||||||
|
<Container ref="charContainer" :depth="isometricDepth" :x="currentPositionX" :y="currentPositionY">
|
||||||
|
<!-- <CharacterHair :mapCharacter="props.mapCharacter" :currentX="currentX" :currentY="currentY" />-->
|
||||||
|
<!-- <CharacterChest :mapCharacter="props.mapCharacter" :currentX="currentX" :currentY="currentY" />-->
|
||||||
|
<Sprite ref="charSprite" :origin-y="1" :flipX="isFlippedX" />
|
||||||
|
</Container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import config from '@/application/config'
|
||||||
|
import { type MapCharacter, type Sprite as SpriteT } from '@/application/types'
|
||||||
|
import CharacterHair from '@/components/game/character/partials/CharacterHair.vue'
|
||||||
|
import ChatBubble from '@/components/game/character/partials/ChatBubble.vue'
|
||||||
|
import Healthbar from '@/components/game/character/partials/Healthbar.vue'
|
||||||
|
import { loadSpriteTextures } from '@/composables/gameComposable'
|
||||||
|
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/mapComposable'
|
||||||
|
import { CharacterTypeStorage } from '@/storage/storages'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useMapStore } from '@/stores/mapStore'
|
||||||
|
import { Container, refObj, Sprite, useScene } from 'phavuer'
|
||||||
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
// import CharacterChest from '@/components/game/character/partials/CharacterChest.vue'
|
||||||
|
|
||||||
|
enum Direction {
|
||||||
|
POSITIVE,
|
||||||
|
NEGATIVE,
|
||||||
|
UNCHANGED
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
tilemap: Phaser.Tilemaps.Tilemap
|
||||||
|
mapCharacter: MapCharacter
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const charContainer = refObj<Phaser.GameObjects.Container>()
|
||||||
|
const charSprite = refObj<Phaser.GameObjects.Sprite>()
|
||||||
|
const charSpriteId = ref('')
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const mapStore = useMapStore()
|
||||||
|
const scene = useScene()
|
||||||
|
|
||||||
|
const currentPositionX = ref(0)
|
||||||
|
const currentPositionY = ref(0)
|
||||||
|
const isometricDepth = ref(1)
|
||||||
|
const isInitialPosition = ref(true)
|
||||||
|
const tween = ref<Phaser.Tweens.Tween | null>(null)
|
||||||
|
|
||||||
|
const updateIsometricDepth = (positionX: number, positionY: number) => {
|
||||||
|
isometricDepth.value = calculateIsometricDepth(positionX, positionY, 28, 94, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePosition = (positionX: number, positionY: number, direction: Direction) => {
|
||||||
|
const newPositionX = tileToWorldX(props.tilemap, positionX, positionY)
|
||||||
|
const newPositionY = tileToWorldY(props.tilemap, positionX, positionY)
|
||||||
|
|
||||||
|
if (isInitialPosition.value) {
|
||||||
|
currentPositionX.value = newPositionX
|
||||||
|
currentPositionY.value = newPositionY
|
||||||
|
isInitialPosition.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tween.value?.isPlaying()) {
|
||||||
|
tween.value.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
const distance = Math.sqrt(Math.pow(newPositionX - currentPositionX.value, 2) + Math.pow(newPositionY - currentPositionY.value, 2))
|
||||||
|
|
||||||
|
if (distance >= config.tile_size.width / 1.1) {
|
||||||
|
currentPositionX.value = newPositionX
|
||||||
|
currentPositionY.value = newPositionY
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const duration = distance * 5.7
|
||||||
|
|
||||||
|
tween.value = props.tilemap.scene.tweens.add({
|
||||||
|
targets: { x: currentPositionX.value, y: currentPositionY.value },
|
||||||
|
x: newPositionX,
|
||||||
|
y: newPositionY,
|
||||||
|
duration,
|
||||||
|
ease: 'Linear',
|
||||||
|
onStart: () => {
|
||||||
|
if (direction === Direction.POSITIVE) {
|
||||||
|
updateIsometricDepth(positionX, positionY)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onUpdate: (tween) => {
|
||||||
|
// @ts-ignore
|
||||||
|
currentPositionX.value = tween.targets[0].x
|
||||||
|
// @ts-ignore
|
||||||
|
currentPositionY.value = tween.targets[0].y
|
||||||
|
},
|
||||||
|
onComplete: () => {
|
||||||
|
if (direction === Direction.NEGATIVE) {
|
||||||
|
updateIsometricDepth(positionX, positionY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const calcDirection = (oldPositionX: number, oldPositionY: number, newPositionX: number, newPositionY: number): Direction => {
|
||||||
|
if (newPositionY < oldPositionY || newPositionX < oldPositionX) return Direction.NEGATIVE
|
||||||
|
if (newPositionX > oldPositionX || newPositionY > oldPositionY) return Direction.POSITIVE
|
||||||
|
return Direction.UNCHANGED
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFlippedX = computed(() => [6, 4].includes(props.mapCharacter.character.rotation ?? 0))
|
||||||
|
|
||||||
|
const charTexture = computed(() => {
|
||||||
|
const spriteId = charSpriteId.value ?? 'idle_right_down'
|
||||||
|
const action = props.mapCharacter.isMoving ? 'walk' : 'idle'
|
||||||
|
const direction = [0, 6].includes(props.mapCharacter.character.rotation) ? 'left_up' : 'right_down'
|
||||||
|
|
||||||
|
return `${spriteId}-${action}_${direction}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateSprite = () => {
|
||||||
|
if (props.mapCharacter.isMoving) {
|
||||||
|
charSprite.value!.anims.play(charTexture.value, true)
|
||||||
|
} else {
|
||||||
|
charSprite.value!.anims.stop()
|
||||||
|
charSprite.value!.setFrame(0)
|
||||||
|
charSprite.value!.setTexture(charTexture.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => ({
|
||||||
|
positionX: props.mapCharacter.character.positionX,
|
||||||
|
positionY: props.mapCharacter.character.positionY,
|
||||||
|
isMoving: props.mapCharacter.isMoving,
|
||||||
|
rotation: props.mapCharacter.character.rotation
|
||||||
|
}),
|
||||||
|
(newValues, oldValues) => {
|
||||||
|
if (!newValues) return
|
||||||
|
|
||||||
|
if (!oldValues || newValues.positionX !== oldValues.positionX || newValues.positionY !== oldValues.positionY) {
|
||||||
|
const direction = !oldValues ? Direction.POSITIVE : calcDirection(oldValues.positionX, oldValues.positionY, newValues.positionX, newValues.positionY)
|
||||||
|
updatePosition(newValues.positionX, newValues.positionY, direction)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle animation updates
|
||||||
|
if (newValues.isMoving !== oldValues?.isMoving || newValues.rotation !== oldValues?.rotation) {
|
||||||
|
updateSprite()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const characterTypeStorage = new CharacterTypeStorage()
|
||||||
|
characterTypeStorage.getSpriteId(props.mapCharacter.character.characterType!).then((spriteId) => {
|
||||||
|
console.log(spriteId)
|
||||||
|
charSpriteId.value = spriteId
|
||||||
|
loadSpriteTextures(scene, spriteId)
|
||||||
|
.then(() => {
|
||||||
|
charSprite.value!.setTexture(charTexture.value)
|
||||||
|
charSprite.value!.setFlipX(isFlippedX.value)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error loading texture:', error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
charContainer.value!.setName(props.mapCharacter.character!.name)
|
||||||
|
|
||||||
|
if (props.mapCharacter.character.id === gameStore.character!.id) {
|
||||||
|
mapStore.setCharacterLoaded(true)
|
||||||
|
|
||||||
|
// #146 : Set camera position to character, need to be improved still
|
||||||
|
scene.cameras.main.startFollow(charContainer.value as Phaser.GameObjects.Container)
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePosition(props.mapCharacter.character.positionX, props.mapCharacter.character.positionY, props.mapCharacter.character.rotation)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
tween.value?.stop()
|
||||||
|
})
|
||||||
|
</script>
|
51
src/components/game/character/partials/CharacterChest.vue
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<Image v-bind="imageProps" v-if="gameStore.getLoadedAsset(texture)" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { MapCharacter, Sprite as SpriteT } from '@/application/types'
|
||||||
|
import { loadSpriteTextures } from '@/composables/gameComposable'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { Image, useScene } from 'phavuer'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
mapCharacter: MapCharacter
|
||||||
|
currentX: number
|
||||||
|
currentY: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const scene = useScene()
|
||||||
|
|
||||||
|
const texture = computed(() => {
|
||||||
|
const { rotation, characterHair } = props.mapCharacter.character
|
||||||
|
const spriteId = characterHair?.sprite?.id
|
||||||
|
const direction = [0, 6].includes(rotation) ? 'back' : 'front'
|
||||||
|
|
||||||
|
return `${spriteId}-${direction}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const isFlippedX = computed(() => [6, 4].includes(props.mapCharacter.character.rotation ?? 0))
|
||||||
|
|
||||||
|
const imageProps = computed(() => {
|
||||||
|
// Get the current sprite action based on direction
|
||||||
|
const direction = [0, 6].includes(props.mapCharacter.character.rotation ?? 0) ? 'back' : 'front'
|
||||||
|
const spriteAction = props.mapCharacter.character.characterHair?.sprite?.spriteActions?.find((spriteAction) => spriteAction.action === direction)
|
||||||
|
|
||||||
|
return {
|
||||||
|
depth: 1,
|
||||||
|
originX: Number(spriteAction?.originX) ?? 0,
|
||||||
|
originY: Number(spriteAction?.originY) ?? 0,
|
||||||
|
flipX: isFlippedX.value,
|
||||||
|
texture: texture.value
|
||||||
|
// y: props.mapCharacter.isMoving ? Math.floor(Date.now() / 250) % 2 : 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
loadSpriteTextures(scene, props.mapCharacter.character.characterHair?.sprite as SpriteT)
|
||||||
|
.then(() => {})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error loading texture:', error)
|
||||||
|
})
|
||||||
|
</script>
|
51
src/components/game/character/partials/CharacterHair.vue
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<Image v-bind="imageProps" v-if="gameStore.getLoadedAsset(texture)" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { MapCharacter, Sprite as SpriteT } from '@/application/types'
|
||||||
|
import { loadSpriteTextures } from '@/composables/gameComposable'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { Image, useScene } from 'phavuer'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
mapCharacter: MapCharacter
|
||||||
|
currentX: number
|
||||||
|
currentY: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const scene = useScene()
|
||||||
|
|
||||||
|
const texture = computed(() => {
|
||||||
|
const { rotation, characterHair } = props.mapCharacter.character
|
||||||
|
const spriteId = characterHair?.sprite?.id
|
||||||
|
const direction = [0, 6].includes(rotation) ? 'back' : 'front'
|
||||||
|
|
||||||
|
return `${spriteId}-${direction}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const isFlippedX = computed(() => [6, 4].includes(props.mapCharacter.character.rotation ?? 0))
|
||||||
|
|
||||||
|
const imageProps = computed(() => {
|
||||||
|
// Get the current sprite action based on direction
|
||||||
|
const direction = [0, 6].includes(props.mapCharacter.character.rotation ?? 0) ? 'back' : 'front'
|
||||||
|
const spriteAction = props.mapCharacter.character.characterHair?.sprite?.spriteActions?.find((spriteAction) => spriteAction.action === direction)
|
||||||
|
|
||||||
|
return {
|
||||||
|
depth: 1,
|
||||||
|
originX: Number(spriteAction?.originX) ?? 0,
|
||||||
|
originY: Number(spriteAction?.originY) ?? 0,
|
||||||
|
flipX: isFlippedX.value,
|
||||||
|
texture: texture.value,
|
||||||
|
y: props.mapCharacter.isMoving ? Math.floor(Date.now() / 250) % 2 : 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
loadSpriteTextures(scene, props.mapCharacter.character.characterHair?.sprite as SpriteT)
|
||||||
|
.then(() => {})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error loading texture:', error)
|
||||||
|
})
|
||||||
|
</script>
|
47
src/components/game/character/partials/ChatBubble.vue
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<Container ref="charChatContainer" :depth="999" :x="currentX" :y="currentY">
|
||||||
|
<RoundRectangle @create="createChatBubble" :origin-x="0.5" :origin-y="7.5" :fillColor="0xffffff" :width="194" :height="21" :radius="20" />
|
||||||
|
<Text @create="createChatText" :style="{ fontSize: 13, fontFamily: 'Arial', color: '#000' }" />
|
||||||
|
</Container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { MapCharacter } from '@/application/types'
|
||||||
|
import { Container, refObj, RoundRectangle, Text, useGame } from 'phavuer'
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
mapCharacter: MapCharacter
|
||||||
|
currentX: number
|
||||||
|
currentY: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const game = useGame()
|
||||||
|
const charChatContainer = refObj<Phaser.GameObjects.Container>()
|
||||||
|
|
||||||
|
const createChatBubble = (container: Phaser.GameObjects.Container) => {
|
||||||
|
container.setName(`${props.mapCharacter.character.name}_chatBubble`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const createChatText = (text: Phaser.GameObjects.Text) => {
|
||||||
|
text.setName(`${props.mapCharacter.character.name}_chatText`)
|
||||||
|
text.setFontSize(13)
|
||||||
|
text.setFontFamily('Arial')
|
||||||
|
text.setOrigin(0.5, 10.9)
|
||||||
|
text.setResolution(2)
|
||||||
|
|
||||||
|
// Fix text alignment on Windows and Android
|
||||||
|
if (game.device.os.windows || game.device.os.android) {
|
||||||
|
text.setOrigin(0.5, 9.75)
|
||||||
|
|
||||||
|
if (game.device.browser.firefox) {
|
||||||
|
text.setOrigin(0.5, 10.9)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
charChatContainer.value!.setName(`${props.mapCharacter.character!.name}_chatContainer`)
|
||||||
|
charChatContainer.value!.setVisible(false)
|
||||||
|
})
|
||||||
|
</script>
|
36
src/components/game/character/partials/Healthbar.vue
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<template>
|
||||||
|
<Container :depth="999" :x="currentX" :y="currentY">
|
||||||
|
<Text @create="createNicknameText" :text="props.mapCharacter.character.name" />
|
||||||
|
<RoundRectangle :origin-x="0.5" :origin-y="18.5" :fillColor="0xffffff" :width="74" :height="6" :radius="5" />
|
||||||
|
<RoundRectangle :origin-x="0.5" :origin-y="36.4" :fillColor="0x00b3b3" :width="70" :height="3" :radius="5" />
|
||||||
|
</Container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { MapCharacter } from '@/application/types'
|
||||||
|
import { Container, RoundRectangle, Text, useGame } from 'phavuer'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
mapCharacter: MapCharacter
|
||||||
|
currentX: number
|
||||||
|
currentY: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const game = useGame()
|
||||||
|
|
||||||
|
const createNicknameText = (text: Phaser.GameObjects.Text) => {
|
||||||
|
text.setFontSize(13)
|
||||||
|
text.setFontFamily('Arial')
|
||||||
|
text.setOrigin(0.5, 9)
|
||||||
|
text.setResolution(2)
|
||||||
|
|
||||||
|
// Fix text alignment on Windows and Android
|
||||||
|
if (game.device.os.windows || game.device.os.android) {
|
||||||
|
text.setOrigin(0.5, 8)
|
||||||
|
|
||||||
|
if (game.device.browser.firefox) {
|
||||||
|
text.setOrigin(0.5, 9)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
14
src/components/game/map/Characters.vue
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<Character v-for="item in mapStore.characters" :key="item.character.id" :tilemap="tilemap" :mapCharacter="item" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Character from '@/components/game/character/Character.vue'
|
||||||
|
import { useMapStore } from '@/stores/mapStore'
|
||||||
|
|
||||||
|
const mapStore = useMapStore()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
tilemap: Phaser.Tilemaps.Tilemap
|
||||||
|
}>()
|
||||||
|
</script>
|
46
src/components/game/map/Map.vue
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<MapTiles :key="mapStore.mapId" @tileMap:create="tileMap = $event" />
|
||||||
|
<!-- <MapObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />-->
|
||||||
|
<Characters v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { MapCharacter, mapLoadData, UUID } from '@/application/types'
|
||||||
|
import Characters from '@/components/game/map/Characters.vue'
|
||||||
|
import MapTiles from '@/components/game/map/MapTiles.vue'
|
||||||
|
import MapObjects from '@/components/game/map/PlacedMapObjects.vue'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useMapStore } from '@/stores/mapStore'
|
||||||
|
import { onUnmounted, shallowRef } from 'vue'
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const mapStore = useMapStore()
|
||||||
|
|
||||||
|
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
mapStore.reset()
|
||||||
|
gameStore.connection?.off('map:character:teleport')
|
||||||
|
gameStore.connection?.off('map:character:join')
|
||||||
|
gameStore.connection?.off('map:character:leave')
|
||||||
|
gameStore.connection?.off('map:character:move')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
gameStore.connection?.on('map:character:teleport', async (data: mapLoadData) => {
|
||||||
|
mapStore.setMapId(data.mapId)
|
||||||
|
mapStore.setCharacters(data.characters)
|
||||||
|
})
|
||||||
|
|
||||||
|
gameStore.connection?.on('map:character:join', async (data: MapCharacter) => {
|
||||||
|
mapStore.addCharacter(data)
|
||||||
|
})
|
||||||
|
|
||||||
|
gameStore.connection?.on('map:character:leave', (characterId: UUID) => {
|
||||||
|
mapStore.removeCharacter(characterId)
|
||||||
|
})
|
||||||
|
|
||||||
|
gameStore.connection?.on('map:character:move', (data: { characterId: UUID; positionX: number; positionY: number; rotation: number; isMoving: boolean }) => {
|
||||||
|
mapStore.updateCharacterPosition(data)
|
||||||
|
})
|
||||||
|
</script>
|
75
src/components/game/map/MapTiles.vue
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<Controls v-if="tileLayer" :layer="tileLayer" :depth="0" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import config from '@/application/config'
|
||||||
|
import type { UUID } from '@/application/types'
|
||||||
|
import { unduplicateArray } from '@/application/utilities'
|
||||||
|
import Controls from '@/components/utilities/Controls.vue'
|
||||||
|
import { FlattenMapArray, loadMapTilesIntoScene, setLayerTiles } from '@/composables/mapComposable'
|
||||||
|
import { MapStorage } from '@/storage/storages'
|
||||||
|
import { useMapStore } from '@/stores/mapStore'
|
||||||
|
import { useScene } from 'phavuer'
|
||||||
|
import { onBeforeUnmount, onMounted, shallowRef } from 'vue'
|
||||||
|
|
||||||
|
import Tileset = Phaser.Tilemaps.Tileset
|
||||||
|
|
||||||
|
const emit = defineEmits(['tileMap:create'])
|
||||||
|
const scene = useScene()
|
||||||
|
const mapStore = useMapStore()
|
||||||
|
const mapStorage = new MapStorage()
|
||||||
|
|
||||||
|
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
|
||||||
|
const tileLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
|
||||||
|
|
||||||
|
function createTileMap(mapData: any) {
|
||||||
|
const mapConfig = new Phaser.Tilemaps.MapData({
|
||||||
|
width: mapData?.width,
|
||||||
|
height: mapData?.height,
|
||||||
|
tileWidth: config.tile_size.width,
|
||||||
|
tileHeight: config.tile_size.height,
|
||||||
|
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
|
||||||
|
format: Phaser.Tilemaps.Formats.ARRAY_2D
|
||||||
|
})
|
||||||
|
|
||||||
|
const newTileMap = new Phaser.Tilemaps.Tilemap(scene, mapConfig)
|
||||||
|
emit('tileMap:create', newTileMap)
|
||||||
|
return newTileMap
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTileLayer(currentTileMap: Phaser.Tilemaps.Tilemap, mapData: any) {
|
||||||
|
const tilesArray = unduplicateArray(FlattenMapArray(mapData?.tiles ?? []))
|
||||||
|
|
||||||
|
const tilesetImages = tilesArray.map((tile: any, index: number) => {
|
||||||
|
return currentTileMap.addTilesetImage(tile, tile, config.tile_size.width, config.tile_size.height, 1, 2, index + 1, { x: 0, y: -config.tile_size.height })
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add blank tile
|
||||||
|
tilesetImages.push(currentTileMap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.width, config.tile_size.height, 1, 2, 0, { x: 0, y: -config.tile_size.height }))
|
||||||
|
|
||||||
|
const layer = currentTileMap.createBlankLayer('tiles', tilesetImages as Tileset[], 0, config.tile_size.height) as Phaser.Tilemaps.TilemapLayer
|
||||||
|
|
||||||
|
layer.setDepth(0)
|
||||||
|
layer.setCullPadding(2, 2)
|
||||||
|
return layer
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadMapTilesIntoScene(mapStore.mapId as UUID, scene)
|
||||||
|
.then(() => mapStorage.get(mapStore.mapId))
|
||||||
|
.then((mapData) => {
|
||||||
|
tileMap.value = createTileMap(mapData)
|
||||||
|
tileLayer.value = createTileLayer(tileMap.value, mapData)
|
||||||
|
setLayerTiles(tileMap.value, tileLayer.value, mapData?.tiles)
|
||||||
|
})
|
||||||
|
.catch((error) => console.error('Failed to initialize map:', error))
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (!tileMap.value) return
|
||||||
|
tileMap.value.destroyLayer('tiles')
|
||||||
|
tileMap.value.removeAllLayers()
|
||||||
|
tileMap.value.destroy()
|
||||||
|
})
|
||||||
|
</script>
|
14
src/components/game/map/PlacedMapObjects.vue
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<PlacedMapObject v-for="placedMapObject in mapStore.map?.placedMapObjects" :tilemap="tilemap" :placedMapObject />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import PlacedMapObject from '@/components/game/map/partials/PlacedMapObject.vue'
|
||||||
|
import { useMapStore } from '@/stores/mapStore'
|
||||||
|
|
||||||
|
const mapStore = useMapStore()
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
tilemap: Phaser.Tilemaps.Tilemap
|
||||||
|
}>()
|
||||||
|
</script>
|
41
src/components/game/map/partials/PlacedMapObject.vue
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<template>
|
||||||
|
<Image v-if="gameStore.isAssetLoaded(props.placedMapObject.mapObject)" v-bind="imageProps" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { PlacedMapObject, TextureData } from '@/application/types'
|
||||||
|
import { loadTexture } from '@/composables/gameComposable'
|
||||||
|
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/mapComposable'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { Image, useScene } from 'phavuer'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
tilemap: Phaser.Tilemaps.Tilemap
|
||||||
|
placedMapObject: PlacedMapObject
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const scene = useScene()
|
||||||
|
|
||||||
|
const imageProps = computed(() => ({
|
||||||
|
depth: calculateIsometricDepth(props.placedMapObject.positionX, props.placedMapObject.positionY, props.placedMapObject.mapObject.frameWidth, props.placedMapObject.mapObject.frameHeight),
|
||||||
|
x: tileToWorldX(props.tilemap, props.placedMapObject.positionX, props.placedMapObject.positionY),
|
||||||
|
y: tileToWorldY(props.tilemap, props.placedMapObject.positionX, props.placedMapObject.positionY),
|
||||||
|
flipX: props.placedMapObject.isRotated,
|
||||||
|
texture: props.placedMapObject.mapObject.id,
|
||||||
|
originY: Number(props.placedMapObject.mapObject.originX),
|
||||||
|
originX: Number(props.placedMapObject.mapObject.originY)
|
||||||
|
}))
|
||||||
|
|
||||||
|
loadTexture(scene, {
|
||||||
|
key: props.placedMapObject.mapObject.id,
|
||||||
|
data: '/textures/map_objects/' + props.placedMapObject.mapObject.id + '.png',
|
||||||
|
group: 'map_objects',
|
||||||
|
updatedAt: props.placedMapObject.mapObject.updatedAt,
|
||||||
|
frameWidth: props.placedMapObject.mapObject.frameWidth,
|
||||||
|
frameHeight: props.placedMapObject.mapObject.frameHeight
|
||||||
|
} as TextureData).catch((error) => {
|
||||||
|
console.error('Error loading texture:', error)
|
||||||
|
})
|
||||||
|
</script>
|
@ -1,11 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal :isModalOpen="gameStore.uiSettings.isGmPanelOpen" @modal:close="() => gameStore.toggleGmPanel()" :modal-width="1000" :modal-height="650" :can-full-screen="true">
|
<Modal :isModalOpen="gameStore.uiSettings.isGmPanelOpen" @modal:close="() => gameStore.toggleGmPanel()" :modal-width="1000" :modal-height="650" :is-full-screen="true" :bg-style="'dark'">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<div class="flex gap-1.5 flex-wrap">
|
<div class="flex gap-1.5 flex-wrap">
|
||||||
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">General</button>
|
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">General</button>
|
||||||
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Users</button>
|
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Users</button>
|
||||||
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Chats</button>
|
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Chats</button>
|
||||||
<button @mousedown.stop class="btn-cyan active py-1.5 px-4 min-w-24">Asset manager</button>
|
<button @mousedown.stop class="btn-cyan active py-1.5 px-4 min-w-24">Asset manager</button>
|
||||||
|
<button class="btn-cyan py-1.5 px-4 min-w-24" type="button" @click="() => mapEditorStore.toggleActive()">Map editor</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #modalBody>
|
<template #modalBody>
|
||||||
@ -17,12 +18,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from '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 Modal from '@/components/utilities/Modal.vue'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useMapEditorStore } from '@/stores/mapEditorStore'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
const mapEditorStore = useMapEditorStore()
|
||||||
|
|
||||||
let toggle = ref('asset-manager')
|
let toggle = ref('asset-manager')
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,44 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Modal :isModalOpen="true" :closable="false" :is-resizable="false" :modal-width="modalWidth" :modal-height="modalHeight" :modal-position-x="posXY.x" :modal-position-y="posXY.y">
|
|
||||||
<template #modalHeader>
|
|
||||||
<h3 class="m-0 font-medium shrink-0 text-white">GM tools</h3>
|
|
||||||
</template>
|
|
||||||
<template #modalBody>
|
|
||||||
<div class="content flex flex-col gap-2.5 m-4 h-20">
|
|
||||||
<button class="btn-cyan py-1.5 px-4 w-full" type="button" @click="gameStore.toggleGmPanel()">Toggle GM panel</button>
|
|
||||||
<button class="btn-cyan py-1.5 px-4 w-full" type="button" @click="() => zoneEditorStore.toggleActive()">Zone manager</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Modal>
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts">
|
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import { onMounted, ref } from 'vue'
|
|
||||||
|
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const modalWidth = ref(200)
|
|
||||||
const modalHeight = ref(170)
|
|
||||||
|
|
||||||
let posXY = ref({ x: 0, y: 0 })
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
posXY.value = customPositionGmPanel(modalWidth.value)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const customPositionGmPanel = (modalWidth: number) => {
|
|
||||||
const padding = 25
|
|
||||||
const width = window.innerWidth
|
|
||||||
|
|
||||||
const x = width - (modalWidth + 4) - 25
|
|
||||||
const y = padding
|
|
||||||
|
|
||||||
return { x, y }
|
|
||||||
}
|
|
||||||
|
|
||||||
posXY.value = customPositionGmPanel(modalWidth.value)
|
|
||||||
</script>
|
|
@ -1,85 +1,79 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full w-full relative">
|
<div class="flex gap-4 h-[calc(100%_-_32px)] w-[calc(100%_-_32px)] relative m-4">
|
||||||
<div class="w-2/12 flex flex-col relative overflow-auto">
|
<div class="w-2/12 flex flex-col relative overflow-auto rounded-md default-border bg-gray p-2.5">
|
||||||
<!-- Asset Categories -->
|
<!-- Asset Categories -->
|
||||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'tiles' }" @click="() => (selectedCategory = 'tiles')">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'tiles' }" @click="() => (selectedCategory = 'tiles')">
|
||||||
<span :class="{ 'text-white': selectedCategory === 'tiles' }">Tiles</span>
|
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'tiles' }">Tiles</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'objects' }" @click="() => (selectedCategory = 'objects')">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'map_objects' }" @click="() => (selectedCategory = 'map_objects')">
|
||||||
<span :class="{ 'text-white': selectedCategory === 'objects' }">Objects</span>
|
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'map_objects' }">Map objects</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'sprites' }" @click="() => (selectedCategory = 'sprites')">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'sprites' }" @click="() => (selectedCategory = 'sprites')">
|
||||||
<span :class="{ 'text-white': selectedCategory === 'sprites' }">Sprites</span>
|
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'sprites' }">Sprites</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'items' }" @click="() => (selectedCategory = 'items')">
|
||||||
<span>Items</span>
|
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'items' }">Items</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group">
|
||||||
<span>NPC's</span>
|
<span class="group-hover:text-white">NPC's</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'shops' }" @click="() => (selectedCategory = 'shops')">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan/80': selectedCategory === 'shops' }" @click="() => (selectedCategory = 'shops')">
|
||||||
<span :class="{ 'text-white': selectedCategory === 'shops' }">Shops</span>
|
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'shops' }">Shops</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'characterTypes' }" @click="() => (selectedCategory = 'characterTypes')">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan/80': selectedCategory === 'characterTypes' }" @click="() => (selectedCategory = 'characterTypes')">
|
||||||
<span :class="{ 'text-white': selectedCategory === 'characterTypes' }">Character types</span>
|
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'characterTypes' }">Character types</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'characterHair' }" @click="() => (selectedCategory = 'characterHair')">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan/80': selectedCategory === 'characterHair' }" @click="() => (selectedCategory = 'characterHair')">
|
||||||
<span :class="{ 'text-white': selectedCategory === 'characterHair' }">Character hair</span>
|
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'characterHair' }">Character hair</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group">
|
||||||
<span>Mounts</span>
|
<span class="group-hover:text-white">Mounts</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group">
|
||||||
<span>Pets</span>
|
<span class="group-hover:text-white">Pets</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group">
|
||||||
<span>Emoticons</span>
|
<span class="group-hover:text-white">Emoticons</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute w-px bg-gray-500 h-full top-0 left-1/6"></div>
|
|
||||||
|
|
||||||
<!-- Assets list -->
|
<!-- Assets list -->
|
||||||
<div class="overflow-auto h-full w-4/12 flex flex-col relative">
|
<div class="overflow-auto h-full w-4/12 flex flex-col relative">
|
||||||
<TileList v-if="selectedCategory === 'tiles'" />
|
<TileList v-if="selectedCategory === 'tiles'" />
|
||||||
<ObjectList v-if="selectedCategory === 'objects'" />
|
<MapObjectList v-if="selectedCategory === 'map_objects'" />
|
||||||
<SpriteList v-if="selectedCategory === 'sprites'" />
|
<SpriteList v-if="selectedCategory === 'sprites'" />
|
||||||
|
<ItemList v-if="selectedCategory === 'items'" />
|
||||||
<CharacterTypeList v-if="selectedCategory === 'characterTypes'" />
|
<CharacterTypeList v-if="selectedCategory === 'characterTypes'" />
|
||||||
|
<CharacterHairList v-if="selectedCategory === 'characterHair'" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute w-px bg-gray-500 h-full top-0 left-1/2"></div>
|
|
||||||
|
|
||||||
<!-- Asset details -->
|
<!-- Asset details -->
|
||||||
<div class="flex w-1/2 after:hidden flex-col relative overflow-auto">
|
<div class="flex w-7/12 after:hidden flex-col relative overflow-auto">
|
||||||
<TileDetails v-if="selectedCategory === 'tiles' && assetManagerStore.selectedTile" />
|
<TileDetails v-if="selectedCategory === 'tiles' && assetManagerStore.selectedTile" />
|
||||||
<ObjectDetails v-if="selectedCategory === 'objects' && assetManagerStore.selectedObject" />
|
<MapObjectDetails v-if="selectedCategory === 'map_objects' && assetManagerStore.selectedMapObject" />
|
||||||
<SpriteDetails v-if="selectedCategory === 'sprites' && assetManagerStore.selectedSprite" />
|
<SpriteDetails v-if="selectedCategory === 'sprites' && assetManagerStore.selectedSprite" />
|
||||||
|
<ItemDetails v-if="selectedCategory === 'items' && assetManagerStore.selectedItem" />
|
||||||
<CharacterTypeDetails v-if="selectedCategory === 'characterTypes' && assetManagerStore.selectedCharacterType" />
|
<CharacterTypeDetails v-if="selectedCategory === 'characterTypes' && assetManagerStore.selectedCharacterType" />
|
||||||
|
<CharacterHairDetails v-if="selectedCategory === 'characterHair' && assetManagerStore.selectedCharacterHair" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import CharacterHairDetails from '@/components/gameMaster/assetManager/partials/characterHair/CharacterHairDetails.vue'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import CharacterHairList from '@/components/gameMaster/assetManager/partials/characterHair/CharacterHairList.vue'
|
||||||
import TileList from '@/components/gameMaster/assetManager/partials/tile/TileList.vue'
|
|
||||||
import TileDetails from '@/components/gameMaster/assetManager/partials/tile/TileDetails.vue'
|
|
||||||
import ObjectList from '@/components/gameMaster/assetManager/partials/object/ObjectList.vue'
|
|
||||||
import ObjectDetails from '@/components/gameMaster/assetManager/partials/object/ObjectDetails.vue'
|
|
||||||
import SpriteList from '@/components/gameMaster/assetManager/partials/sprite/SpriteList.vue'
|
|
||||||
import SpriteDetails from '@/components/gameMaster/assetManager/partials/sprite/SpriteDetails.vue'
|
|
||||||
import CharacterTypeList from '@/components/gameMaster/assetManager/partials/characterType/CharacterTypeList.vue'
|
|
||||||
import CharacterTypeDetails from '@/components/gameMaster/assetManager/partials/characterType/CharacterTypeDetails.vue'
|
import CharacterTypeDetails from '@/components/gameMaster/assetManager/partials/characterType/CharacterTypeDetails.vue'
|
||||||
|
import CharacterTypeList from '@/components/gameMaster/assetManager/partials/characterType/CharacterTypeList.vue'
|
||||||
|
import ItemDetails from '@/components/gameMaster/assetManager/partials/item/itemDetails.vue'
|
||||||
|
import ItemList from '@/components/gameMaster/assetManager/partials/item/itemList.vue'
|
||||||
|
import MapObjectDetails from '@/components/gameMaster/assetManager/partials/mapObject/MapObjectDetails.vue'
|
||||||
|
import MapObjectList from '@/components/gameMaster/assetManager/partials/mapObject/MapObjectList.vue'
|
||||||
|
import SpriteDetails from '@/components/gameMaster/assetManager/partials/sprite/SpriteDetails.vue'
|
||||||
|
import SpriteList from '@/components/gameMaster/assetManager/partials/sprite/SpriteList.vue'
|
||||||
|
import TileDetails from '@/components/gameMaster/assetManager/partials/tile/TileDetails.vue'
|
||||||
|
import TileList from '@/components/gameMaster/assetManager/partials/tile/TileList.vue'
|
||||||
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
const assetManagerStore = useAssetManagerStore()
|
const assetManagerStore = useAssetManagerStore()
|
||||||
const selectedCategory = ref('tiles')
|
const selectedCategory = ref('tiles')
|
||||||
|
@ -0,0 +1,124 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-full overflow-auto">
|
||||||
|
<div class="p-2.5 block rounded-md default-border bg-gray">
|
||||||
|
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveCharacterHair">
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input v-model="characterName" class="input-field" type="text" name="name" placeholder="Character Type Name" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="gender">Gender</label>
|
||||||
|
<select v-model="characterGender" class="input-field" name="gender">
|
||||||
|
<option v-for="gender in genderOptions" :key="gender" :value="gender">{{ gender }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="isSelectable">Is selectable</label>
|
||||||
|
<select v-model="characterIsSelectable" class="input-field" name="isSelectable">
|
||||||
|
<option :value="false">No</option>
|
||||||
|
<option :value="true">Yes</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="spriteId">Sprite</label>
|
||||||
|
<select v-model="characterSpriteId" class="input-field" name="spriteId">
|
||||||
|
<option disabled selected value="">Select sprite</option>
|
||||||
|
<option v-for="sprite in assetManagerStore.spriteList" :key="sprite.id" :value="sprite.id">{{ sprite.name }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
||||||
|
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeCharacterHair">Remove</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { CharacterGender, CharacterHair, Sprite } from '@/application/types'
|
||||||
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const assetManagerStore = useAssetManagerStore()
|
||||||
|
|
||||||
|
const selectedCharacterHair = computed(() => assetManagerStore.selectedCharacterHair)
|
||||||
|
|
||||||
|
const characterName = ref('')
|
||||||
|
const characterGender = ref<CharacterGender>('MALE' as CharacterGender.MALE)
|
||||||
|
const characterIsSelectable = ref<boolean>(false)
|
||||||
|
const characterSpriteId = ref<string | null | undefined>(null)
|
||||||
|
|
||||||
|
const genderOptions: CharacterGender[] = ['MALE' as CharacterGender.MALE, 'FEMALE' as CharacterGender.FEMALE]
|
||||||
|
|
||||||
|
if (!selectedCharacterHair.value) {
|
||||||
|
console.error('No character hair selected')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedCharacterHair.value) {
|
||||||
|
characterName.value = selectedCharacterHair.value.name
|
||||||
|
characterGender.value = selectedCharacterHair.value.gender
|
||||||
|
characterIsSelectable.value = selectedCharacterHair.value.isSelectable
|
||||||
|
characterSpriteId.value = selectedCharacterHair.value.sprite?.id
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCharacterHair() {
|
||||||
|
if (!selectedCharacterHair.value) return
|
||||||
|
|
||||||
|
gameStore.connection?.emit('gm:characterHair:remove', { id: selectedCharacterHair.value.id }, (response: boolean) => {
|
||||||
|
if (!response) {
|
||||||
|
console.error('Failed to remove character hair')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
refreshCharacterHairList()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshCharacterHairList(unsetSelectedCharacterHair = true) {
|
||||||
|
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => {
|
||||||
|
assetManagerStore.setCharacterHairList(response)
|
||||||
|
|
||||||
|
if (unsetSelectedCharacterHair) {
|
||||||
|
assetManagerStore.setSelectedCharacterHair(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCharacterHair() {
|
||||||
|
const characterHairData = {
|
||||||
|
id: selectedCharacterHair.value!.id,
|
||||||
|
name: characterName.value,
|
||||||
|
gender: characterGender.value,
|
||||||
|
isSelectable: characterIsSelectable.value,
|
||||||
|
spriteId: characterSpriteId.value
|
||||||
|
}
|
||||||
|
|
||||||
|
gameStore.connection?.emit('gm:characterHair:update', characterHairData, (response: boolean) => {
|
||||||
|
if (!response) {
|
||||||
|
console.error('Failed to save character type')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
refreshCharacterHairList(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(selectedCharacterHair, (characterHair: CharacterHair | null) => {
|
||||||
|
if (!characterHair) return
|
||||||
|
characterName.value = characterHair.name
|
||||||
|
characterGender.value = characterHair.gender
|
||||||
|
characterIsSelectable.value = characterHair.isSelectable
|
||||||
|
characterSpriteId.value = characterHair.sprite?.id
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!selectedCharacterHair.value) return
|
||||||
|
|
||||||
|
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
||||||
|
assetManagerStore.setSpriteList(response)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
assetManagerStore.setSelectedCharacterHair(null)
|
||||||
|
})
|
||||||
|
</script>
|
@ -0,0 +1,100 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative mb-5 flex items-center gap-x-2.5">
|
||||||
|
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
|
||||||
|
<label for="create-character" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2.5 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
|
||||||
|
<button class="p-0 h-5" id="create-character" @click="createNewCharacterHair">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="white">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||||
|
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
|
||||||
|
<a
|
||||||
|
v-for="{ data: characterHair } in list"
|
||||||
|
:key="characterHair.id"
|
||||||
|
class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group"
|
||||||
|
:class="{ 'bg-cyan': assetManagerStore.selectedCharacterHair?.id === characterHair.id }"
|
||||||
|
@click="assetManagerStore.setSelectedCharacterHair(characterHair as CharacterHair)"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2.5">
|
||||||
|
<span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedCharacterHair?.id === characterHair.id }">{{ characterHair.name }}</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
||||||
|
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
||||||
|
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { CharacterHair } from '@/application/types'
|
||||||
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useVirtualList } from '@vueuse/core'
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const assetManagerStore = useAssetManagerStore()
|
||||||
|
|
||||||
|
const searchQuery = ref('')
|
||||||
|
|
||||||
|
const hasScrolled = ref(false)
|
||||||
|
const elementToScroll = ref()
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
// Trigger a re-render of the virtual list
|
||||||
|
virtualList.value?.scrollTo(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const createNewCharacterHair = () => {
|
||||||
|
gameStore.connection?.emit('gm:characterHair:create', {}, (response: boolean) => {
|
||||||
|
if (!response) {
|
||||||
|
console.error('Failed to create new character type')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => {
|
||||||
|
assetManagerStore.setCharacterHairList(response)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredCharacterHairs = computed(() => {
|
||||||
|
if (!searchQuery.value) {
|
||||||
|
return assetManagerStore.characterHairList
|
||||||
|
}
|
||||||
|
return assetManagerStore.characterHairList.filter((character) => character.name.toLowerCase().includes(searchQuery.value.toLowerCase()))
|
||||||
|
})
|
||||||
|
|
||||||
|
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(filteredCharacterHairs, {
|
||||||
|
itemHeight: 48
|
||||||
|
})
|
||||||
|
|
||||||
|
const virtualList = ref({ scrollTo })
|
||||||
|
|
||||||
|
const onScroll = () => {
|
||||||
|
let scrollTop = elementToScroll.value.style.marginTop.replace('px', '')
|
||||||
|
|
||||||
|
if (scrollTop > 80) {
|
||||||
|
hasScrolled.value = true
|
||||||
|
} else if (scrollTop <= 80) {
|
||||||
|
hasScrolled.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toTop() {
|
||||||
|
virtualList.value?.scrollTo(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => {
|
||||||
|
console.log(response)
|
||||||
|
assetManagerStore.setCharacterHairList(response)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full overflow-auto">
|
<div class="h-full overflow-auto">
|
||||||
<div class="m-2.5 p-2.5 block">
|
<div class="p-2.5 block rounded-md default-border bg-gray">
|
||||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveCharacterType">
|
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveCharacterType">
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="name">Name</label>
|
<label for="name">Name</label>
|
||||||
@ -19,8 +19,18 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="spriteId">Sprite ID</label>
|
<label for="isSelectable">Is selectable</label>
|
||||||
<input v-model="characterSpriteId" class="input-field" type="text" name="spriteId" placeholder="Sprite ID" />
|
<select v-model="characterIsSelectable" class="input-field" name="isSelectable">
|
||||||
|
<option :value="false">No</option>
|
||||||
|
<option :value="true">Yes</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="spriteId">Sprite</label>
|
||||||
|
<select v-model="characterSpriteId" class="input-field" name="spriteId">
|
||||||
|
<option disabled selected value="">Select sprite</option>
|
||||||
|
<option v-for="sprite in assetManagerStore.spriteList" :key="sprite.id" :value="sprite.id">{{ sprite.name }}</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
||||||
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeCharacterType">Remove</button>
|
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeCharacterType">Remove</button>
|
||||||
@ -30,10 +40,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { CharacterType, CharacterGender, CharacterRace } from '@/types'
|
import type { CharacterGender, CharacterRace, CharacterType, Sprite } from '@/application/types'
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const assetManagerStore = useAssetManagerStore()
|
const assetManagerStore = useAssetManagerStore()
|
||||||
@ -43,7 +53,8 @@ const selectedCharacterType = computed(() => assetManagerStore.selectedCharacter
|
|||||||
const characterName = ref('')
|
const characterName = ref('')
|
||||||
const characterGender = ref<CharacterGender>('MALE' as CharacterGender.MALE)
|
const characterGender = ref<CharacterGender>('MALE' as CharacterGender.MALE)
|
||||||
const characterRace = ref<CharacterRace>('HUMAN' as CharacterRace.HUMAN)
|
const characterRace = ref<CharacterRace>('HUMAN' as CharacterRace.HUMAN)
|
||||||
const characterSpriteId = ref('')
|
const characterIsSelectable = ref<boolean>(false)
|
||||||
|
const characterSpriteId = ref<string | null | undefined>(null)
|
||||||
|
|
||||||
const genderOptions: CharacterGender[] = ['MALE' as CharacterGender.MALE, 'FEMALE' as CharacterGender.FEMALE]
|
const genderOptions: CharacterGender[] = ['MALE' as CharacterGender.MALE, 'FEMALE' as CharacterGender.FEMALE]
|
||||||
const raceOptions: CharacterRace[] = ['HUMAN' as CharacterRace.HUMAN, 'ELF' as CharacterRace.ELF, 'DWARF' as CharacterRace.DWARF, 'ORC' as CharacterRace.ORC, 'GOBLIN' as CharacterRace.GOBLIN]
|
const raceOptions: CharacterRace[] = ['HUMAN' as CharacterRace.HUMAN, 'ELF' as CharacterRace.ELF, 'DWARF' as CharacterRace.DWARF, 'ORC' as CharacterRace.ORC, 'GOBLIN' as CharacterRace.GOBLIN]
|
||||||
@ -56,7 +67,8 @@ if (selectedCharacterType.value) {
|
|||||||
characterName.value = selectedCharacterType.value.name
|
characterName.value = selectedCharacterType.value.name
|
||||||
characterGender.value = selectedCharacterType.value.gender
|
characterGender.value = selectedCharacterType.value.gender
|
||||||
characterRace.value = selectedCharacterType.value.race
|
characterRace.value = selectedCharacterType.value.race
|
||||||
characterSpriteId.value = selectedCharacterType.value.spriteId
|
characterIsSelectable.value = selectedCharacterType.value.isSelectable
|
||||||
|
characterSpriteId.value = selectedCharacterType.value.sprite?.id
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeCharacterType() {
|
function removeCharacterType() {
|
||||||
@ -83,10 +95,11 @@ function refreshCharacterTypeList(unsetSelectedCharacterType = true) {
|
|||||||
|
|
||||||
function saveCharacterType() {
|
function saveCharacterType() {
|
||||||
const characterTypeData = {
|
const characterTypeData = {
|
||||||
id: selectedCharacterType.value?.id,
|
id: selectedCharacterType.value!.id,
|
||||||
name: characterName.value,
|
name: characterName.value,
|
||||||
gender: characterGender.value,
|
gender: characterGender.value,
|
||||||
race: characterRace.value,
|
race: characterRace.value,
|
||||||
|
isSelectable: characterIsSelectable.value,
|
||||||
spriteId: characterSpriteId.value
|
spriteId: characterSpriteId.value
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,11 +117,16 @@ watch(selectedCharacterType, (characterType: CharacterType | null) => {
|
|||||||
characterName.value = characterType.name
|
characterName.value = characterType.name
|
||||||
characterGender.value = characterType.gender
|
characterGender.value = characterType.gender
|
||||||
characterRace.value = characterType.race
|
characterRace.value = characterType.race
|
||||||
characterSpriteId.value = characterType.spriteId
|
characterIsSelectable.value = characterType.isSelectable
|
||||||
|
characterSpriteId.value = characterType.sprite?.id
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!selectedCharacterType.value) return
|
if (!selectedCharacterType.value) return
|
||||||
|
|
||||||
|
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
||||||
|
assetManagerStore.setSpriteList(response)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
@ -1,36 +1,42 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative p-2.5 flex items-center gap-x-2.5">
|
<div class="relative mb-5 flex items-center gap-x-2.5">
|
||||||
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
|
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
|
||||||
<label for="create-character" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
|
<label for="create-character" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2.5 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
|
||||||
<button class="p-0 h-5" id="create-character" @click="createNewCharacterType">
|
<button class="p-0 h-5" id="create-character" @click="createNewCharacterType">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="white">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="white">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</label>
|
</label>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-bind="containerProps" class="overflow-y-auto relative" @scroll="onScroll">
|
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||||
<div v-bind="wrapperProps" ref="elementToScroll">
|
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
|
||||||
<a v-for="{ data: characterType } in list" :key="characterType.id" class="relative p-2.5 cursor-pointer block" :class="{ 'bg-cyan/80': assetManagerStore.selectedCharacterType?.id === characterType.id }" @click="assetManagerStore.setSelectedCharacterType(characterType as CharacterType)">
|
<a
|
||||||
|
v-for="{ data: characterType } in list"
|
||||||
|
:key="characterType.id"
|
||||||
|
class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group"
|
||||||
|
:class="{ 'bg-cyan': assetManagerStore.selectedCharacterType?.id === characterType.id }"
|
||||||
|
@click="assetManagerStore.setSelectedCharacterType(characterType as CharacterType)"
|
||||||
|
>
|
||||||
<div class="flex items-center gap-2.5">
|
<div class="flex items-center gap-2.5">
|
||||||
<span>{{ characterType.name }}</span>
|
<span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedCharacterType?.id === characterType.id }">{{ characterType.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<button class="left-[calc(50%_-_60px)] fixed bottom-2.5 min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
||||||
<img class="absolute invert w-8 h-8 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
|
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
||||||
</button>
|
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import type { CharacterType } from '@/application/types'
|
||||||
import { onMounted, ref, computed } from 'vue'
|
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import type { CharacterType } from '@/types'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useVirtualList } from '@vueuse/core'
|
import { useVirtualList } from '@vueuse/core'
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const assetManagerStore = useAssetManagerStore()
|
const assetManagerStore = useAssetManagerStore()
|
||||||
@ -87,6 +93,7 @@ function toTop() {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => {
|
gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => {
|
||||||
|
console.log(response)
|
||||||
assetManagerStore.setCharacterTypeList(response)
|
assetManagerStore.setCharacterTypeList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -0,0 +1,143 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-full overflow-auto">
|
||||||
|
<div class="p-2.5 block rounded-md default-border bg-gray">
|
||||||
|
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveItem">
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input v-model="itemName" class="input-field" type="text" name="name" placeholder="Item Name" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<input v-model="itemDescription" class="input-field" type="text" name="description" placeholder="Item Description" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="itemType">Type</label>
|
||||||
|
<select v-model="itemType" class="input-field" name="itemType">
|
||||||
|
<option v-for="type in itemTypeOptions" :key="type" :value="type">{{ type }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="rarity">Rarity</label>
|
||||||
|
<select v-model="itemRarity" class="input-field" name="rarity">
|
||||||
|
<option v-for="rarity in rarityOptions" :key="rarity" :value="rarity">{{ rarity }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="stackable">Stackable</label>
|
||||||
|
<select v-model="itemStackable" class="input-field" name="stackable">
|
||||||
|
<option :value="false">No</option>
|
||||||
|
<option :value="true">Yes</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="spriteId">Sprite</label>
|
||||||
|
<select v-model="itemSpriteId" class="input-field" name="spriteId">
|
||||||
|
<option disabled selected value="">Select sprite</option>
|
||||||
|
<option v-for="sprite in assetManagerStore.spriteList" :key="sprite.id" :value="sprite.id">{{ sprite.name }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
||||||
|
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeItem">Remove</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Item, ItemRarity, ItemType, Sprite } from '@/application/types'
|
||||||
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const assetManagerStore = useAssetManagerStore()
|
||||||
|
|
||||||
|
const selectedItem = computed(() => assetManagerStore.selectedItem)
|
||||||
|
|
||||||
|
const itemName = ref('')
|
||||||
|
const itemDescription = ref('')
|
||||||
|
const itemType = ref<ItemType>('WEAPON' as ItemType)
|
||||||
|
const itemRarity = ref<ItemRarity>('COMMON' as ItemRarity)
|
||||||
|
const itemStackable = ref<boolean>(false)
|
||||||
|
const itemSpriteId = ref<string | null | undefined>(null)
|
||||||
|
|
||||||
|
const itemTypeOptions: ItemType[] = ['WEAPON', 'HELMET', 'CHEST', 'LEGS', 'BOOTS', 'GLOVES', 'RING', 'NECKLACE']
|
||||||
|
const rarityOptions: ItemRarity[] = ['COMMON', 'UNCOMMON', 'RARE', 'EPIC', 'LEGENDARY']
|
||||||
|
|
||||||
|
if (!selectedItem.value) {
|
||||||
|
console.error('No item selected')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedItem.value) {
|
||||||
|
itemName.value = selectedItem.value.name
|
||||||
|
itemDescription.value = selectedItem.value.description || ''
|
||||||
|
itemType.value = selectedItem.value.itemType
|
||||||
|
itemRarity.value = selectedItem.value.rarity
|
||||||
|
itemStackable.value = selectedItem.value.stackable
|
||||||
|
itemSpriteId.value = selectedItem.value.spriteId
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeItem() {
|
||||||
|
if (!selectedItem.value) return
|
||||||
|
|
||||||
|
gameStore.connection?.emit('gm:item:remove', { id: selectedItem.value.id }, (response: boolean) => {
|
||||||
|
if (!response) {
|
||||||
|
console.error('Failed to remove item')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
refreshItemList()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshItemList(unsetSelectedItem = true) {
|
||||||
|
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => {
|
||||||
|
assetManagerStore.setItemList(response)
|
||||||
|
|
||||||
|
if (unsetSelectedItem) {
|
||||||
|
assetManagerStore.setSelectedItem(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveItem() {
|
||||||
|
const itemData = {
|
||||||
|
id: selectedItem.value!.id,
|
||||||
|
name: itemName.value,
|
||||||
|
description: itemDescription.value,
|
||||||
|
itemType: itemType.value,
|
||||||
|
rarity: itemRarity.value,
|
||||||
|
stackable: itemStackable.value,
|
||||||
|
spriteId: itemSpriteId.value
|
||||||
|
}
|
||||||
|
|
||||||
|
gameStore.connection?.emit('gm:item:update', itemData, (response: boolean) => {
|
||||||
|
if (!response) {
|
||||||
|
console.error('Failed to save item')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
refreshItemList(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(selectedItem, (item: Item | null) => {
|
||||||
|
if (!item) return
|
||||||
|
itemName.value = item.name
|
||||||
|
itemDescription.value = item.description || ''
|
||||||
|
itemType.value = item.itemType
|
||||||
|
itemRarity.value = item.rarity
|
||||||
|
itemStackable.value = item.stackable
|
||||||
|
itemSpriteId.value = item.spriteId
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!selectedItem.value) return
|
||||||
|
|
||||||
|
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
||||||
|
assetManagerStore.setSpriteList(response)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
assetManagerStore.setSelectedItem(null)
|
||||||
|
})
|
||||||
|
</script>
|
@ -0,0 +1,95 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative mb-5 flex items-center gap-x-2.5">
|
||||||
|
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
|
||||||
|
<label for="create-item" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2.5 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
|
||||||
|
<button class="p-0 h-5" id="create-item" @click="createNewItem">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="white">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||||
|
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
|
||||||
|
<a v-for="{ data: item } in list" :key="item.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedItem?.id === item.id }" @click="assetManagerStore.setSelectedItem(item as Item)">
|
||||||
|
<div class="flex items-center gap-2.5">
|
||||||
|
<span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedItem?.id === item.id }">
|
||||||
|
{{ item.name }}
|
||||||
|
<small class="text-gray-400">({{ item.itemType }})</small>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
||||||
|
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
||||||
|
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Item } from '@/application/types'
|
||||||
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useVirtualList } from '@vueuse/core'
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const assetManagerStore = useAssetManagerStore()
|
||||||
|
|
||||||
|
const searchQuery = ref('')
|
||||||
|
|
||||||
|
const hasScrolled = ref(false)
|
||||||
|
const elementToScroll = ref()
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
virtualList.value?.scrollTo(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const createNewItem = () => {
|
||||||
|
gameStore.connection?.emit('gm:item:create', {}, (response: boolean) => {
|
||||||
|
if (!response) {
|
||||||
|
console.error('Failed to create new item')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => {
|
||||||
|
assetManagerStore.setItemList(response)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredItems = computed(() => {
|
||||||
|
if (!searchQuery.value) {
|
||||||
|
return assetManagerStore.itemList
|
||||||
|
}
|
||||||
|
return assetManagerStore.itemList.filter((item) => item.name.toLowerCase().includes(searchQuery.value.toLowerCase()) || item.itemType.toLowerCase().includes(searchQuery.value.toLowerCase()))
|
||||||
|
})
|
||||||
|
|
||||||
|
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(filteredItems, {
|
||||||
|
itemHeight: 48
|
||||||
|
})
|
||||||
|
|
||||||
|
const virtualList = ref({ scrollTo })
|
||||||
|
|
||||||
|
const onScroll = () => {
|
||||||
|
let scrollTop = elementToScroll.value.style.marginTop.replace('px', '')
|
||||||
|
|
||||||
|
if (scrollTop > 80) {
|
||||||
|
hasScrolled.value = true
|
||||||
|
} else if (scrollTop <= 80) {
|
||||||
|
hasScrolled.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toTop() {
|
||||||
|
virtualList.value?.scrollTo(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => {
|
||||||
|
assetManagerStore.setItemList(response)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
@ -0,0 +1,163 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-full overflow-auto">
|
||||||
|
<div class="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray">
|
||||||
|
<img class="max-h-56" :src="`${config.server_endpoint}/textures/map_objects/${selectedMapObject?.id}.png`" :alt="'Object ' + selectedMapObject?.id" />
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 block">
|
||||||
|
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject">
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input v-model="mapObjectName" class="input-field" type="text" name="name" placeholder="Wall #1" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field-half">
|
||||||
|
<label for="origin-x">Origin X</label>
|
||||||
|
<input v-model="mapObjectOriginX" class="input-field" type="number" step="any" name="origin-x" placeholder="Origin X" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field-half">
|
||||||
|
<label for="origin-y">Origin Y</label>
|
||||||
|
<input v-model="mapObjectOriginY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="tags">Tags</label>
|
||||||
|
<ChipsInput v-model="mapObjectTags" @update:modelValue="mapObjectTags = $event" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="is-animated">Is animated</label>
|
||||||
|
<select v-model="mapObjectIsAnimated" class="input-field" name="is-animated">
|
||||||
|
<option :value="false">No</option>
|
||||||
|
<option :value="true">Yes</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="frame-speed">Frame rate</label>
|
||||||
|
<input v-model="mapObjectFrameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field-half">
|
||||||
|
<label for="frame-width">Frame width</label>
|
||||||
|
<input v-model="mapObjectFrameWidth" class="input-field" type="number" step="any" name="frame-width" placeholder="Frame width" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field-half">
|
||||||
|
<label for="frame-height">Frame height</label>
|
||||||
|
<input v-model="mapObjectFrameHeight" class="input-field" type="number" step="any" name="frame-height" placeholder="Frame height" />
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
||||||
|
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeObject">Delete</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import config from '@/application/config'
|
||||||
|
import type { MapObject } from '@/application/types'
|
||||||
|
import ChipsInput from '@/components/forms/ChipsInput.vue'
|
||||||
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useMapEditorStore } from '@/stores/mapEditorStore'
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const assetManagerStore = useAssetManagerStore()
|
||||||
|
const mapEditorStore = useMapEditorStore()
|
||||||
|
|
||||||
|
const selectedMapObject = computed(() => assetManagerStore.selectedMapObject)
|
||||||
|
|
||||||
|
const mapObjectName = ref('')
|
||||||
|
const mapObjectTags = ref<string[]>([])
|
||||||
|
const mapObjectOriginX = ref(0)
|
||||||
|
const mapObjectOriginY = ref(0)
|
||||||
|
const mapObjectIsAnimated = ref(false)
|
||||||
|
const mapObjectFrameRate = ref(0)
|
||||||
|
const mapObjectFrameWidth = ref(0)
|
||||||
|
const mapObjectFrameHeight = ref(0)
|
||||||
|
|
||||||
|
if (!selectedMapObject.value) {
|
||||||
|
console.error('No map mapObject selected')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedMapObject.value) {
|
||||||
|
mapObjectName.value = selectedMapObject.value.name
|
||||||
|
mapObjectTags.value = selectedMapObject.value.tags
|
||||||
|
mapObjectOriginX.value = selectedMapObject.value.originX
|
||||||
|
mapObjectOriginY.value = selectedMapObject.value.originY
|
||||||
|
mapObjectIsAnimated.value = selectedMapObject.value.isAnimated
|
||||||
|
mapObjectFrameRate.value = selectedMapObject.value.frameRate
|
||||||
|
mapObjectFrameWidth.value = selectedMapObject.value.frameWidth
|
||||||
|
mapObjectFrameHeight.value = selectedMapObject.value.frameHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeObject() {
|
||||||
|
gameStore.connection?.emit('gm:mapObject:remove', { mapObject: selectedMapObject.value?.id }, (response: boolean) => {
|
||||||
|
if (!response) {
|
||||||
|
console.error('Failed to remove mapObject')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
refreshObjectList()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshObjectList(unsetSelectedMapObject = true) {
|
||||||
|
gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => {
|
||||||
|
assetManagerStore.setMapObjectList(response)
|
||||||
|
|
||||||
|
if (unsetSelectedMapObject) {
|
||||||
|
assetManagerStore.setSelectedMapObject(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapEditorStore.active) {
|
||||||
|
mapEditorStore.setMapObjectList(response)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveObject() {
|
||||||
|
if (!selectedMapObject.value) {
|
||||||
|
console.error('No mapObject selected')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gameStore.connection?.emit(
|
||||||
|
'gm:mapObject:update',
|
||||||
|
{
|
||||||
|
id: selectedMapObject.value.id,
|
||||||
|
name: mapObjectName.value,
|
||||||
|
tags: mapObjectTags.value,
|
||||||
|
originX: mapObjectOriginX.value,
|
||||||
|
originY: mapObjectOriginY.value,
|
||||||
|
isAnimated: mapObjectIsAnimated.value,
|
||||||
|
frameRate: mapObjectFrameRate.value,
|
||||||
|
frameWidth: mapObjectFrameWidth.value,
|
||||||
|
frameHeight: mapObjectFrameHeight.value
|
||||||
|
},
|
||||||
|
(response: boolean) => {
|
||||||
|
if (!response) {
|
||||||
|
console.error('Failed to save mapObject')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
refreshObjectList(false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(selectedMapObject, (mapObject: MapObject | null) => {
|
||||||
|
if (!mapObject) return
|
||||||
|
mapObjectName.value = mapObject.name
|
||||||
|
mapObjectTags.value = mapObject.tags
|
||||||
|
mapObjectOriginX.value = mapObject.originX
|
||||||
|
mapObjectOriginY.value = mapObject.originY
|
||||||
|
mapObjectIsAnimated.value = mapObject.isAnimated
|
||||||
|
mapObjectFrameRate.value = mapObject.frameRate
|
||||||
|
mapObjectFrameWidth.value = mapObject.frameWidth
|
||||||
|
mapObjectFrameHeight.value = mapObject.frameHeight
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!selectedMapObject.value) return
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
assetManagerStore.setSelectedMapObject(null)
|
||||||
|
})
|
||||||
|
</script>
|
@ -1,39 +1,39 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative p-2.5 flex items-center gap-x-2.5">
|
<div class="relative mb-5 flex items-center gap-x-2.5">
|
||||||
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
|
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
|
||||||
<label for="upload-asset" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
|
<label for="upload-asset" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2.5 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
|
||||||
<input class="hidden" id="upload-asset" ref="objectUploadField" type="file" accept="image/png" multiple @change="handleFileUpload" />
|
<input class="hidden" id="upload-asset" ref="objectUploadField" type="file" accept="image/png" multiple @change="handleFileUpload" />
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||||
</svg>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-bind="containerProps" class="overflow-y-auto relative" @scroll="onScroll">
|
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||||
<div v-bind="wrapperProps" ref="elementToScroll">
|
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
|
||||||
<a v-for="{ data: object } in list" :key="object.id" class="relative p-2.5 cursor-pointer block" :class="{ 'bg-cyan/80': assetManagerStore.selectedObject?.id === object.id }" @click="assetManagerStore.setSelectedObject(object as Object)">
|
<a v-for="{ data: mapObject } in list" :key="mapObject.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedMapObject?.id === mapObject.id }" @click="assetManagerStore.setSelectedMapObject(mapObject as MapObject)">
|
||||||
<div class="flex items-center gap-2.5">
|
<div class="flex items-center gap-2.5">
|
||||||
<div class="h-7 w-16 max-w-16 flex justify-center">
|
<div class="h-7 w-16 max-w-16 flex justify-center">
|
||||||
<img class="h-7" :src="`${config.server_endpoint}/assets/objects/${object.id}.png`" alt="Object" />
|
<img class="h-7" :src="`${config.server_endpoint}/textures/map_objects/${mapObject.id}.png`" alt="Object" />
|
||||||
</div>
|
</div>
|
||||||
<span>{{ object.name }}</span>
|
<span :class="{ 'text-white': assetManagerStore.selectedMapObject?.id === mapObject.id }">{{ mapObject.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<button class="left-[calc(50%_-_60px)] fixed bottom-2.5 min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
||||||
<img class="absolute invert w-8 h-8 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
|
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
||||||
</button>
|
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/config'
|
import config from '@/application/config'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import type { MapObject } from '@/application/types'
|
||||||
import { onMounted, ref, computed } from 'vue'
|
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import type { Object } from '@/types'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useVirtualList } from '@vueuse/core'
|
import { useVirtualList } from '@vueuse/core'
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const objectUploadField = ref(null)
|
const objectUploadField = ref(null)
|
||||||
@ -47,14 +47,14 @@ const elementToScroll = ref()
|
|||||||
const handleFileUpload = (e: Event) => {
|
const handleFileUpload = (e: Event) => {
|
||||||
const files = (e.target as HTMLInputElement).files
|
const files = (e.target as HTMLInputElement).files
|
||||||
if (!files) return
|
if (!files) return
|
||||||
gameStore.connection?.emit('gm:object:upload', files, (response: boolean) => {
|
gameStore.connection?.emit('gm:mapObject:upload', files, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
if (config.development) console.error('Failed to upload object')
|
if (config.development) console.error('Failed to upload object')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => {
|
gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => {
|
||||||
assetManagerStore.setObjectList(response)
|
assetManagerStore.setMapObjectList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -66,9 +66,9 @@ const handleSearch = () => {
|
|||||||
|
|
||||||
const filteredObjects = computed(() => {
|
const filteredObjects = computed(() => {
|
||||||
if (!searchQuery.value) {
|
if (!searchQuery.value) {
|
||||||
return assetManagerStore.objectList
|
return assetManagerStore.mapObjectList
|
||||||
}
|
}
|
||||||
return assetManagerStore.objectList.filter((object) => object.name.toLowerCase().includes(searchQuery.value.toLowerCase()))
|
return assetManagerStore.mapObjectList.filter((object) => object.name.toLowerCase().includes(searchQuery.value.toLowerCase()))
|
||||||
})
|
})
|
||||||
|
|
||||||
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(filteredObjects, {
|
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(filteredObjects, {
|
||||||
@ -92,8 +92,8 @@ function toTop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => {
|
gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => {
|
||||||
assetManagerStore.setObjectList(response)
|
assetManagerStore.setMapObjectList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
@ -1,163 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="h-full overflow-auto">
|
|
||||||
<div class="relative p-2.5 flex flex-col items-center justify-between h-72">
|
|
||||||
<div class="filler"></div>
|
|
||||||
<img class="max-h-56" :src="`${config.server_endpoint}/assets/objects/${selectedObject?.id}.png`" :alt="'Object ' + selectedObject?.id" />
|
|
||||||
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeObject">Remove</button>
|
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</div>
|
|
||||||
<div class="m-2.5 p-2.5 block">
|
|
||||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject">
|
|
||||||
<div class="form-field-full">
|
|
||||||
<label for="name">Name</label>
|
|
||||||
<input v-model="objectName" class="input-field" type="text" name="name" placeholder="Wall #1" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field-half">
|
|
||||||
<label for="origin-x">Origin X</label>
|
|
||||||
<input v-model="objectOriginX" class="input-field" type="number" step="any" name="origin-x" placeholder="Origin X" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field-half">
|
|
||||||
<label for="origin-y">Origin Y</label>
|
|
||||||
<input v-model="objectOriginY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field-full">
|
|
||||||
<label for="origin-x">Tags</label>
|
|
||||||
<ChipsInput v-model="objectTags" @update:modelValue="objectTags = $event" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field-full">
|
|
||||||
<label for="origin-x">Is animated</label>
|
|
||||||
<select v-model="objectIsAnimated" class="input-field" name="is-animated">
|
|
||||||
<option :value="false">No</option>
|
|
||||||
<option :value="true">Yes</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-field-full">
|
|
||||||
<label for="frame-speed">Frame speed</label>
|
|
||||||
<input v-model="objectFrameSpeed" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame speed" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field-half">
|
|
||||||
<label for="frame-width">Frame width</label>
|
|
||||||
<input v-model="objectFrameWidth" class="input-field" type="number" step="any" name="frame-width" placeholder="Frame width" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field-half">
|
|
||||||
<label for="frame-height">Frame height</label>
|
|
||||||
<input v-model="objectFrameHeight" class="input-field" type="number" step="any" name="frame-height" placeholder="Frame height" />
|
|
||||||
</div>
|
|
||||||
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { Object } from '@/types'
|
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import config from '@/config'
|
|
||||||
import ChipsInput from '@/components/forms/ChipsInput.vue'
|
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const assetManagerStore = useAssetManagerStore()
|
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
|
||||||
|
|
||||||
const selectedObject = computed(() => assetManagerStore.selectedObject)
|
|
||||||
|
|
||||||
const objectName = ref('')
|
|
||||||
const objectTags = ref<string[]>([])
|
|
||||||
const objectOriginX = ref(0)
|
|
||||||
const objectOriginY = ref(0)
|
|
||||||
const objectIsAnimated = ref(false)
|
|
||||||
const objectFrameSpeed = ref(0)
|
|
||||||
const objectFrameWidth = ref(0)
|
|
||||||
const objectFrameHeight = ref(0)
|
|
||||||
|
|
||||||
if (!selectedObject.value) {
|
|
||||||
console.error('No object selected')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedObject.value) {
|
|
||||||
objectName.value = selectedObject.value.name
|
|
||||||
objectTags.value = selectedObject.value.tags
|
|
||||||
objectOriginX.value = selectedObject.value.originX
|
|
||||||
objectOriginY.value = selectedObject.value.originY
|
|
||||||
objectIsAnimated.value = selectedObject.value.isAnimated
|
|
||||||
objectFrameSpeed.value = selectedObject.value.frameSpeed
|
|
||||||
objectFrameWidth.value = selectedObject.value.frameWidth
|
|
||||||
objectFrameHeight.value = selectedObject.value.frameHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeObject() {
|
|
||||||
gameStore.connection?.emit('gm:object:remove', { object: selectedObject.value?.id }, (response: boolean) => {
|
|
||||||
if (!response) {
|
|
||||||
console.error('Failed to remove object')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
refreshObjectList()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshObjectList(unsetSelectedObject = true) {
|
|
||||||
gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => {
|
|
||||||
assetManagerStore.setObjectList(response)
|
|
||||||
|
|
||||||
if (unsetSelectedObject) {
|
|
||||||
assetManagerStore.setSelectedObject(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (zoneEditorStore.active) {
|
|
||||||
zoneEditorStore.setObjectList(response)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveObject() {
|
|
||||||
if (!selectedObject.value) {
|
|
||||||
console.error('No object selected')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
gameStore.connection?.emit(
|
|
||||||
'gm:object:update',
|
|
||||||
{
|
|
||||||
id: selectedObject.value.id,
|
|
||||||
name: objectName.value,
|
|
||||||
tags: objectTags.value,
|
|
||||||
originX: objectOriginX.value,
|
|
||||||
originY: objectOriginY.value,
|
|
||||||
isAnimated: objectIsAnimated.value,
|
|
||||||
frameSpeed: objectFrameSpeed.value,
|
|
||||||
frameWidth: objectFrameWidth.value,
|
|
||||||
frameHeight: objectFrameHeight.value
|
|
||||||
},
|
|
||||||
(response: boolean) => {
|
|
||||||
if (!response) {
|
|
||||||
console.error('Failed to save object')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
refreshObjectList(false)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(selectedObject, (object: Object | null) => {
|
|
||||||
if (!object) return
|
|
||||||
objectName.value = object.name
|
|
||||||
objectTags.value = object.tags
|
|
||||||
objectOriginX.value = object.originX
|
|
||||||
objectOriginY.value = object.originY
|
|
||||||
objectIsAnimated.value = object.isAnimated
|
|
||||||
objectFrameSpeed.value = object.frameSpeed
|
|
||||||
objectFrameWidth.value = object.frameWidth
|
|
||||||
objectFrameHeight.value = object.frameHeight
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (!selectedObject.value) return
|
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
assetManagerStore.setSelectedObject(null)
|
|
||||||
})
|
|
||||||
</script>
|
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full overflow-auto">
|
<div class="h-full overflow-auto">
|
||||||
<div class="relative p-4 flex flex-col">
|
<div class="relative flex flex-col">
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2 p-2.5 rounded-md default-border bg-gray">
|
||||||
<div class="w-full flex flex-col">
|
<div class="w-full flex flex-col">
|
||||||
<label class="mb-1.5 font-titles" for="name">Name</label>
|
<label class="mb-1.5 font-titles" for="name">Name</label>
|
||||||
<input v-model="spriteName" class="input-field" type="text" name="name" placeholder="New sprite" />
|
<input v-model="spriteName" class="input-field" type="text" name="name" placeholder="New sprite" />
|
||||||
@ -10,7 +10,11 @@
|
|||||||
<div class="w-full flex gap-2 mt-2 pb-4 relative">
|
<div class="w-full flex gap-2 mt-2 pb-4 relative">
|
||||||
<button class="btn-cyan px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="saveSprite">Save</button>
|
<button class="btn-cyan px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="saveSprite">Save</button>
|
||||||
<button class="btn-red px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="deleteSprite">Delete</button>
|
<button class="btn-red px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="deleteSprite">Delete</button>
|
||||||
<div class="w-[calc(100%_+_32px)] absolute left-[-15px] bottom-0 h-px bg-gray-500"></div>
|
<button class="btn bg-indigo-500 hover:bg-indigo-600 rounded text-white px-4 py-2 flex-1 sm:flex-none" type="button" @click.prevent="copySprite">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -51,8 +55,8 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-full" v-if="action.isAnimated">
|
<div class="form-field-full" v-if="action.isAnimated">
|
||||||
<label for="frame-speed">Frame speed</label>
|
<label for="frame-speed">Frame rate</label>
|
||||||
<input v-model.number="action.frameSpeed" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame speed" />
|
<input v-model.number="action.frameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<SpriteActionsInput v-model="action.sprites" />
|
<SpriteActionsInput v-model="action.sprites" />
|
||||||
@ -65,13 +69,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Sprite, SpriteAction } from '@/types'
|
import type { Sprite, SpriteAction } from '@/application/types'
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { uuidv4 } from '@/application/utilities'
|
||||||
|
import SpriteActionsInput from '@/components/gameMaster/assetManager/partials/sprite/partials/SpriteImagesInput.vue'
|
||||||
|
import Accordion from '@/components/utilities/Accordion.vue'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import Accordion from '@/components/utilities/Accordion.vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import SpriteActionsInput from '@/components/gameMaster/assetManager/partials/sprite/partials/SpriteImagesInput.vue'
|
|
||||||
import { uuidv4 } from '@/utilities'
|
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const assetManagerStore = useAssetManagerStore()
|
const assetManagerStore = useAssetManagerStore()
|
||||||
@ -87,7 +91,7 @@ if (!selectedSprite.value) {
|
|||||||
|
|
||||||
if (selectedSprite.value) {
|
if (selectedSprite.value) {
|
||||||
spriteName.value = selectedSprite.value.name
|
spriteName.value = selectedSprite.value.name
|
||||||
spriteActions.value = selectedSprite.value.spriteActions
|
spriteActions.value = sortSpriteActions(selectedSprite.value.spriteActions)
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteSprite() {
|
function deleteSprite() {
|
||||||
@ -100,6 +104,16 @@ function deleteSprite() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function copySprite() {
|
||||||
|
gameStore.connection?.emit('gm:sprite:copy', { id: selectedSprite.value?.id }, (response: boolean) => {
|
||||||
|
if (!response) {
|
||||||
|
console.error('Failed to copy sprite')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
refreshSpriteList(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function refreshSpriteList(unsetSelectedSprite = true) {
|
function refreshSpriteList(unsetSelectedSprite = true) {
|
||||||
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
||||||
assetManagerStore.setSpriteList(response)
|
assetManagerStore.setSpriteList(response)
|
||||||
@ -128,7 +142,7 @@ function saveSprite() {
|
|||||||
originY: action.originY,
|
originY: action.originY,
|
||||||
isAnimated: action.isAnimated,
|
isAnimated: action.isAnimated,
|
||||||
isLooping: action.isLooping,
|
isLooping: action.isLooping,
|
||||||
frameSpeed: action.frameSpeed,
|
frameRate: action.frameRate,
|
||||||
frameWidth: action.frameWidth,
|
frameWidth: action.frameWidth,
|
||||||
frameHeight: action.frameHeight
|
frameHeight: action.frameHeight
|
||||||
}
|
}
|
||||||
@ -148,7 +162,7 @@ function addNewImage() {
|
|||||||
if (!selectedSprite.value) return
|
if (!selectedSprite.value) return
|
||||||
|
|
||||||
const newImage: SpriteAction = {
|
const newImage: SpriteAction = {
|
||||||
id: uuidv4(), // Temporary ID, should be replaced by server-generated ID
|
id: uuidv4(),
|
||||||
spriteId: selectedSprite.value.id,
|
spriteId: selectedSprite.value.id,
|
||||||
sprite: selectedSprite.value,
|
sprite: selectedSprite.value,
|
||||||
action: 'new_action',
|
action: 'new_action',
|
||||||
@ -157,7 +171,7 @@ function addNewImage() {
|
|||||||
originY: 0,
|
originY: 0,
|
||||||
isAnimated: false,
|
isAnimated: false,
|
||||||
isLooping: false,
|
isLooping: false,
|
||||||
frameSpeed: 0,
|
frameRate: 0,
|
||||||
frameWidth: 0,
|
frameWidth: 0,
|
||||||
frameHeight: 0
|
frameHeight: 0
|
||||||
}
|
}
|
||||||
@ -166,13 +180,18 @@ function addNewImage() {
|
|||||||
spriteActions.value = []
|
spriteActions.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
spriteActions.value.push(newImage)
|
spriteActions.value = sortSpriteActions([...spriteActions.value, newImage])
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortSpriteActions(actions: SpriteAction[]): SpriteAction[] {
|
||||||
|
if (!actions) return []
|
||||||
|
return [...actions].sort((a, b) => a.action.localeCompare(b.action))
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(selectedSprite, (sprite: Sprite | null) => {
|
watch(selectedSprite, (sprite: Sprite | null) => {
|
||||||
if (!sprite) return
|
if (!sprite) return
|
||||||
spriteName.value = sprite.name
|
spriteName.value = sprite.name
|
||||||
spriteActions.value = sprite.spriteActions
|
spriteActions.value = sortSpriteActions(sprite.spriteActions)
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
@ -1,35 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative p-2.5 flex items-center gap-x-2.5">
|
<div class="relative mb-5 flex items-center gap-x-2.5">
|
||||||
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
|
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
|
||||||
<button @click.prevent="newButtonClickHandler" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
|
<button @click.prevent="newButtonClickHandler" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2.5 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-bind="containerProps" class="overflow-y-auto relative" @scroll="onScroll">
|
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||||
<div v-bind="wrapperProps" ref="elementToScroll">
|
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
|
||||||
<a v-for="{ data: sprite } in list" :key="sprite.id" class="relative p-2.5 cursor-pointer block" :class="{ 'bg-cyan/80': assetManagerStore.selectedSprite?.id === sprite.id }" @click="assetManagerStore.setSelectedSprite(sprite as Sprite)">
|
<a v-for="{ data: sprite } in list" :key="sprite.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedSprite?.id === sprite.id }" @click="assetManagerStore.setSelectedSprite(sprite as Sprite)">
|
||||||
<div class="flex items-center gap-2.5">
|
<div class="flex items-center gap-2.5">
|
||||||
<span>{{ sprite.name }}</span>
|
<span :class="{ 'text-white': assetManagerStore.selectedSprite?.id === sprite.id }">{{ sprite.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<button class="left-[calc(50%_-_60px)] fixed bottom-2.5 min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
||||||
<img class="absolute invert w-8 h-8 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
|
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
||||||
</button>
|
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/config'
|
import config from '@/application/config'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import type { Sprite } from '@/application/types'
|
||||||
import { onMounted, ref, computed } from 'vue'
|
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useVirtualList } from '@vueuse/core'
|
import { useVirtualList } from '@vueuse/core'
|
||||||
import type { Sprite } from '@/types'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const assetManagerStore = useAssetManagerStore()
|
const assetManagerStore = useAssetManagerStore()
|
||||||
|
@ -1,14 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-wrap gap-3">
|
<div class="flex flex-wrap gap-3">
|
||||||
<div v-for="(image, index) in modelValue" :key="index" class="h-20 w-20 p-4 bg-gray-50 bg-opacity-50 rounded text-center relative group cursor-move" draggable="true" @dragstart="dragStart($event, index)" @dragover.prevent @dragenter.prevent @drop="drop($event, index)">
|
<div v-for="(image, index) in modelValue" :key="index" class="h-20 w-20 p-4 bg-gray-300 bg-opacity-50 rounded text-center relative group cursor-move" draggable="true" @dragstart="dragStart($event, index)" @dragover.prevent @dragenter.prevent @drop="drop($event, index)">
|
||||||
<img :src="image" class="max-w-full max-h-full object-contain pointer-events-none" alt="Uploaded image" />
|
<img :src="image" class="max-w-full max-h-full object-contain pointer-events-none" alt="Uploaded image" />
|
||||||
<button @click.stop="deleteImage(index)" class="absolute top-1 right-1 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" aria-label="Delete image">
|
<div class="absolute top-1 left-1 flex-row space-y-1">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<button @click.stop="deleteImage(index)" class="bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" aria-label="Delete image">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
</svg>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
</button>
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="bg-blue-500 text-white rounded-full w-6 h-6 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" aria-label="Scope image">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-20 w-20 p-4 bg-gray-100 bg-opacity-50 rounded justify-center items-center flex hover:cursor-pointer" @click="triggerFileInput" @drop.prevent="onDrop" @dragover.prevent>
|
<div class="h-20 w-20 p-4 bg-gray-200 bg-opacity-50 rounded justify-center items-center flex hover:cursor-pointer" @click="triggerFileInput" @drop.prevent="onDrop" @dragover.prevent>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 invert" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 invert" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||||
</svg>
|
</svg>
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full overflow-auto">
|
<div class="h-full overflow-auto">
|
||||||
<div class="relative p-2.5 flex flex-col items-center justify-between h-72">
|
<div class="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray">
|
||||||
<div class="filler"></div>
|
<img class="max-h-72" :src="`${config.server_endpoint}/textures/tiles/${selectedTile?.id}.png`" :alt="'Tile ' + selectedTile?.id" />
|
||||||
<img class="max-h-72" :src="`${config.server_endpoint}/assets/tiles/${selectedTile?.id}.png`" :alt="'Tile ' + selectedTile?.id" />
|
|
||||||
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="deleteTile">Delete</button>
|
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="m-2.5 p-2.5 block">
|
<div class="mt-5 block">
|
||||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveTile">
|
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveTile">
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="name">Name</label>
|
<label for="name">Name</label>
|
||||||
@ -16,24 +13,27 @@
|
|||||||
<label for="origin-x">Tags</label>
|
<label for="origin-x">Tags</label>
|
||||||
<ChipsInput v-model="tileTags" @update:modelValue="tileTags = $event" />
|
<ChipsInput v-model="tileTags" @update:modelValue="tileTags = $event" />
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
<div class="flex gap-4">
|
||||||
|
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
||||||
|
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="deleteTile">Delete</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Tile } from '@/types'
|
import config from '@/application/config'
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue'
|
import type { Tile } from '@/application/types'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import config from '@/config'
|
|
||||||
import ChipsInput from '@/components/forms/ChipsInput.vue'
|
import ChipsInput from '@/components/forms/ChipsInput.vue'
|
||||||
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useMapEditorStore } from '@/stores/mapEditorStore'
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const assetManagerStore = useAssetManagerStore()
|
const assetManagerStore = useAssetManagerStore()
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const mapEditorStore = useMapEditorStore()
|
||||||
|
|
||||||
const selectedTile = computed(() => assetManagerStore.selectedTile)
|
const selectedTile = computed(() => assetManagerStore.selectedTile)
|
||||||
|
|
||||||
@ -73,8 +73,8 @@ function refreshTileList(unsetSelectedTile = true) {
|
|||||||
assetManagerStore.setSelectedTile(null)
|
assetManagerStore.setSelectedTile(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (zoneEditorStore.active) {
|
if (mapEditorStore.active) {
|
||||||
zoneEditorStore.setTileList(response)
|
mapEditorStore.setTileList(response)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|