Compare commits
119 Commits
feature/#2
...
feature/#2
Author | SHA1 | Date | |
---|---|---|---|
c1360899e1 | |||
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 |
@ -1,4 +1,4 @@
|
|||||||
VITE_NAME=Sylvan Quest
|
VITE_NAME=Noxious
|
||||||
VITE_DEVELOPMENT=true
|
VITE_DEVELOPMENT=true
|
||||||
VITE_SERVER_ENDPOINT=http://localhost:4000
|
VITE_SERVER_ENDPOINT=http://localhost:4000
|
||||||
VITE_TILE_SIZE_X=64
|
VITE_TILE_SIZE_X=64
|
||||||
|
@ -4,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>
|
||||||
|
1434
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",
|
||||||
|
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: 915 B After Width: | Height: | Size: 918 B |
Before Width: | Height: | Size: 768 B After Width: | Height: | Size: 768 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 375 B After Width: | Height: | Size: 375 B |
Before Width: | Height: | Size: 652 B After Width: | Height: | Size: 652 B |
Before Width: | Height: | Size: 346 B After Width: | Height: | Size: 346 B |
@ -1 +0,0 @@
|
|||||||
<svg width="48" xmlns="http://www.w3.org/2000/svg" height="48" id="screenshot-e9346f42-72c8-800c-8004-507b356b7f18" viewBox="0 0 48 48" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-e9346f42-72c8-800c-8004-507b356b7f18" width="1em" height="1em" rx="0" ry="0" style="fill: rgb(0, 0, 0);"><g id="shape-e9346f42-72c8-800c-8004-507b356b7f1b"><defs style="fill: rgb(0, 0, 0);"/></g><g id="shape-e9346f42-72c8-800c-8004-507b356b7f1c"><defs><mask width="1.2" height="1.2" x="-0.1" id="render-3-ipSEnterKey0" data-old-y="-0.1" data-old-width="1.2" data-old-x="-0.1" y="-0.1" data-old-height="1.2"><g class="svg-mask-wrapper" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"><path fill="#fff" stroke="#fff" d="M44 44V4H24v16H4v24z"/><path stroke="#000" d="m21 28l-4 4l4 4"/><path stroke="#000" d="M34 23v9H17"/></g></g></mask></defs><g class="fills" id="fills-e9346f42-72c8-800c-8004-507b356b7f1c"><path d="M0.000,0.000L48.000,0.000L48.000,48.000L0.000,48.000ZZ" mask="url(#render-3-ipSEnterKey0)" style="fill: #696969; fill-opacity: 1;"/></g></g></g></svg>
|
|
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 2.1 KiB |
3
public/assets/icons/triangle-icon.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="13" height="16" viewBox="0 0 13 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M1.00044 7.42339L12.0005 1.07252C12.3193 0.888385 12.7271 0.997653 12.9111 1.31652C12.9697 1.41785 13.0005 1.53285 13.0005 1.64985L13.0005 14.3516C13.0005 14.7198 12.702 15.0182 12.3338 15.0182C12.2167 15.0182 12.1018 14.9874 12.0005 14.9289L1.00044 8.57805C0.681573 8.39399 0.572326 7.98625 0.756419 7.66739C0.814932 7.56605 0.899092 7.48185 1.00044 7.42339Z" fill="#4D4D4D"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 490 B |
3
public/assets/icons/x-button-gray.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8.5859 10.0001L0.792969 2.20718L2.20718 0.792969L10.0001 8.58582L17.793 0.792969L19.2072 2.20718L11.4143 10.0001L19.2072 17.7929L17.793 19.2072L10.0001 11.4143L2.20718 19.2072L0.792969 17.7929L8.5859 10.0001Z" fill="#999999"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 340 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 274 B |
@ -1 +0,0 @@
|
|||||||
<svg width="170" xmlns="http://www.w3.org/2000/svg" height="275" id="screenshot-c7008730-586b-8052-8004-79a2f5807f5c" viewBox="0 0 170 275" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-c7008730-586b-8052-8004-79a2f5807f5c"><defs><linearGradient id="fill-color-gradient-render-1-0" x1="0.4238862507773219" y1="0.26897966364321624" x2="0.7513711107948178" y2="0.9146156628521559" gradientTransform=""><stop offset="0" stop-color="#ffffff" stop-opacity="0.4"/><stop offset="1" stop-color="#ffffff" stop-opacity="0"/></linearGradient><pattern patternUnits="userSpaceOnUse" x="0" y="0" width="170" height="275" patternTransform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)" id="fill-0-render-1"><g><rect width="170" height="275" style="fill: url("#fill-color-gradient-render-1-0");"/></g></pattern></defs><g class="fills" id="fills-c7008730-586b-8052-8004-79a2f5807f5c"><path d="M170.000,20.000L170.000,255.000C170.000,266.038,161.038,275.000,150.000,275.000L20.000,275.000C8.962,275.000,0.000,266.038,0.000,255.000L0.000,20.000C0.000,8.962,8.962,0.000,20.000,0.000L150.000,0.000C150.000,0.000,150.000,0.000,150.000,0.000C150.000,11.038,158.962,20.000,170.000,20.000Z" fill="url(#fill-0-render-1)"/></g><g id="strokes-c7008730-586b-8052-8004-79a2f5807f5c" class="strokes"><g class="inner-stroke-shape"><defs><clipPath id="inner-stroke-render-1-c7008730-586b-8052-8004-79a2f5807f5c-0"><use href="#stroke-shape-render-1-c7008730-586b-8052-8004-79a2f5807f5c-0"/></clipPath><path d="M170.000,20.000L170.000,255.000C170.000,266.038,161.038,275.000,150.000,275.000L20.000,275.000C8.962,275.000,0.000,266.038,0.000,255.000L0.000,20.000C0.000,8.962,8.962,0.000,20.000,0.000L150.000,0.000C150.000,0.000,150.000,0.000,150.000,0.000C150.000,11.038,158.962,20.000,170.000,20.000Z" id="stroke-shape-render-1-c7008730-586b-8052-8004-79a2f5807f5c-0" style="fill: none; stroke-width: 4; stroke: rgb(255, 255, 255); stroke-opacity: 1;"/></defs><use href="#stroke-shape-render-1-c7008730-586b-8052-8004-79a2f5807f5c-0" clip-path="url('#inner-stroke-render-1-c7008730-586b-8052-8004-79a2f5807f5c-0')"/></g></g></g></svg>
|
|
Before Width: | Height: | Size: 2.1 KiB |
@ -1 +0,0 @@
|
|||||||
<svg width="290" xmlns="http://www.w3.org/2000/svg" height="87" id="screenshot-e9942e24-155b-8096-8004-7eaff9882cd6" viewBox="0 0 290 87" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-e9942e24-155b-8096-8004-7eaff9882cd6"><g class="fills" id="fills-e9942e24-155b-8096-8004-7eaff9882cd6"><path d="M3.485,8.722C7.089,3.457,13.144,0.000,20.000,0.000L270.000,0.000C281.038,0.000,290.000,8.962,290.000,20.000L290.000,67.000C290.000,78.038,281.038,87.000,270.000,87.000L20.000,87.000C13.166,87.000,7.128,83.565,3.520,78.329C21.157,76.589,35.000,61.648,35.000,43.500C35.000,25.390,21.216,10.475,3.485,8.722ZM0.000,67.000L0.000,20.000"/></g><g id="strokes-e9942e24-155b-8096-8004-7eaff9882cd6" class="strokes"><g class="inner-stroke-shape"><defs><clipPath id="inner-stroke-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0"><use href="#stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0"/></clipPath><path d="M3.485,8.722C7.089,3.457,13.144,0.000,20.000,0.000L270.000,0.000C281.038,0.000,290.000,8.962,290.000,20.000L290.000,67.000C290.000,78.038,281.038,87.000,270.000,87.000L20.000,87.000C13.166,87.000,7.128,83.565,3.520,78.329C21.157,76.589,35.000,61.648,35.000,43.500C35.000,25.390,21.216,10.475,3.485,8.722ZM0.000,67.000L0.000,20.000" id="stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0" style="fill: none; stroke-width: 6; stroke: rgb(77 77 77); stroke-opacity: 1;"/></defs><use href="#stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0" clip-path="url('#inner-stroke-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0')"/></g></g></g></svg>
|
|
Before Width: | Height: | Size: 1.6 KiB |
@ -1 +0,0 @@
|
|||||||
<svg width="290" xmlns="http://www.w3.org/2000/svg" height="87" id="screenshot-e9942e24-155b-8096-8004-7eaff9882cd6" viewBox="0 0 290 87" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-e9942e24-155b-8096-8004-7eaff9882cd6"><g class="fills" id="fills-e9942e24-155b-8096-8004-7eaff9882cd6"><path d="M3.485,8.722C7.089,3.457,13.144,0.000,20.000,0.000L270.000,0.000C281.038,0.000,290.000,8.962,290.000,20.000L290.000,67.000C290.000,78.038,281.038,87.000,270.000,87.000L20.000,87.000C13.166,87.000,7.128,83.565,3.520,78.329C21.157,76.589,35.000,61.648,35.000,43.500C35.000,25.390,21.216,10.475,3.485,8.722ZM0.000,67.000L0.000,20.000" style="fill: rgb(127, 127, 127); fill-opacity: 0.7;"/></g><g id="strokes-e9942e24-155b-8096-8004-7eaff9882cd6" class="strokes"><g class="inner-stroke-shape"><defs><clipPath id="inner-stroke-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0"><use href="#stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0"/></clipPath><path d="M3.485,8.722C7.089,3.457,13.144,0.000,20.000,0.000L270.000,0.000C281.038,0.000,290.000,8.962,290.000,20.000L290.000,67.000C290.000,78.038,281.038,87.000,270.000,87.000L20.000,87.000C13.166,87.000,7.128,83.565,3.520,78.329C21.157,76.589,35.000,61.648,35.000,43.500C35.000,25.390,21.216,10.475,3.485,8.722ZM0.000,67.000L0.000,20.000" id="stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0" style="fill: none; stroke-width: 6; stroke: rgb(255, 255, 255); stroke-opacity: 1;"/></defs><use href="#stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0" clip-path="url('#inner-stroke-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0')"/></g></g></g></svg>
|
|
Before Width: | Height: | Size: 1.6 KiB |
@ -1 +0,0 @@
|
|||||||
<svg width="1508.086" xmlns="http://www.w3.org/2000/svg" height="1511.251" id="screenshot-0d120e2a-8725-8061-8004-79728483f7ea" viewBox="-201.784 -208.012 1508.086 1511.251" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-0d120e2a-8725-8061-8004-79728483f7ea" width="800px" height="800px" rx="0" ry="0" style="opacity: 0.3; fill: rgb(0, 0, 0);"><g id="shape-0d120e2a-8725-8061-8004-79728484b3fe"><g class="fills" id="fills-0d120e2a-8725-8061-8004-79728484b3fe"><path d="M1190.359,745.690L1043.367,575.945L1133.504,630.603C1099.180,585.722,1047.978,532.622,975.722,469.519L780.783,401.898L896.468,404.538C851.234,371.382,797.068,339.090,738.130,311.073L601.350,337.338L689.349,289.039C627.425,263.088,562.143,241.922,497.713,229.160C430.172,215.674,363.453,211.534,303.512,221.641L314.753,151.382C271.664,177.012,239.130,209.992,214.226,251.017L204.390,177.710C166.181,212.950,148.095,250.172,143.131,301.343C69.092,307.974,-2.300,327.925,-73.861,347.005L-63.628,384.898C361.675,238.903,753.109,407.667,987.467,615.054L960.305,643.506C749.259,458.743,490.332,358.712,193.406,380.541C209.110,415.226,228.858,447.126,251.288,474.785L371.998,430.671L277.417,504.449C294.635,521.771,312.591,536.670,331.111,548.765L396.081,470.998L358.366,564.253C377.625,573.924,397.183,580.217,416.674,582.837C534.164,599.232,652.310,618.566,782.785,703.535L773.618,601.955L831.163,737.792C852.261,754.489,876.370,771.191,897.168,782.701L861.169,684.190L960.409,811.519C976.589,817.512,992.991,822.477,1008.953,826.486C1066.083,840.287,1120.015,842.594,1157.256,819.002C1175.975,807.393,1189.205,786.963,1190.791,762.853C1191.080,756.933,1190.907,750.762,1190.359,745.690ZZ" style="fill: rgb(13 109 105);"/></g></g></g></svg>
|
|
Before Width: | Height: | Size: 1.7 KiB |
BIN
public/assets/tlogo.png
Normal file
After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 470 KiB After Width: | Height: | Size: 470 KiB |
Before Width: | Height: | Size: 471 KiB After Width: | Height: | Size: 471 KiB |
Before Width: | Height: | Size: 598 KiB After Width: | Height: | Size: 598 KiB |
25
public/assets/ui-elements/character-select-ui-shape.svg
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<svg width="190" height="202" viewBox="0 0 190 202" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0 8H8V0C7.40741 4.14815 4.14815 7.40741 0 8Z" fill="#1A1A1A"/>
|
||||||
|
<path d="M7.55 0V0C7.55 4.16975 4.16975 7.55 0 7.55V7.55" stroke="#4D4D4D"/>
|
||||||
|
<mask id="path-3-inside-1_632_705" fill="white">
|
||||||
|
<path d="M0 8H8V194H0V8Z"/>
|
||||||
|
</mask>
|
||||||
|
<path d="M0 8H8V194H0V8Z" fill="#1A1A1A"/>
|
||||||
|
<path d="M1 194V8H-1V194H1Z" fill="#4D4D4D" mask="url(#path-3-inside-1_632_705)"/>
|
||||||
|
<path d="M0 194H8V202C7.40741 197.852 4.14815 194.593 0 194Z" fill="#1A1A1A"/>
|
||||||
|
<path d="M7.55 202V202C7.55 197.83 4.16975 194.45 0 194.45V194.45" stroke="#4D4D4D"/>
|
||||||
|
<mask id="path-7-inside-2_632_705" fill="white">
|
||||||
|
<path d="M8 0H182V202H8V0Z"/>
|
||||||
|
</mask>
|
||||||
|
<path d="M8 0H182V202H8V0Z" fill="#1A1A1A"/>
|
||||||
|
<path d="M8 1H182V-1H8V1ZM182 201H8V203H182V201Z" fill="#4D4D4D" mask="url(#path-7-inside-2_632_705)"/>
|
||||||
|
<path d="M190 8H182V0C182.593 4.14815 185.852 7.40741 190 8Z" fill="#1A1A1A"/>
|
||||||
|
<path d="M182.45 0V0C182.45 4.16975 185.83 7.55 190 7.55V7.55" stroke="#4D4D4D"/>
|
||||||
|
<mask id="path-11-inside-3_632_705" fill="white">
|
||||||
|
<path d="M190 8H182V194H190V8Z"/>
|
||||||
|
</mask>
|
||||||
|
<path d="M190 8H182V194H190V8Z" fill="#1A1A1A"/>
|
||||||
|
<path d="M189 194V8H191V194H189Z" fill="#4D4D4D" mask="url(#path-11-inside-3_632_705)"/>
|
||||||
|
<path d="M190 194H182V202C182.593 197.852 185.852 194.593 190 194Z" fill="#1A1A1A"/>
|
||||||
|
<path d="M182.45 202V202C182.45 197.83 185.83 194.45 190 194.45V194.45" stroke="#4D4D4D"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 470 KiB After Width: | Height: | Size: 470 KiB |
23
public/assets/ui-elements/login-ui-box-inner.svg
Normal file
After Width: | Height: | Size: 302 KiB |
23
public/assets/ui-elements/login-ui-box-outer.svg
Normal file
After Width: | Height: | Size: 400 KiB |
Before Width: | Height: | Size: 301 KiB |
Before Width: | Height: | Size: 400 KiB |
Before Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 2.1 MiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 1.1 MiB |
24
src/App.vue
@ -1,20 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<Notifications />
|
<Notifications />
|
||||||
<GmTools v-if="gameStore.character?.role === 'gm'" />
|
<BackgroundImageLoader />
|
||||||
<GmPanel v-if="gameStore.character?.role === 'gm'" />
|
<GmPanel v-if="gameStore.character?.role === 'gm'" />
|
||||||
<component :is="currentScreen" />
|
<component :is="currentScreen" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<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 '@/components/screens/Login.vue'
|
|
||||||
import Characters from '@/components/screens/Characters.vue'
|
import Characters from '@/components/screens/Characters.vue'
|
||||||
import Game from '@/components/screens/Game.vue'
|
import Game from '@/components/screens/Game.vue'
|
||||||
|
import Login from '@/components/screens/Login.vue'
|
||||||
import ZoneEditor from '@/components/screens/ZoneEditor.vue'
|
import ZoneEditor from '@/components/screens/ZoneEditor.vue'
|
||||||
|
import BackgroundImageLoader from '@/components/utilities/BackgroundImageLoader.vue'
|
||||||
|
import Notifications from '@/components/utilities/Notifications.vue'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
import { computed, watch } from 'vue'
|
import { computed, watch } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
@ -44,4 +44,16 @@ addEventListener('click', (event) => {
|
|||||||
const audio = new Audio('/assets/music/click-btn.mp3')
|
const audio = new Audio('/assets/music/click-btn.mp3')
|
||||||
audio.play()
|
audio.play()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Watch for "G" key press and toggle the gm panel
|
||||||
|
addEventListener('keydown', (event) => {
|
||||||
|
if (gameStore.character?.role !== 'gm') return // Only allow toggling the gm panel if the character is a gm
|
||||||
|
|
||||||
|
// Check if no input is active or focus is on an input
|
||||||
|
if (event.repeat || event.isComposing || event.defaultPrevented || document.activeElement?.tagName.toUpperCase() === 'INPUT' || document.activeElement?.tagName.toUpperCase() === 'TEXTAREA') return
|
||||||
|
|
||||||
|
if (event.key === 'G') {
|
||||||
|
gameStore.toggleGmPanel()
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import config from '@/config'
|
import config from '@/application/config'
|
||||||
|
import type { AssetDataT } from '@/application/types'
|
||||||
import Dexie from 'dexie'
|
import Dexie from 'dexie'
|
||||||
import type { AssetDataT } from '@/types'
|
|
||||||
|
|
||||||
export class AssetStorage {
|
export class Assets {
|
||||||
private db: Dexie
|
private db: Dexie
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -25,7 +25,19 @@ export class AssetStorage {
|
|||||||
const blob = await response.blob()
|
const blob = await response.blob()
|
||||||
|
|
||||||
// Store the asset in the database
|
// Store the asset in the database
|
||||||
await this.db.table('assets').put({ key: asset.key, data: blob, group: asset.group, updatedAt: asset.updatedAt, frameCount: asset.frameCount, frameWidth: asset.frameWidth, frameHeight: asset.frameHeight })
|
await this.db.table('assets').put({
|
||||||
|
key: asset.key,
|
||||||
|
data: blob,
|
||||||
|
group: asset.group,
|
||||||
|
updatedAt: asset.updatedAt,
|
||||||
|
originX: asset.originX,
|
||||||
|
originY: asset.originY,
|
||||||
|
isAnimated: asset.isAnimated,
|
||||||
|
frameRate: asset.frameRate,
|
||||||
|
frameWidth: asset.frameWidth,
|
||||||
|
frameHeight: asset.frameHeight,
|
||||||
|
frameCount: asset.frameCount
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to add asset ${asset.key}:`, error)
|
console.error(`Failed to add asset ${asset.key}:`, error)
|
||||||
}
|
}
|
@ -1,22 +1,33 @@
|
|||||||
|
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 HttpResponse<T> = {
|
||||||
|
success: boolean
|
||||||
|
message?: string
|
||||||
|
data?: T
|
||||||
|
}
|
||||||
|
|
||||||
export type AssetDataT = {
|
export type AssetDataT = {
|
||||||
key: string
|
key: string
|
||||||
data: string
|
data: string
|
||||||
group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
|
group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
|
originX?: number
|
||||||
|
originY?: number
|
||||||
isAnimated?: boolean
|
isAnimated?: boolean
|
||||||
frameCount?: number
|
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
|
||||||
@ -24,13 +35,13 @@ export type Tile = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Object = {
|
export type Object = {
|
||||||
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
|
||||||
@ -39,17 +50,23 @@ export type Object = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
spriteId: UUID | null
|
||||||
|
sprite?: Sprite
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
characters: CharacterItem[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ItemType = 'WEAPON' | 'HELMET' | 'CHEST' | 'LEGS' | 'BOOTS' | 'GLOVES' | 'RING' | 'NECKLACE'
|
||||||
|
export type ItemRarity = 'COMMON' | 'UNCOMMON' | 'RARE' | 'EPIC' | 'LEGENDARY'
|
||||||
|
|
||||||
export type Zone = {
|
export type Zone = {
|
||||||
id: number
|
id: UUID
|
||||||
name: string
|
name: string
|
||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
@ -65,18 +82,18 @@ export type Zone = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ZoneEffect = {
|
export type ZoneEffect = {
|
||||||
id: string
|
id: UUID
|
||||||
zoneId: number
|
zoneId: UUID
|
||||||
zone: Zone
|
zone: Zone
|
||||||
effect: string
|
effect: string
|
||||||
strength: number
|
strength: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ZoneObject = {
|
export type ZoneObject = {
|
||||||
id: string
|
id: UUID
|
||||||
zoneId: number
|
zoneId: UUID
|
||||||
zone: Zone
|
zone: Zone
|
||||||
objectId: string
|
objectId: UUID
|
||||||
object: Object
|
object: Object
|
||||||
depth: number
|
depth: number
|
||||||
isRotated: boolean
|
isRotated: boolean
|
||||||
@ -92,8 +109,8 @@ export enum ZoneEventTileType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ZoneEventTile = {
|
export type ZoneEventTile = {
|
||||||
id: string
|
id: UUID
|
||||||
zoneId: number
|
zoneId: UUID
|
||||||
zone: Zone
|
zone: Zone
|
||||||
type: ZoneEventTileType
|
type: ZoneEventTileType
|
||||||
positionX: number
|
positionX: number
|
||||||
@ -102,10 +119,10 @@ export type ZoneEventTile = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ZoneEventTileTeleport = {
|
export type ZoneEventTileTeleport = {
|
||||||
id: string
|
id: UUID
|
||||||
zoneEventTileId: string
|
zoneEventTileId: UUID
|
||||||
zoneEventTile: ZoneEventTile
|
zoneEventTile: ZoneEventTile
|
||||||
toZoneId: number
|
toZoneId: UUID
|
||||||
toZone: Zone
|
toZone: Zone
|
||||||
toPositionX: number
|
toPositionX: number
|
||||||
toPositionY: number
|
toPositionY: number
|
||||||
@ -113,7 +130,7 @@ export type ZoneEventTileTeleport = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
id: number
|
id: UUID
|
||||||
username: string
|
username: string
|
||||||
password: string
|
password: string
|
||||||
characters: Character[]
|
characters: Character[]
|
||||||
@ -133,10 +150,11 @@ 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
|
||||||
|
isSelectable: boolean
|
||||||
characters: Character[]
|
characters: Character[]
|
||||||
spriteId?: string
|
spriteId?: string
|
||||||
sprite?: Sprite
|
sprite?: Sprite
|
||||||
@ -144,9 +162,17 @@ export type CharacterType = {
|
|||||||
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
|
||||||
@ -158,29 +184,49 @@ export type Character = {
|
|||||||
positionX: number
|
positionX: number
|
||||||
positionY: number
|
positionY: number
|
||||||
rotation: number
|
rotation: number
|
||||||
zoneId: number
|
characterTypeId: UUID | null
|
||||||
|
characterType: CharacterType | null | string
|
||||||
|
characterHairId: UUID | null
|
||||||
|
characterHair: CharacterHair | null
|
||||||
|
zoneId: UUID
|
||||||
zone: Zone
|
zone: Zone
|
||||||
characterTypeId: number | null
|
|
||||||
characterType: CharacterType | null
|
|
||||||
chats: Chat[]
|
chats: Chat[]
|
||||||
items: CharacterItem[]
|
items: CharacterItem[]
|
||||||
|
equipment: CharacterEquipment[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ExtendedCharacter = Character & {
|
export type ZoneCharacter = {
|
||||||
|
character: Character
|
||||||
isMoving?: boolean
|
isMoving?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CharacterItem = {
|
export type CharacterItem = {
|
||||||
id: number
|
id: UUID
|
||||||
characterId: number
|
characterId: UUID
|
||||||
character: Character
|
character: Character
|
||||||
itemId: string
|
itemId: UUID
|
||||||
item: Item
|
item: Item
|
||||||
quantity: number
|
quantity: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CharacterEquipment = {
|
||||||
|
id: UUID
|
||||||
|
slot: CharacterEquipmentSlotType
|
||||||
|
characterItemId: UUID
|
||||||
|
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
|
||||||
@ -189,8 +235,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[]
|
||||||
@ -200,27 +245,34 @@ 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
|
characterId: UUID
|
||||||
character: Character
|
character: Character
|
||||||
zoneId: number
|
zoneId: UUID
|
||||||
zone: Zone
|
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 zoneLoadData = {
|
||||||
|
zone: Zone
|
||||||
|
characters: ZoneCharacter[]
|
||||||
|
}
|
@ -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,44 +1,62 @@
|
|||||||
<template>
|
<template>
|
||||||
<Scene name="effects" @preload="preloadScene" @create="createScene" @update="updateScene"> </Scene>
|
<Scene name="effects" @preload="preloadScene" @create="createScene" @update="updateScene" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Scene } from 'phavuer'
|
import type { WeatherState } from '@/application/types'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useZoneStore } from '@/stores/zoneStore'
|
import { useZoneStore } from '@/stores/zoneStore'
|
||||||
import { onBeforeUnmount, ref, watch } from 'vue'
|
import { Scene } from 'phavuer'
|
||||||
|
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const LIGHT_CONFIG = {
|
||||||
|
SUNRISE_HOUR: 6,
|
||||||
|
SUNSET_HOUR: 20,
|
||||||
|
DAY_STRENGTH: 100,
|
||||||
|
NIGHT_STRENGTH: 30
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stores and refs
|
||||||
|
const gameStore = useGameStore()
|
||||||
const zoneStore = useZoneStore()
|
const zoneStore = useZoneStore()
|
||||||
|
|
||||||
const sceneRef = ref<Phaser.Scene | null>(null)
|
const sceneRef = ref<Phaser.Scene | null>(null)
|
||||||
|
const zoneEffectsReady = ref(false)
|
||||||
|
|
||||||
// Effect-related refs
|
// Effect objects
|
||||||
const lightEffect = ref<Phaser.GameObjects.Graphics | null>(null)
|
const effects = {
|
||||||
const rainEmitter = ref<Phaser.GameObjects.Particles.ParticleEmitter | null>(null)
|
light: ref<Phaser.GameObjects.Graphics | null>(null),
|
||||||
const fogSprite = ref<Phaser.GameObjects.Sprite | null>(null)
|
rain: ref<Phaser.GameObjects.Particles.ParticleEmitter | null>(null),
|
||||||
|
fog: ref<Phaser.GameObjects.Sprite | null>(null)
|
||||||
|
}
|
||||||
|
|
||||||
const preloadScene = async (scene: Phaser.Scene) => {
|
// Weather state
|
||||||
|
const weatherState = ref<WeatherState>({
|
||||||
|
isRainEnabled: false,
|
||||||
|
rainPercentage: 0,
|
||||||
|
isFogEnabled: false,
|
||||||
|
fogDensity: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// Scene setup
|
||||||
|
const preloadScene = (scene: Phaser.Scene) => {
|
||||||
scene.load.image('raindrop', 'assets/raindrop.png')
|
scene.load.image('raindrop', 'assets/raindrop.png')
|
||||||
scene.load.image('fog', 'assets/fog.png')
|
scene.load.image('fog', 'assets/fog.png')
|
||||||
}
|
}
|
||||||
|
|
||||||
const createScene = async (scene: Phaser.Scene) => {
|
const createScene = (scene: Phaser.Scene) => {
|
||||||
sceneRef.value = scene
|
sceneRef.value = scene
|
||||||
createLightEffect(scene)
|
initializeEffects(scene)
|
||||||
createRainEffect(scene)
|
setupSocketListeners()
|
||||||
createFogEffect(scene)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateScene = () => {
|
const initializeEffects = (scene: Phaser.Scene) => {
|
||||||
updateEffects()
|
// Light
|
||||||
}
|
effects.light.value = scene.add.graphics().setDepth(1000)
|
||||||
|
|
||||||
const createLightEffect = (scene: Phaser.Scene) => {
|
// Rain
|
||||||
lightEffect.value = scene.add.graphics()
|
effects.rain.value = scene.add
|
||||||
lightEffect.value.setDepth(1000)
|
.particles(0, 0, 'raindrop', {
|
||||||
}
|
|
||||||
|
|
||||||
const createRainEffect = (scene: Phaser.Scene) => {
|
|
||||||
rainEmitter.value = scene.add.particles(0, 0, 'raindrop', {
|
|
||||||
x: { min: 0, max: window.innerWidth },
|
x: { min: 0, max: window.innerWidth },
|
||||||
y: -50,
|
y: -50,
|
||||||
quantity: 5,
|
quantity: 5,
|
||||||
@ -48,63 +66,114 @@ const createRainEffect = (scene: Phaser.Scene) => {
|
|||||||
alpha: { start: 0.5, end: 0 },
|
alpha: { start: 0.5, end: 0 },
|
||||||
blendMode: 'ADD'
|
blendMode: 'ADD'
|
||||||
})
|
})
|
||||||
rainEmitter.value.setDepth(900)
|
.setDepth(900)
|
||||||
rainEmitter.value.stop()
|
effects.rain.value.stop()
|
||||||
|
|
||||||
|
// Fog
|
||||||
|
effects.fog.value = scene.add
|
||||||
|
.sprite(window.innerWidth / 2, window.innerHeight / 2, 'fog')
|
||||||
|
.setScale(2)
|
||||||
|
.setAlpha(0)
|
||||||
|
.setDepth(950)
|
||||||
}
|
}
|
||||||
|
|
||||||
const createFogEffect = (scene: Phaser.Scene) => {
|
// Effect updates
|
||||||
fogSprite.value = scene.add.sprite(window.innerWidth / 2, window.innerHeight / 2, 'fog')
|
const updateScene = () => {
|
||||||
fogSprite.value.setScale(2)
|
const timeBasedLight = calculateLightStrength(gameStore.world.date)
|
||||||
fogSprite.value.setAlpha(0)
|
const zoneEffects = zoneStore.zone?.zoneEffects?.reduce(
|
||||||
fogSprite.value.setDepth(950)
|
(acc, curr) => ({
|
||||||
}
|
...acc,
|
||||||
|
[curr.effect]: curr.strength
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
) as { [key: string]: number }
|
||||||
|
|
||||||
const updateEffects = () => {
|
// Only update effects once zoneEffects are loaded
|
||||||
const effects = zoneStore.zone?.zoneEffects || []
|
if (!zoneEffectsReady.value) {
|
||||||
|
if (zoneEffects && Object.keys(zoneEffects).length) {
|
||||||
effects.forEach((effect) => {
|
zoneEffectsReady.value = true
|
||||||
switch (effect.effect) {
|
|
||||||
case 'light':
|
|
||||||
updateLightEffect(effect.strength)
|
|
||||||
break
|
|
||||||
case 'rain':
|
|
||||||
updateRainEffect(effect.strength)
|
|
||||||
break
|
|
||||||
case 'fog':
|
|
||||||
updateFogEffect(effect.strength)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateLightEffect = (strength: number) => {
|
|
||||||
if (!lightEffect.value) return
|
|
||||||
const darkness = 1 - strength / 100
|
|
||||||
lightEffect.value.clear()
|
|
||||||
lightEffect.value.fillStyle(0x000000, darkness)
|
|
||||||
lightEffect.value.fillRect(0, 0, window.innerWidth, window.innerHeight)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateRainEffect = (strength: number) => {
|
|
||||||
if (!rainEmitter.value) return
|
|
||||||
if (strength > 0) {
|
|
||||||
rainEmitter.value.start()
|
|
||||||
rainEmitter.value.setQuantity(Math.floor((strength / 100) * 10))
|
|
||||||
} else {
|
} else {
|
||||||
rainEmitter.value.stop()
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalEffects =
|
||||||
|
zoneEffects && Object.keys(zoneEffects).length
|
||||||
|
? zoneEffects
|
||||||
|
: {
|
||||||
|
light: timeBasedLight,
|
||||||
|
rain: weatherState.value.isRainEnabled ? weatherState.value.rainPercentage : 0,
|
||||||
|
fog: weatherState.value.isFogEnabled ? weatherState.value.fogDensity * 100 : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
applyEffects(finalEffects)
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyEffects = (effectValues: any) => {
|
||||||
|
if (effects.light.value) {
|
||||||
|
const darkness = 1 - (effectValues.light ?? 100) / 100
|
||||||
|
effects.light.value.clear().fillStyle(0x000000, darkness).fillRect(0, 0, window.innerWidth, window.innerHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (effects.rain.value) {
|
||||||
|
const strength = effectValues.rain ?? 0
|
||||||
|
strength > 0 ? effects.rain.value.start().setQuantity(Math.floor((strength / 100) * 10)) : effects.rain.value.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (effects.fog.value) {
|
||||||
|
effects.fog.value.setAlpha((effectValues.fog ?? 0) / 100)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateFogEffect = (strength: number) => {
|
const calculateLightStrength = (time: Date): number => {
|
||||||
if (!fogSprite.value) return
|
const hour = time.getHours()
|
||||||
fogSprite.value.setAlpha(strength / 100)
|
const minute = time.getMinutes()
|
||||||
|
|
||||||
|
if (hour >= LIGHT_CONFIG.SUNSET_HOUR || hour < LIGHT_CONFIG.SUNRISE_HOUR) return LIGHT_CONFIG.NIGHT_STRENGTH
|
||||||
|
|
||||||
|
if (hour > LIGHT_CONFIG.SUNRISE_HOUR && hour < LIGHT_CONFIG.SUNSET_HOUR - 2) return LIGHT_CONFIG.DAY_STRENGTH
|
||||||
|
|
||||||
|
if (hour === LIGHT_CONFIG.SUNRISE_HOUR) return LIGHT_CONFIG.NIGHT_STRENGTH + ((LIGHT_CONFIG.DAY_STRENGTH - LIGHT_CONFIG.NIGHT_STRENGTH) * minute) / 60
|
||||||
|
|
||||||
|
const totalMinutes = (hour - (LIGHT_CONFIG.SUNSET_HOUR - 2)) * 60 + minute
|
||||||
|
return LIGHT_CONFIG.DAY_STRENGTH - (LIGHT_CONFIG.DAY_STRENGTH - LIGHT_CONFIG.NIGHT_STRENGTH) * (totalMinutes / 120)
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => zoneStore.zone?.zoneEffects, updateEffects, { deep: true })
|
// Socket and window handlers
|
||||||
|
const setupSocketListeners = () => {
|
||||||
|
gameStore.connection?.emit('weather', (response: WeatherState) => {
|
||||||
|
weatherState.value = response
|
||||||
|
updateScene()
|
||||||
|
})
|
||||||
|
|
||||||
|
gameStore.connection!.on('weather', (data: WeatherState) => {
|
||||||
|
weatherState.value = data
|
||||||
|
updateScene()
|
||||||
|
})
|
||||||
|
|
||||||
|
gameStore.connection!.on('date', updateScene)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
if (effects.rain.value) effects.rain.value.updateConfig({ x: { min: 0, max: window.innerWidth } })
|
||||||
|
if (effects.fog.value) effects.fog.value.setPosition(window.innerWidth / 2, window.innerHeight / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
watch(
|
||||||
|
() => zoneStore.zone,
|
||||||
|
() => {
|
||||||
|
zoneEffectsReady.value = false
|
||||||
|
updateScene()
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(() => window.addEventListener('resize', handleResize))
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
if (sceneRef.value) sceneRef.value.scene.remove('effects')
|
if (sceneRef.value) sceneRef.value.scene.remove('effects')
|
||||||
|
gameStore.connection?.off('weather')
|
||||||
})
|
})
|
||||||
|
|
||||||
// @TODO : Fix resize issue
|
|
||||||
</script>
|
</script>
|
||||||
|
192
src/components/game/character/Character.vue
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
<template>
|
||||||
|
<ChatBubble :zoneCharacter="props.zoneCharacter" :currentX="currentX" :currentY="currentY" />
|
||||||
|
<Healthbar :zoneCharacter="props.zoneCharacter" :currentX="currentX" :currentY="currentY" />
|
||||||
|
<Container ref="charContainer" :depth="isometricDepth" :x="currentX" :y="currentY">
|
||||||
|
<CharacterHair :zoneCharacter="props.zoneCharacter" :currentX="currentX" :currentY="currentY" />
|
||||||
|
<!-- <CharacterChest :zoneCharacter="props.zoneCharacter" :currentX="currentX" :currentY="currentY" />-->
|
||||||
|
<Sprite ref="charSprite" :origin-y="1" :flipX="isFlippedX" />
|
||||||
|
</Container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import config from '@/application/config'
|
||||||
|
import { type Sprite as SpriteT, type ZoneCharacter } 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/zoneComposable'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useZoneStore } from '@/stores/zoneStore'
|
||||||
|
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
|
||||||
|
zoneCharacter: ZoneCharacter
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const charContainer = refObj<Phaser.GameObjects.Container>()
|
||||||
|
const charSprite = refObj<Phaser.GameObjects.Sprite>()
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const zoneStore = useZoneStore()
|
||||||
|
const scene = useScene()
|
||||||
|
|
||||||
|
const currentX = ref(0)
|
||||||
|
const currentY = ref(0)
|
||||||
|
const isometricDepth = ref(1)
|
||||||
|
const isInitialPosition = ref(true)
|
||||||
|
const isMoving = ref(false)
|
||||||
|
let animationFrame: number | null = null
|
||||||
|
const moveSpeed = 5.7
|
||||||
|
|
||||||
|
const updateIsometricDepth = (x: number, y: number) => {
|
||||||
|
isometricDepth.value = calculateIsometricDepth(x, y, 28, 94, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopMovement = () => {
|
||||||
|
isMoving.value = false
|
||||||
|
if (animationFrame) {
|
||||||
|
cancelAnimationFrame(animationFrame)
|
||||||
|
animationFrame = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePosition = (x: number, y: number, direction: Direction) => {
|
||||||
|
const targetX = tileToWorldX(props.tilemap, x, y)
|
||||||
|
const targetY = tileToWorldY(props.tilemap, x, y)
|
||||||
|
|
||||||
|
if (isInitialPosition.value) {
|
||||||
|
currentX.value = targetX
|
||||||
|
currentY.value = targetY
|
||||||
|
isInitialPosition.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMoving.value) {
|
||||||
|
stopMovement()
|
||||||
|
}
|
||||||
|
|
||||||
|
const distance = Math.sqrt(Math.pow(targetX - currentX.value, 2) + Math.pow(targetY - currentY.value, 2))
|
||||||
|
|
||||||
|
isMoving.value = true
|
||||||
|
const startTime = performance.now()
|
||||||
|
const startX = currentX.value
|
||||||
|
const startY = currentY.value
|
||||||
|
const duration = distance * moveSpeed
|
||||||
|
|
||||||
|
if (direction === Direction.POSITIVE) {
|
||||||
|
updateIsometricDepth(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
const animate = (currentTime: number) => {
|
||||||
|
if (!isMoving.value) return
|
||||||
|
|
||||||
|
const elapsed = currentTime - startTime
|
||||||
|
const progress = Math.min(elapsed / duration, 1)
|
||||||
|
|
||||||
|
currentX.value = startX + (targetX - startX) * progress
|
||||||
|
currentY.value = startY + (targetY - startY) * progress
|
||||||
|
|
||||||
|
if (progress < 1) {
|
||||||
|
animationFrame = requestAnimationFrame(animate)
|
||||||
|
} else {
|
||||||
|
isMoving.value = false
|
||||||
|
if (direction === Direction.NEGATIVE) {
|
||||||
|
updateIsometricDepth(x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
animationFrame = requestAnimationFrame(animate)
|
||||||
|
}
|
||||||
|
|
||||||
|
const calcDirection = (oldX: number, oldY: number, newX: number, newY: number): Direction => {
|
||||||
|
if (newY < oldY || newX < oldX) return Direction.NEGATIVE
|
||||||
|
if (newX > oldX || newY > oldY) return Direction.POSITIVE
|
||||||
|
return Direction.UNCHANGED
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFlippedX = computed(() => [6, 4].includes(props.zoneCharacter.character.rotation ?? 0))
|
||||||
|
|
||||||
|
const charTexture = computed(() => {
|
||||||
|
const { rotation, characterType } = props.zoneCharacter.character
|
||||||
|
const spriteId = characterType?.sprite ?? 'idle_right_down'
|
||||||
|
const action = props.zoneCharacter.isMoving ? 'walk' : 'idle'
|
||||||
|
const direction = [0, 6].includes(rotation) ? 'left_up' : 'right_down'
|
||||||
|
|
||||||
|
return `${spriteId}-${action}_${direction}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateSprite = () => {
|
||||||
|
if (props.zoneCharacter.isMoving) {
|
||||||
|
charSprite.value!.anims.play(charTexture.value, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
charSprite.value!.anims.stop()
|
||||||
|
charSprite.value!.setFrame(0)
|
||||||
|
charSprite.value!.setTexture(charTexture.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => ({
|
||||||
|
x: props.zoneCharacter.character.positionX,
|
||||||
|
y: props.zoneCharacter.character.positionY,
|
||||||
|
isMoving: props.zoneCharacter.isMoving,
|
||||||
|
rotation: props.zoneCharacter.character.rotation
|
||||||
|
}),
|
||||||
|
(newValues, oldValues) => {
|
||||||
|
if (!newValues) return
|
||||||
|
|
||||||
|
if (!oldValues || newValues.x !== oldValues.x || newValues.y !== oldValues.y) {
|
||||||
|
const direction = !oldValues ? Direction.POSITIVE : calcDirection(oldValues.x, oldValues.y, newValues.x, newValues.y)
|
||||||
|
updatePosition(newValues.x, newValues.y, direction)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle animation updates
|
||||||
|
if (newValues.isMoving !== oldValues?.isMoving || newValues.rotation !== oldValues?.rotation) {
|
||||||
|
updateSprite()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(() => props.zoneCharacter, updateSprite)
|
||||||
|
|
||||||
|
loadSpriteTextures(scene, props.zoneCharacter.character.characterType?.sprite as string)
|
||||||
|
.then(() => {
|
||||||
|
charSprite.value!.setTexture(charTexture.value)
|
||||||
|
charSprite.value!.setFlipX(isFlippedX.value)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error loading texture:', error)
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
charContainer.value!.setName(props.zoneCharacter.character!.name)
|
||||||
|
|
||||||
|
if (props.zoneCharacter.character.id === gameStore.character!.id) {
|
||||||
|
zoneStore.setCharacterLoaded(true)
|
||||||
|
|
||||||
|
// #146 : Set camera position to character, need to be improved still
|
||||||
|
// scene.cameras.main.startFollow(charContainer.value as Phaser.GameObjects.Container)
|
||||||
|
// scene.cameras.main.stopFollow()
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePosition(props.zoneCharacter.character.positionX, props.zoneCharacter.character.positionY, props.zoneCharacter.character.rotation)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopMovement()
|
||||||
|
})
|
||||||
|
</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 { Sprite as SpriteT, ZoneCharacter } 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<{
|
||||||
|
zoneCharacter: ZoneCharacter
|
||||||
|
currentX: number
|
||||||
|
currentY: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const scene = useScene()
|
||||||
|
|
||||||
|
const texture = computed(() => {
|
||||||
|
const { rotation, characterHair } = props.zoneCharacter.character
|
||||||
|
const spriteId = characterHair?.sprite?.id
|
||||||
|
const direction = [0, 6].includes(rotation) ? 'back' : 'front'
|
||||||
|
|
||||||
|
return `${spriteId}-${direction}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const isFlippedX = computed(() => [6, 4].includes(props.zoneCharacter.character.rotation ?? 0))
|
||||||
|
|
||||||
|
const imageProps = computed(() => {
|
||||||
|
// Get the current sprite action based on direction
|
||||||
|
const direction = [0, 6].includes(props.zoneCharacter.character.rotation ?? 0) ? 'back' : 'front'
|
||||||
|
const spriteAction = props.zoneCharacter.character.characterHair?.sprite?.spriteActions?.find((spriteAction) => spriteAction.action === direction)
|
||||||
|
|
||||||
|
return {
|
||||||
|
depth: 1,
|
||||||
|
originX: Number(spriteAction?.originX) ?? 0,
|
||||||
|
originY: Number(spriteAction?.originY) ?? 0,
|
||||||
|
flipX: isFlippedX.value,
|
||||||
|
texture: texture.value
|
||||||
|
// y: props.zoneCharacter.isMoving ? Math.floor(Date.now() / 250) % 2 : 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
loadSpriteTextures(scene, props.zoneCharacter.character.characterHair?.sprite as SpriteT)
|
||||||
|
.then(() => {})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error loading texture:', error)
|
||||||
|
})
|
||||||
|
</script>
|
51
src/components/game/character/partials/CharacterHair.vue
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<Image v-bind="imageProps" v-if="gameStore.getLoadedAsset(texture)" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Sprite as SpriteT, ZoneCharacter } 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<{
|
||||||
|
zoneCharacter: ZoneCharacter
|
||||||
|
currentX: number
|
||||||
|
currentY: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const scene = useScene()
|
||||||
|
|
||||||
|
const texture = computed(() => {
|
||||||
|
const { rotation, characterHair } = props.zoneCharacter.character
|
||||||
|
const spriteId = characterHair?.sprite?.id
|
||||||
|
const direction = [0, 6].includes(rotation) ? 'back' : 'front'
|
||||||
|
|
||||||
|
return `${spriteId}-${direction}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const isFlippedX = computed(() => [6, 4].includes(props.zoneCharacter.character.rotation ?? 0))
|
||||||
|
|
||||||
|
const imageProps = computed(() => {
|
||||||
|
// Get the current sprite action based on direction
|
||||||
|
const direction = [0, 6].includes(props.zoneCharacter.character.rotation ?? 0) ? 'back' : 'front'
|
||||||
|
const spriteAction = props.zoneCharacter.character.characterHair?.sprite?.spriteActions?.find((spriteAction) => spriteAction.action === direction)
|
||||||
|
|
||||||
|
return {
|
||||||
|
depth: 1,
|
||||||
|
originX: Number(spriteAction?.originX) ?? 0,
|
||||||
|
originY: Number(spriteAction?.originY) ?? 0,
|
||||||
|
flipX: isFlippedX.value,
|
||||||
|
texture: texture.value,
|
||||||
|
y: props.zoneCharacter.isMoving ? Math.floor(Date.now() / 250) % 2 : 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
loadSpriteTextures(scene, props.zoneCharacter.character.characterHair?.sprite as SpriteT)
|
||||||
|
.then(() => {})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error loading texture:', error)
|
||||||
|
})
|
||||||
|
</script>
|
46
src/components/game/character/partials/ChatBubble.vue
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<Container ref="charChatContainer" :depth="999" :x="currentX" :y="currentY">
|
||||||
|
<RoundRectangle @create="createChatBubble" :origin-x="0.5" :origin-y="7.5" :fillColor="0xffffff" :width="194" :height="21" :radius="20" />
|
||||||
|
<Text @create="createChatText" :style="{ fontSize: 13, fontFamily: 'Arial', color: '#000' }" />
|
||||||
|
</Container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { ZoneCharacter } from '@/application/types'
|
||||||
|
import { Container, refObj, RoundRectangle, Text, useGame } from 'phavuer'
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
zoneCharacter: ZoneCharacter
|
||||||
|
currentX: number
|
||||||
|
currentY: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const game = useGame()
|
||||||
|
const charChatContainer = refObj<Phaser.GameObjects.Container>()
|
||||||
|
|
||||||
|
const createChatBubble = (container: Phaser.GameObjects.Container) => {
|
||||||
|
container.setName(`${props.zoneCharacter.character.name}_chatBubble`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const createChatText = (text: Phaser.GameObjects.Text) => {
|
||||||
|
text.setName(`${props.zoneCharacter.character.name}_chatText`)
|
||||||
|
text.setFontSize(13)
|
||||||
|
text.setFontFamily('Arial')
|
||||||
|
text.setOrigin(0.5, 10.9)
|
||||||
|
|
||||||
|
// Fix text alignment on Windows and Android
|
||||||
|
if (game.device.os.windows || game.device.os.android) {
|
||||||
|
text.setOrigin(0.5, 9.75)
|
||||||
|
|
||||||
|
if (game.device.browser.firefox) {
|
||||||
|
text.setOrigin(0.5, 10.9)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
charChatContainer.value!.setName(`${props.zoneCharacter.character!.name}_chatContainer`)
|
||||||
|
charChatContainer.value!.setVisible(false)
|
||||||
|
})
|
||||||
|
</script>
|
35
src/components/game/character/partials/Healthbar.vue
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<Container :depth="999" :x="currentX" :y="currentY">
|
||||||
|
<Text @create="createNicknameText" :text="props.zoneCharacter.character.name" />
|
||||||
|
<RoundRectangle :origin-x="0.5" :origin-y="18.5" :fillColor="0xffffff" :width="74" :height="6" :radius="5" />
|
||||||
|
<RoundRectangle :origin-x="0.5" :origin-y="36.4" :fillColor="0x00b3b3" :width="70" :height="3" :radius="5" />
|
||||||
|
</Container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { ZoneCharacter } from '@/application/types'
|
||||||
|
import { Container, RoundRectangle, Text, useGame } from 'phavuer'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
zoneCharacter: ZoneCharacter
|
||||||
|
currentX: number
|
||||||
|
currentY: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const game = useGame()
|
||||||
|
|
||||||
|
const createNicknameText = (text: Phaser.GameObjects.Text) => {
|
||||||
|
text.setFontSize(13)
|
||||||
|
text.setFontFamily('Arial')
|
||||||
|
text.setOrigin(0.5, 9)
|
||||||
|
|
||||||
|
// Fix text alignment on Windows and Android
|
||||||
|
if (game.device.os.windows || game.device.os.android) {
|
||||||
|
text.setOrigin(0.5, 8)
|
||||||
|
|
||||||
|
if (game.device.browser.firefox) {
|
||||||
|
text.setOrigin(0.5, 9)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
14
src/components/game/zone/Characters.vue
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<Character v-for="item in zoneStore.characters" :key="item.character.id" :tilemap="tilemap" :zoneCharacter="item" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Character from '@/components/game/character/Character.vue'
|
||||||
|
import { useZoneStore } from '@/stores/zoneStore'
|
||||||
|
|
||||||
|
const zoneStore = useZoneStore()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
tilemap: Phaser.Tilemaps.Tilemap
|
||||||
|
}>()
|
||||||
|
</script>
|
50
src/components/game/zone/Zone.vue
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<ZoneTiles :key="zoneStore.zone?.id ?? 0" @tileMap:create="tileMap = $event" />
|
||||||
|
<ZoneObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
||||||
|
<Characters v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { ZoneCharacter, zoneLoadData } from '@/application/types'
|
||||||
|
import Characters from '@/components/game/zone/Characters.vue'
|
||||||
|
import ZoneObjects from '@/components/game/zone/ZoneObjects.vue'
|
||||||
|
import ZoneTiles from '@/components/game/zone/ZoneTiles.vue'
|
||||||
|
import { loadZoneTilesIntoScene } from '@/composables/zoneComposable'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useZoneStore } from '@/stores/zoneStore'
|
||||||
|
import { useScene } from 'phavuer'
|
||||||
|
import { onUnmounted, ref } from 'vue'
|
||||||
|
|
||||||
|
const scene = useScene()
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const zoneStore = useZoneStore()
|
||||||
|
|
||||||
|
const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null)
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
zoneStore.reset()
|
||||||
|
gameStore.connection!.off('zone:character:teleport')
|
||||||
|
gameStore.connection!.off('zone:character:join')
|
||||||
|
gameStore.connection!.off('zone:character:leave')
|
||||||
|
gameStore.connection!.off('zone:character:move')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
gameStore.connection!.on('zone:character:teleport', async (data: zoneLoadData) => {
|
||||||
|
await loadZoneTilesIntoScene(data.zone.id, scene)
|
||||||
|
zoneStore.setZone(data.zone)
|
||||||
|
zoneStore.setCharacters(data.characters)
|
||||||
|
})
|
||||||
|
|
||||||
|
gameStore.connection!.on('zone:character:join', async (data: ZoneCharacter) => {
|
||||||
|
zoneStore.addCharacter(data)
|
||||||
|
})
|
||||||
|
|
||||||
|
gameStore.connection!.on('zone:character:leave', (characterId: number) => {
|
||||||
|
zoneStore.removeCharacter(characterId)
|
||||||
|
})
|
||||||
|
|
||||||
|
gameStore.connection!.on('zone:character:move', (data: { characterId: number; positionX: number; positionY: number; rotation: number; isMoving: boolean }) => {
|
||||||
|
zoneStore.updateCharacterPosition(data)
|
||||||
|
})
|
||||||
|
</script>
|
@ -3,8 +3,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import ZoneObject from '@/components/game/zone/partials/ZoneObject.vue'
|
||||||
import { useZoneStore } from '@/stores/zoneStore'
|
import { useZoneStore } from '@/stores/zoneStore'
|
||||||
import ZoneObject from '@/components/zone/partials/ZoneObject.vue'
|
|
||||||
|
|
||||||
const zoneStore = useZoneStore()
|
const zoneStore = useZoneStore()
|
||||||
|
|
@ -3,13 +3,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/config'
|
import config from '@/application/config'
|
||||||
import { useScene } from 'phavuer'
|
import { unduplicateArray } from '@/application/utilities'
|
||||||
import { useZoneStore } from '@/stores/zoneStore'
|
|
||||||
import { onBeforeUnmount } from 'vue'
|
|
||||||
import { FlattenZoneArray, setLayerTiles } from '@/composables/zoneComposable'
|
|
||||||
import Controls from '@/components/utilities/Controls.vue'
|
import Controls from '@/components/utilities/Controls.vue'
|
||||||
import { unduplicateArray } from '@/utilities'
|
import { FlattenZoneArray, setLayerTiles } from '@/composables/zoneComposable'
|
||||||
|
import { useZoneStore } from '@/stores/zoneStore'
|
||||||
|
import { useScene } from 'phavuer'
|
||||||
|
import { onBeforeUnmount } from 'vue'
|
||||||
|
|
||||||
const emit = defineEmits(['tileMap:create'])
|
const emit = defineEmits(['tileMap:create'])
|
||||||
|
|
@ -3,12 +3,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted } from 'vue'
|
import type { AssetDataT, ZoneObject } from '@/application/types'
|
||||||
import { Image, useScene } from 'phavuer'
|
|
||||||
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
|
||||||
import { loadTexture } from '@/composables/gameComposable'
|
import { loadTexture } from '@/composables/gameComposable'
|
||||||
import type { AssetDataT, ZoneObject } from '@/types'
|
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { Image, useScene } from 'phavuer'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
tilemap: Phaser.Tilemaps.Tilemap
|
tilemap: Phaser.Tilemaps.Tilemap
|
@ -1,11 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal :isModalOpen="gameStore.uiSettings.isGmPanelOpen" @modal:close="() => gameStore.toggleGmPanel()" :modal-width="1000" :modal-height="650" :can-full-screen="true">
|
<Modal :isModalOpen="gameStore.uiSettings.isGmPanelOpen" @modal:close="() => gameStore.toggleGmPanel()" :modal-width="1000" :modal-height="650" :is-full-screen="true" :bg-style="'dark'">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<div class="flex gap-1.5 flex-wrap">
|
<div class="flex gap-1.5 flex-wrap">
|
||||||
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">General</button>
|
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">General</button>
|
||||||
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Users</button>
|
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Users</button>
|
||||||
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Chats</button>
|
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Chats</button>
|
||||||
<button @mousedown.stop class="btn-cyan active py-1.5 px-4 min-w-24">Asset manager</button>
|
<button @mousedown.stop class="btn-cyan active py-1.5 px-4 min-w-24">Asset manager</button>
|
||||||
|
<button class="btn-cyan py-1.5 px-4 min-w-24" type="button" @click="() => zoneEditorStore.toggleActive()">Map editor</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #modalBody>
|
<template #modalBody>
|
||||||
@ -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 { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
|
|
||||||
let toggle = ref('asset-manager')
|
let toggle = ref('asset-manager')
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,44 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Modal :isModalOpen="true" :closable="false" :is-resizable="false" :modal-width="modalWidth" :modal-height="modalHeight" :modal-position-x="posXY.x" :modal-position-y="posXY.y">
|
|
||||||
<template #modalHeader>
|
|
||||||
<h3 class="m-0 font-medium shrink-0 text-white">GM tools</h3>
|
|
||||||
</template>
|
|
||||||
<template #modalBody>
|
|
||||||
<div class="content flex flex-col gap-2.5 m-4 h-20">
|
|
||||||
<button class="btn-cyan py-1.5 px-4 w-full" type="button" @click="gameStore.toggleGmPanel()">Toggle GM panel</button>
|
|
||||||
<button class="btn-cyan py-1.5 px-4 w-full" type="button" @click="() => zoneEditorStore.toggleActive()">Zone manager</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Modal>
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts">
|
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import { onMounted, ref } from 'vue'
|
|
||||||
|
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const modalWidth = ref(200)
|
|
||||||
const modalHeight = ref(170)
|
|
||||||
|
|
||||||
let posXY = ref({ x: 0, y: 0 })
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
posXY.value = customPositionGmPanel(modalWidth.value)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const customPositionGmPanel = (modalWidth: number) => {
|
|
||||||
const padding = 25
|
|
||||||
const width = window.innerWidth
|
|
||||||
|
|
||||||
const x = width - (modalWidth + 4) - 25
|
|
||||||
const y = padding
|
|
||||||
|
|
||||||
return { x, y }
|
|
||||||
}
|
|
||||||
|
|
||||||
posXY.value = customPositionGmPanel(modalWidth.value)
|
|
||||||
</script>
|
|
@ -1,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 === 'objects' }" @click="() => (selectedCategory = 'objects')">
|
||||||
<span :class="{ 'text-white': selectedCategory === 'objects' }">Objects</span>
|
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'objects' }">Objects</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'sprites' }" @click="() => (selectedCategory = 'sprites')">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'sprites' }" @click="() => (selectedCategory = 'sprites')">
|
||||||
<span :class="{ 'text-white': selectedCategory === 'sprites' }">Sprites</span>
|
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'sprites' }">Sprites</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'items' }" @click="() => (selectedCategory = 'items')">
|
||||||
<span>Items</span>
|
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'items' }">Items</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group">
|
||||||
<span>NPC's</span>
|
<span class="group-hover:text-white">NPC's</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'shops' }" @click="() => (selectedCategory = 'shops')">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan/80': selectedCategory === 'shops' }" @click="() => (selectedCategory = 'shops')">
|
||||||
<span :class="{ 'text-white': selectedCategory === 'shops' }">Shops</span>
|
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'shops' }">Shops</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'characterTypes' }" @click="() => (selectedCategory = 'characterTypes')">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan/80': selectedCategory === 'characterTypes' }" @click="() => (selectedCategory = 'characterTypes')">
|
||||||
<span :class="{ 'text-white': selectedCategory === 'characterTypes' }">Character types</span>
|
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'characterTypes' }">Character types</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'characterHair' }" @click="() => (selectedCategory = 'characterHair')">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan/80': selectedCategory === 'characterHair' }" @click="() => (selectedCategory = 'characterHair')">
|
||||||
<span :class="{ 'text-white': selectedCategory === 'characterHair' }">Character hair</span>
|
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'characterHair' }">Character hair</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group">
|
||||||
<span>Mounts</span>
|
<span class="group-hover:text-white">Mounts</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group">
|
||||||
<span>Pets</span>
|
<span class="group-hover:text-white">Pets</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group">
|
||||||
<span>Emoticons</span>
|
<span class="group-hover:text-white">Emoticons</span>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute w-px bg-gray-500 h-full top-0 left-1/6"></div>
|
|
||||||
|
|
||||||
<!-- Assets list -->
|
<!-- Assets list -->
|
||||||
<div class="overflow-auto h-full w-4/12 flex flex-col relative">
|
<div class="overflow-auto h-full w-4/12 flex flex-col relative">
|
||||||
<TileList v-if="selectedCategory === 'tiles'" />
|
<TileList v-if="selectedCategory === 'tiles'" />
|
||||||
<ObjectList v-if="selectedCategory === 'objects'" />
|
<ObjectList v-if="selectedCategory === 'objects'" />
|
||||||
<SpriteList v-if="selectedCategory === 'sprites'" />
|
<SpriteList v-if="selectedCategory === 'sprites'" />
|
||||||
|
<ItemList v-if="selectedCategory === 'items'" />
|
||||||
<CharacterTypeList v-if="selectedCategory === 'characterTypes'" />
|
<CharacterTypeList v-if="selectedCategory === 'characterTypes'" />
|
||||||
|
<CharacterHairList v-if="selectedCategory === 'characterHair'" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute w-px bg-gray-500 h-full top-0 left-1/2"></div>
|
|
||||||
|
|
||||||
<!-- Asset details -->
|
<!-- Asset details -->
|
||||||
<div class="flex w-1/2 after:hidden flex-col relative overflow-auto">
|
<div class="flex w-7/12 after:hidden flex-col relative overflow-auto">
|
||||||
<TileDetails v-if="selectedCategory === 'tiles' && assetManagerStore.selectedTile" />
|
<TileDetails v-if="selectedCategory === 'tiles' && assetManagerStore.selectedTile" />
|
||||||
<ObjectDetails v-if="selectedCategory === 'objects' && assetManagerStore.selectedObject" />
|
<ObjectDetails v-if="selectedCategory === 'objects' && assetManagerStore.selectedObject" />
|
||||||
<SpriteDetails v-if="selectedCategory === 'sprites' && assetManagerStore.selectedSprite" />
|
<SpriteDetails v-if="selectedCategory === 'sprites' && assetManagerStore.selectedSprite" />
|
||||||
|
<ItemDetails v-if="selectedCategory === 'items' && assetManagerStore.selectedItem" />
|
||||||
<CharacterTypeDetails v-if="selectedCategory === 'characterTypes' && assetManagerStore.selectedCharacterType" />
|
<CharacterTypeDetails v-if="selectedCategory === 'characterTypes' && assetManagerStore.selectedCharacterType" />
|
||||||
|
<CharacterHairDetails v-if="selectedCategory === 'characterHair' && assetManagerStore.selectedCharacterHair" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<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 ObjectDetails from '@/components/gameMaster/assetManager/partials/object/ObjectDetails.vue'
|
||||||
|
import ObjectList from '@/components/gameMaster/assetManager/partials/object/ObjectList.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.spriteId
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCharacterHair() {
|
||||||
|
if (!selectedCharacterHair.value) return
|
||||||
|
|
||||||
|
gameStore.connection?.emit('gm:characterHair:remove', { id: selectedCharacterHair.value.id }, (response: boolean) => {
|
||||||
|
if (!response) {
|
||||||
|
console.error('Failed to remove character hair')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
refreshCharacterHairList()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshCharacterHairList(unsetSelectedCharacterHair = true) {
|
||||||
|
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => {
|
||||||
|
assetManagerStore.setCharacterHairList(response)
|
||||||
|
|
||||||
|
if (unsetSelectedCharacterHair) {
|
||||||
|
assetManagerStore.setSelectedCharacterHair(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCharacterHair() {
|
||||||
|
const characterHairData = {
|
||||||
|
id: selectedCharacterHair.value!.id,
|
||||||
|
name: characterName.value,
|
||||||
|
gender: characterGender.value,
|
||||||
|
isSelectable: characterIsSelectable.value,
|
||||||
|
spriteId: characterSpriteId.value
|
||||||
|
}
|
||||||
|
|
||||||
|
gameStore.connection?.emit('gm:characterHair:update', characterHairData, (response: boolean) => {
|
||||||
|
if (!response) {
|
||||||
|
console.error('Failed to save character type')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
refreshCharacterHairList(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(selectedCharacterHair, (characterHair: CharacterHair | null) => {
|
||||||
|
if (!characterHair) return
|
||||||
|
characterName.value = characterHair.name
|
||||||
|
characterGender.value = characterHair.gender
|
||||||
|
characterIsSelectable.value = characterHair.isSelectable
|
||||||
|
characterSpriteId.value = characterHair.spriteId
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!selectedCharacterHair.value) return
|
||||||
|
|
||||||
|
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
||||||
|
assetManagerStore.setSpriteList(response)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
assetManagerStore.setSelectedCharacterHair(null)
|
||||||
|
})
|
||||||
|
</script>
|
@ -0,0 +1,99 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative mb-5 flex items-center gap-x-2.5">
|
||||||
|
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
|
||||||
|
<label for="create-character" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2.5 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
|
||||||
|
<button class="p-0 h-5" id="create-character" @click="createNewCharacterHair">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="white">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||||
|
<div v-bind="wrapperProps" ref="elementToScroll">
|
||||||
|
<a
|
||||||
|
v-for="{ data: characterHair } in list"
|
||||||
|
:key="characterHair.id"
|
||||||
|
class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group"
|
||||||
|
:class="{ 'bg-cyan': assetManagerStore.selectedCharacterHair?.id === characterHair.id }"
|
||||||
|
@click="assetManagerStore.setSelectedCharacterHair(characterHair as CharacterHair)"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2.5">
|
||||||
|
<span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedCharacterHair?.id === characterHair.id }">{{ characterHair.name }}</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
||||||
|
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
||||||
|
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import 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[]) => {
|
||||||
|
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,6 +67,7 @@ if (selectedCharacterType.value) {
|
|||||||
characterName.value = selectedCharacterType.value.name
|
characterName.value = selectedCharacterType.value.name
|
||||||
characterGender.value = selectedCharacterType.value.gender
|
characterGender.value = selectedCharacterType.value.gender
|
||||||
characterRace.value = selectedCharacterType.value.race
|
characterRace.value = selectedCharacterType.value.race
|
||||||
|
characterIsSelectable.value = selectedCharacterType.value.isSelectable
|
||||||
characterSpriteId.value = selectedCharacterType.value.spriteId
|
characterSpriteId.value = selectedCharacterType.value.spriteId
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,10 +95,11 @@ function refreshCharacterTypeList(unsetSelectedCharacterType = true) {
|
|||||||
|
|
||||||
function saveCharacterType() {
|
function saveCharacterType() {
|
||||||
const characterTypeData = {
|
const characterTypeData = {
|
||||||
id: selectedCharacterType.value?.id,
|
id: selectedCharacterType.value!.id,
|
||||||
name: characterName.value,
|
name: characterName.value,
|
||||||
gender: characterGender.value,
|
gender: characterGender.value,
|
||||||
race: characterRace.value,
|
race: characterRace.value,
|
||||||
|
isSelectable: characterIsSelectable.value,
|
||||||
spriteId: characterSpriteId.value
|
spriteId: characterSpriteId.value
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,11 +117,16 @@ watch(selectedCharacterType, (characterType: CharacterType | null) => {
|
|||||||
characterName.value = characterType.name
|
characterName.value = characterType.name
|
||||||
characterGender.value = characterType.gender
|
characterGender.value = characterType.gender
|
||||||
characterRace.value = characterType.race
|
characterRace.value = characterType.race
|
||||||
|
characterIsSelectable.value = characterType.isSelectable
|
||||||
characterSpriteId.value = characterType.spriteId
|
characterSpriteId.value = characterType.spriteId
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!selectedCharacterType.value) return
|
if (!selectedCharacterType.value) return
|
||||||
|
|
||||||
|
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
||||||
|
assetManagerStore.setSpriteList(response)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
@ -1,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">
|
||||||
<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">
|
||||||
|
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
|
||||||
</button>
|
</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()
|
||||||
|
@ -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 } 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">
|
||||||
|
<a v-for="{ data: item } in list" :key="item.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedItem?.id === item.id }" @click="assetManagerStore.setSelectedItem(item as Item)">
|
||||||
|
<div class="flex items-center gap-2.5">
|
||||||
|
<span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedItem?.id === item.id }">
|
||||||
|
{{ item.name }}
|
||||||
|
<small class="text-gray-400">({{ item.itemType }})</small>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
||||||
|
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
||||||
|
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import 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>
|
@ -1,12 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full overflow-auto">
|
<div class="h-full overflow-auto">
|
||||||
<div class="relative p-2.5 flex flex-col items-center justify-between h-72">
|
<div class="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray">
|
||||||
<div class="filler"></div>
|
|
||||||
<img class="max-h-56" :src="`${config.server_endpoint}/assets/objects/${selectedObject?.id}.png`" :alt="'Object ' + selectedObject?.id" />
|
<img class="max-h-56" :src="`${config.server_endpoint}/assets/objects/${selectedObject?.id}.png`" :alt="'Object ' + selectedObject?.id" />
|
||||||
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeObject">Remove</button>
|
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="m-2.5 p-2.5 block">
|
<div class="mt-5 block">
|
||||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject">
|
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject">
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="name">Name</label>
|
<label for="name">Name</label>
|
||||||
@ -21,19 +18,19 @@
|
|||||||
<input v-model="objectOriginY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
|
<input v-model="objectOriginY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="origin-x">Tags</label>
|
<label for="tags">Tags</label>
|
||||||
<ChipsInput v-model="objectTags" @update:modelValue="objectTags = $event" />
|
<ChipsInput v-model="objectTags" @update:modelValue="objectTags = $event" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="origin-x">Is animated</label>
|
<label for="is-animated">Is animated</label>
|
||||||
<select v-model="objectIsAnimated" class="input-field" name="is-animated">
|
<select v-model="objectIsAnimated" class="input-field" name="is-animated">
|
||||||
<option :value="false">No</option>
|
<option :value="false">No</option>
|
||||||
<option :value="true">Yes</option>
|
<option :value="true">Yes</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="frame-speed">Frame speed</label>
|
<label for="frame-speed">Frame rate</label>
|
||||||
<input v-model="objectFrameSpeed" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame speed" />
|
<input v-model="objectFrameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-half">
|
<div class="form-field-half">
|
||||||
<label for="frame-width">Frame width</label>
|
<label for="frame-width">Frame width</label>
|
||||||
@ -43,20 +40,23 @@
|
|||||||
<label for="frame-height">Frame height</label>
|
<label for="frame-height">Frame height</label>
|
||||||
<input v-model="objectFrameHeight" class="input-field" type="number" step="any" name="frame-height" placeholder="Frame height" />
|
<input v-model="objectFrameHeight" class="input-field" type="number" step="any" name="frame-height" placeholder="Frame height" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex gap-4">
|
||||||
<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="removeObject">Delete</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Object } from '@/types'
|
import config from '@/application/config'
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import type { Object } 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 { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const assetManagerStore = useAssetManagerStore()
|
const assetManagerStore = useAssetManagerStore()
|
||||||
@ -69,7 +69,7 @@ const objectTags = ref<string[]>([])
|
|||||||
const objectOriginX = ref(0)
|
const objectOriginX = ref(0)
|
||||||
const objectOriginY = ref(0)
|
const objectOriginY = ref(0)
|
||||||
const objectIsAnimated = ref(false)
|
const objectIsAnimated = ref(false)
|
||||||
const objectFrameSpeed = ref(0)
|
const objectFrameRate = ref(0)
|
||||||
const objectFrameWidth = ref(0)
|
const objectFrameWidth = ref(0)
|
||||||
const objectFrameHeight = ref(0)
|
const objectFrameHeight = ref(0)
|
||||||
|
|
||||||
@ -83,7 +83,7 @@ if (selectedObject.value) {
|
|||||||
objectOriginX.value = selectedObject.value.originX
|
objectOriginX.value = selectedObject.value.originX
|
||||||
objectOriginY.value = selectedObject.value.originY
|
objectOriginY.value = selectedObject.value.originY
|
||||||
objectIsAnimated.value = selectedObject.value.isAnimated
|
objectIsAnimated.value = selectedObject.value.isAnimated
|
||||||
objectFrameSpeed.value = selectedObject.value.frameSpeed
|
objectFrameRate.value = selectedObject.value.frameRate
|
||||||
objectFrameWidth.value = selectedObject.value.frameWidth
|
objectFrameWidth.value = selectedObject.value.frameWidth
|
||||||
objectFrameHeight.value = selectedObject.value.frameHeight
|
objectFrameHeight.value = selectedObject.value.frameHeight
|
||||||
}
|
}
|
||||||
@ -127,7 +127,7 @@ function saveObject() {
|
|||||||
originX: objectOriginX.value,
|
originX: objectOriginX.value,
|
||||||
originY: objectOriginY.value,
|
originY: objectOriginY.value,
|
||||||
isAnimated: objectIsAnimated.value,
|
isAnimated: objectIsAnimated.value,
|
||||||
frameSpeed: objectFrameSpeed.value,
|
frameRate: objectFrameRate.value,
|
||||||
frameWidth: objectFrameWidth.value,
|
frameWidth: objectFrameWidth.value,
|
||||||
frameHeight: objectFrameHeight.value
|
frameHeight: objectFrameHeight.value
|
||||||
},
|
},
|
||||||
@ -148,7 +148,7 @@ watch(selectedObject, (object: Object | null) => {
|
|||||||
objectOriginX.value = object.originX
|
objectOriginX.value = object.originX
|
||||||
objectOriginY.value = object.originY
|
objectOriginY.value = object.originY
|
||||||
objectIsAnimated.value = object.isAnimated
|
objectIsAnimated.value = object.isAnimated
|
||||||
objectFrameSpeed.value = object.frameSpeed
|
objectFrameRate.value = object.frameRate
|
||||||
objectFrameWidth.value = object.frameWidth
|
objectFrameWidth.value = object.frameWidth
|
||||||
objectFrameHeight.value = object.frameHeight
|
objectFrameHeight.value = object.frameHeight
|
||||||
})
|
})
|
||||||
|
@ -1,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">
|
||||||
<a v-for="{ data: object } in list" :key="object.id" class="relative p-2.5 cursor-pointer block" :class="{ 'bg-cyan/80': assetManagerStore.selectedObject?.id === object.id }" @click="assetManagerStore.setSelectedObject(object as Object)">
|
<a v-for="{ data: object } in list" :key="object.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedObject?.id === object.id }" @click="assetManagerStore.setSelectedObject(object as Object)">
|
||||||
<div class="flex items-center gap-2.5">
|
<div class="flex items-center gap-2.5">
|
||||||
<div class="h-7 w-16 max-w-16 flex justify-center">
|
<div class="h-7 w-16 max-w-16 flex justify-center">
|
||||||
<img class="h-7" :src="`${config.server_endpoint}/assets/objects/${object.id}.png`" alt="Object" />
|
<img class="h-7" :src="`${config.server_endpoint}/assets/objects/${object.id}.png`" alt="Object" />
|
||||||
</div>
|
</div>
|
||||||
<span>{{ object.name }}</span>
|
<span :class="{ 'text-white': assetManagerStore.selectedObject?.id === object.id }">{{ object.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<button class="left-[calc(50%_-_60px)] fixed bottom-2.5 min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
||||||
<img class="absolute invert w-8 h-8 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
|
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
||||||
|
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
|
||||||
</button>
|
</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 { Object } 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)
|
||||||
|
@ -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,17 @@ function addNewImage() {
|
|||||||
spriteActions.value = []
|
spriteActions.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
spriteActions.value.push(newImage)
|
spriteActions.value = sortSpriteActions([...spriteActions.value, newImage])
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortSpriteActions(actions: SpriteAction[]): SpriteAction[] {
|
||||||
|
return [...actions].sort((a, b) => a.action.localeCompare(b.action))
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(selectedSprite, (sprite: Sprite | null) => {
|
watch(selectedSprite, (sprite: Sprite | null) => {
|
||||||
if (!sprite) return
|
if (!sprite) return
|
||||||
spriteName.value = sprite.name
|
spriteName.value = sprite.name
|
||||||
spriteActions.value = sprite.spriteActions
|
spriteActions.value = sortSpriteActions(sprite.spriteActions)
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
@ -1,35 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative p-2.5 flex items-center gap-x-2.5">
|
<div class="relative mb-5 flex items-center gap-x-2.5">
|
||||||
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
|
<input v-model="searchQuery" class="input-field flex-grow" placeholder="Search..." @input="handleSearch" />
|
||||||
<button @click.prevent="newButtonClickHandler" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
|
<button @click.prevent="newButtonClickHandler" class="bg-cyan text-white border border-solid border-white/25 rounded drop-shadow-20 p-2.5 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-bind="containerProps" class="overflow-y-auto relative" @scroll="onScroll">
|
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||||
<div v-bind="wrapperProps" ref="elementToScroll">
|
<div v-bind="wrapperProps" ref="elementToScroll">
|
||||||
<a v-for="{ data: sprite } in list" :key="sprite.id" class="relative p-2.5 cursor-pointer block" :class="{ 'bg-cyan/80': assetManagerStore.selectedSprite?.id === sprite.id }" @click="assetManagerStore.setSelectedSprite(sprite as Sprite)">
|
<a v-for="{ data: sprite } in list" :key="sprite.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedSprite?.id === sprite.id }" @click="assetManagerStore.setSelectedSprite(sprite as Sprite)">
|
||||||
<div class="flex items-center gap-2.5">
|
<div class="flex items-center gap-2.5">
|
||||||
<span>{{ sprite.name }}</span>
|
<span :class="{ 'text-white': assetManagerStore.selectedSprite?.id === sprite.id }">{{ sprite.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<button class="left-[calc(50%_-_60px)] fixed bottom-2.5 min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
||||||
<img class="absolute invert w-8 h-8 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
|
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
||||||
|
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
|
||||||
</button>
|
</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">
|
||||||
|
<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">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<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="M6 18L18 6M6 6l12 12" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</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 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>
|
||||||
|
<div class="h-20 w-20 p-4 bg-gray-200 bg-opacity-50 rounded justify-center items-center flex hover:cursor-pointer" @click="triggerFileInput" @drop.prevent="onDrop" @dragover.prevent>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 invert" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 invert" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||||
</svg>
|
</svg>
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full overflow-auto">
|
<div class="h-full overflow-auto">
|
||||||
<div class="relative p-2.5 flex flex-col items-center justify-between h-72">
|
<div class="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray">
|
||||||
<div class="filler"></div>
|
|
||||||
<img class="max-h-72" :src="`${config.server_endpoint}/assets/tiles/${selectedTile?.id}.png`" :alt="'Tile ' + selectedTile?.id" />
|
<img class="max-h-72" :src="`${config.server_endpoint}/assets/tiles/${selectedTile?.id}.png`" :alt="'Tile ' + selectedTile?.id" />
|
||||||
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="deleteTile">Delete</button>
|
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="m-2.5 p-2.5 block">
|
<div class="mt-5 block">
|
||||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveTile">
|
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveTile">
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="name">Name</label>
|
<label for="name">Name</label>
|
||||||
@ -16,20 +13,23 @@
|
|||||||
<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>
|
||||||
|
<div class="flex gap-4">
|
||||||
<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="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 { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const assetManagerStore = useAssetManagerStore()
|
const assetManagerStore = useAssetManagerStore()
|
||||||
|
@ -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 p-2.5 inline-flex items-center justify-center hover:bg-cyan-800 hover:cursor-pointer">
|
||||||
<input class="hidden" id="upload-asset" ref="tileUploadField" type="file" accept="image/png" multiple @change="handleFileUpload" />
|
<input class="hidden" id="upload-asset" ref="tileUploadField" type="file" accept="image/png" multiple @change="handleFileUpload" />
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||||
</svg>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-bind="containerProps" class="overflow-y-auto relative" @scroll="onScroll">
|
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||||
<div v-bind="wrapperProps" ref="elementToScroll">
|
<div v-bind="wrapperProps" ref="elementToScroll">
|
||||||
<a v-for="{ data: tile } in list" :key="tile.id" class="relative p-2.5 cursor-pointer block" :class="{ 'bg-cyan/80': assetManagerStore.selectedTile?.id === tile.id }" @click="assetManagerStore.setSelectedTile(tile)">
|
<a v-for="{ data: tile } in list" :key="tile.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedTile?.id === tile.id }" @click="assetManagerStore.setSelectedTile(tile)">
|
||||||
<div class="flex items-center gap-2.5">
|
<div class="flex items-center gap-2.5">
|
||||||
<div class="h-7 w-16 max-w-16 flex justify-center">
|
<div class="h-7 w-16 max-w-16 flex justify-center">
|
||||||
<img class="h-7" :src="`${config.server_endpoint}/assets/tiles/${tile.id}.png`" alt="Tile" />
|
<img class="h-7" :src="`${config.server_endpoint}/assets/tiles/${tile.id}.png`" alt="Tile" />
|
||||||
</div>
|
</div>
|
||||||
<span>{{ tile.name }}</span>
|
<span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedTile?.id === tile.id }">{{ tile.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<button class="left-[calc(50%_-_60px)] fixed bottom-2.5 min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
||||||
<img class="absolute invert w-8 h-8 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
|
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
||||||
|
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
|
||||||
</button>
|
</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 { Tile } from '@/application/types'
|
||||||
import { onMounted, ref, computed } from 'vue'
|
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import type { Tile } 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 tileUploadField = ref(null)
|
const tileUploadField = ref(null)
|
||||||
|
@ -14,21 +14,20 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onUnmounted, ref } from 'vue'
|
import { type Zone } from '@/application/types'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import ObjectList from '@/components/gameMaster/zoneEditor/partials/ObjectList.vue'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
import TeleportModal from '@/components/gameMaster/zoneEditor/partials/TeleportModal.vue'
|
||||||
import { type Zone } from '@/types'
|
import TileList from '@/components/gameMaster/zoneEditor/partials/TileList.vue'
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import Toolbar from '@/components/gameMaster/zoneEditor/partials/Toolbar.vue'
|
import Toolbar from '@/components/gameMaster/zoneEditor/partials/Toolbar.vue'
|
||||||
import TileList from '@/components/gameMaster/zoneEditor/partials/TileList.vue'
|
|
||||||
import ObjectList from '@/components/gameMaster/zoneEditor/partials/ObjectList.vue'
|
|
||||||
import ZoneSettings from '@/components/gameMaster/zoneEditor/partials/ZoneSettings.vue'
|
|
||||||
import ZoneList from '@/components/gameMaster/zoneEditor/partials/ZoneList.vue'
|
import ZoneList from '@/components/gameMaster/zoneEditor/partials/ZoneList.vue'
|
||||||
import TeleportModal from '@/components/gameMaster/zoneEditor/partials/TeleportModal.vue'
|
import ZoneSettings from '@/components/gameMaster/zoneEditor/partials/ZoneSettings.vue'
|
||||||
import ZoneTiles from '@/components/gameMaster/zoneEditor/zonePartials/ZoneTiles.vue'
|
|
||||||
import ZoneObjects from '@/components/gameMaster/zoneEditor/zonePartials/ZoneObjects.vue'
|
|
||||||
import ZoneEventTiles from '@/components/gameMaster/zoneEditor/zonePartials/ZoneEventTiles.vue'
|
import ZoneEventTiles from '@/components/gameMaster/zoneEditor/zonePartials/ZoneEventTiles.vue'
|
||||||
|
import ZoneObjects from '@/components/gameMaster/zoneEditor/zonePartials/ZoneObjects.vue'
|
||||||
|
import ZoneTiles from '@/components/gameMaster/zoneEditor/zonePartials/ZoneTiles.vue'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
|
import { onUnmounted, ref } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal :isModalOpen="true" @modal:close="() => zoneEditorStore.toggleCreateZoneModal()" :modal-width="300" :modal-height="400" :is-resizable="false">
|
<Modal :isModalOpen="true" @modal:close="() => zoneEditorStore.toggleCreateZoneModal()" :modal-width="300" :modal-height="420" :is-resizable="false" :bg-style="'none'">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="m-0 font-medium shrink-0 text-white">Create new zone</h3>
|
<h3 class="m-0 font-medium shrink-0 text-white">Create new zone</h3>
|
||||||
</template>
|
</template>
|
||||||
@ -36,11 +36,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import type { Zone } from '@/application/types'
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
import type { Zone } from '@/types'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal :isModalOpen="zoneEditorStore.isObjectListModalShown" :modal-width="645" :modal-height="260" @modal:close="() => (zoneEditorStore.isObjectListModalShown = false)">
|
<Modal :isModalOpen="zoneEditorStore.isObjectListModalShown" :modal-width="645" :modal-height="260" @modal:close="() => (zoneEditorStore.isObjectListModalShown = false)" :bg-style="'none'">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="text-lg text-white">Objects</h3>
|
<h3 class="text-lg text-white">Objects</h3>
|
||||||
</template>
|
</template>
|
||||||
@ -41,12 +41,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/config'
|
import config from '@/application/config'
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import type { Object, ZoneObject } from '@/application/types'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import type { Object, ZoneObject } from '@/types'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const isModalOpen = ref(false)
|
const isModalOpen = ref(false)
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ZoneObject } from '@/types'
|
import type { ZoneObject } from '@/application/types'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
zoneObject: ZoneObject
|
zoneObject: ZoneObject
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal :is-modal-open="showTeleportModal" @modal:close="() => zoneEditorStore.setTool('move')" :modal-width="300" :modal-height="350" :is-resizable="false">
|
<Modal :is-modal-open="showTeleportModal" @modal:close="() => zoneEditorStore.setTool('move')" :modal-width="300" :modal-height="350" :is-resizable="false" :bg-style="'none'">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="m-0 font-medium shrink-0 text-white">Teleport settings</h3>
|
<h3 class="m-0 font-medium shrink-0 text-white">Teleport settings</h3>
|
||||||
</template>
|
</template>
|
||||||
@ -39,11 +39,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import type { Zone } from '@/application/types'
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import type { Zone } from '@/types'
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
const showTeleportModal = computed(() => zoneEditorStore.tool === 'pencil' && zoneEditorStore.drawMode === 'teleport')
|
const showTeleportModal = computed(() => zoneEditorStore.tool === 'pencil' && zoneEditorStore.drawMode === 'teleport')
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal :isModalOpen="zoneEditorStore.isTileListModalShown" :modal-width="645" :modal-height="600" @modal:close="() => (zoneEditorStore.isTileListModalShown = false)">
|
<Modal :isModalOpen="zoneEditorStore.isTileListModalShown" :modal-width="645" :modal-height="600" @modal:close="() => (zoneEditorStore.isTileListModalShown = false)" :bg-style="'none'">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="text-lg text-white">Tiles</h3>
|
<h3 class="text-lg text-white">Tiles</h3>
|
||||||
</template>
|
</template>
|
||||||
@ -81,12 +81,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/config'
|
import config from '@/application/config'
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import type { Tile } from '@/application/types'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import type { Tile } from '@/types'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const isModalOpen = ref(false)
|
const isModalOpen = ref(false)
|
||||||
@ -223,7 +223,7 @@ function selectTile(tile: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isActiveTile(tile: Tile): boolean {
|
function isActiveTile(tile: Tile): boolean {
|
||||||
return zoneEditorStore.selectedTile?.id === tile.id
|
return zoneEditorStore.selectedTile === tile.id
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
@ -82,9 +82,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
import { onClickOutside } from '@vueuse/core'
|
import { onClickOutside } from '@vueuse/core'
|
||||||
|
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<CreateZone v-if="zoneEditorStore.isCreateZoneModalShown" />
|
<CreateZone v-if="zoneEditorStore.isCreateZoneModalShown" />
|
||||||
<Modal :is-modal-open="zoneEditorStore.isZoneListModalShown" @modal:close="() => zoneEditorStore.toggleZoneListModal()" :is-resizable="false" :modal-width="300" :modal-height="360">
|
<Modal :is-modal-open="zoneEditorStore.isZoneListModalShown" @modal:close="() => zoneEditorStore.toggleZoneListModal()" :is-resizable="false" :modal-width="300" :modal-height="360" :bg-style="'none'">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="text-lg text-white">Zones</h3>
|
<h3 class="text-lg text-white">Zones</h3>
|
||||||
</template>
|
</template>
|
||||||
@ -15,7 +15,7 @@
|
|||||||
<div class="flex gap-3 items-center w-full" @click="() => loadZone(zone.id)">
|
<div class="flex gap-3 items-center w-full" @click="() => loadZone(zone.id)">
|
||||||
<span>{{ zone.name }}</span>
|
<span>{{ zone.name }}</span>
|
||||||
<span class="ml-auto gap-1 flex">
|
<span class="ml-auto gap-1 flex">
|
||||||
<button class="btn-red w-11 h-11 z-50" @click.stop="() => deleteZone(zone.id)">X</button>
|
<button class="btn-red w-7 h-7 z-50 flex items-center justify-center" @click.stop="() => deleteZone(zone.id)">x</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||||
@ -26,12 +26,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted } from 'vue'
|
import type { Zone } from '@/application/types'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
|
||||||
import type { Zone } from '@/types'
|
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
|
||||||
import CreateZone from '@/components/gameMaster/zoneEditor/partials/CreateZone.vue'
|
import CreateZone from '@/components/gameMaster/zoneEditor/partials/CreateZone.vue'
|
||||||
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal :is-modal-open="zoneEditorStore.isSettingsModalShown" @modal:close="() => zoneEditorStore.toggleSettingsModal()" :modal-width="600" :modal-height="350">
|
<Modal :is-modal-open="zoneEditorStore.isSettingsModalShown" @modal:close="() => zoneEditorStore.toggleSettingsModal()" :modal-width="600" :modal-height="430" :bg-style="'none'">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="m-0 font-medium shrink-0 text-white">Zone settings</h3>
|
<h3 class="m-0 font-medium shrink-0 text-white">Zone settings</h3>
|
||||||
</template>
|
</template>
|
||||||
@ -47,9 +47,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue'
|
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
const screen = ref('settings')
|
const screen = ref('settings')
|
||||||
|
@ -3,11 +3,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { type ZoneEventTile, ZoneEventTileType } from '@/types'
|
import { ZoneEventTileType, type ZoneEventTile } from '@/application/types'
|
||||||
|
import { uuidv4 } from '@/application/utilities'
|
||||||
|
import { getTile, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
import { Image, useScene } from 'phavuer'
|
import { Image, useScene } from 'phavuer'
|
||||||
import { getTile, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
|
||||||
import { uuidv4 } from '@/utilities'
|
|
||||||
import { onMounted, onUnmounted } from 'vue'
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
|
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
|
@ -3,12 +3,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import type { AssetDataT, ZoneObject } from '@/application/types'
|
||||||
import { Image, useScene } from 'phavuer'
|
|
||||||
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
|
||||||
import { loadTexture } from '@/composables/gameComposable'
|
import { loadTexture } from '@/composables/gameComposable'
|
||||||
import type { AssetDataT, ZoneObject } from '@/types'
|
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { Image, useScene } from 'phavuer'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
tilemap: Phaser.Tilemaps.Tilemap
|
tilemap: Phaser.Tilemaps.Tilemap
|
||||||
|
@ -4,14 +4,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { uuidv4 } from '@/utilities'
|
import type { ZoneObject as ZoneObjectT } from '@/application/types'
|
||||||
import { getTile } from '@/composables/zoneComposable'
|
import { uuidv4 } from '@/application/utilities'
|
||||||
import { useScene } from 'phavuer'
|
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
|
||||||
import SelectedZoneObject from '@/components/gameMaster/zoneEditor/partials/SelectedZoneObject.vue'
|
import SelectedZoneObject from '@/components/gameMaster/zoneEditor/partials/SelectedZoneObject.vue'
|
||||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
|
||||||
import ZoneObject from '@/components/gameMaster/zoneEditor/zonePartials/ZoneObject.vue'
|
import ZoneObject from '@/components/gameMaster/zoneEditor/zonePartials/ZoneObject.vue'
|
||||||
import type { ZoneObject as ZoneObjectT } from '@/types'
|
import { getTile } from '@/composables/zoneComposable'
|
||||||
|
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||||
|
import { useScene } from 'phavuer'
|
||||||
|
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
const zoneEditorStore = useZoneEditorStore()
|
||||||
@ -56,7 +56,7 @@ function pencil(pointer: Phaser.Input.Pointer) {
|
|||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
zoneId: zoneEditorStore.zone.id,
|
zoneId: zoneEditorStore.zone.id,
|
||||||
zone: zoneEditorStore.zone,
|
zone: zoneEditorStore.zone,
|
||||||
objectId: zoneEditorStore.selectedObject,
|
objectId: zoneEditorStore.selectedObject.id,
|
||||||
object: zoneEditorStore.selectedObject,
|
object: zoneEditorStore.selectedObject,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
isRotated: false,
|
isRotated: false,
|
||||||
@ -65,7 +65,7 @@ function pencil(pointer: Phaser.Input.Pointer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add new object to zoneObjects
|
// Add new object to zoneObjects
|
||||||
zoneEditorStore.zone.zoneObjects = zoneEditorStore.zone.zoneObjects.concat(newObject as ZoneObject)
|
zoneEditorStore.zone.zoneObjects = zoneEditorStore.zone.zoneObjects.concat(newObject as ZoneObjectT)
|
||||||
}
|
}
|
||||||
|
|
||||||
function eraser(pointer: Phaser.Input.Pointer) {
|
function eraser(pointer: Phaser.Input.Pointer) {
|
||||||
@ -134,7 +134,7 @@ function moveZoneObject(id: string) {
|
|||||||
// Check if zone is set
|
// Check if zone is set
|
||||||
if (!zoneEditorStore.zone) return
|
if (!zoneEditorStore.zone) return
|
||||||
|
|
||||||
movingZoneObject.value = zoneEditorStore.zone.zoneObjects.find((object) => object.id === id) as ZoneObject
|
movingZoneObject.value = zoneEditorStore.zone.zoneObjects.find((object) => object.id === id) as ZoneObjectT
|
||||||
|
|
||||||
function handlePointerMove(pointer: Phaser.Input.Pointer) {
|
function handlePointerMove(pointer: Phaser.Input.Pointer) {
|
||||||
if (!movingZoneObject.value) return
|
if (!movingZoneObject.value) return
|
||||||
|