Compare commits
124 Commits
feature/#1
...
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 | |||
bdc566e30f | |||
a653b61b51 | |||
7b61f71fa9 | |||
42539cc73d | |||
864369860c |
@ -1,4 +1,4 @@
|
||||
VITE_NAME=Sylvan Quest
|
||||
VITE_NAME=Noxious
|
||||
VITE_DEVELOPMENT=true
|
||||
VITE_SERVER_ENDPOINT=http://localhost:4000
|
||||
VITE_TILE_SIZE_X=64
|
||||
|
@ -4,5 +4,8 @@
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"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">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<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>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
1559
package-lock.json
generated
@ -27,6 +27,7 @@
|
||||
"zod": "^3.22.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.4.0",
|
||||
"@rushstack/eslint-patch": "^1.10.3",
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
@ -37,7 +38,6 @@
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"easystarjs": "^0.4.4",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-vue": "^9.27.0",
|
||||
"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 |
@ -1,3 +1,3 @@
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.91481 9.08504C7.71955 9.2803 7.40297 9.2803 7.20771 9.08504L5.55709 7.43442C5.36183 7.23916 5.04524 7.23916 4.84998 7.43442L2.99578 9.28862C2.80052 9.48389 2.48393 9.48389 2.28867 9.28863L1.00416 8.00412C0.808899 7.80885 0.808899 7.49227 1.00416 7.29701L2.85837 5.4428C3.05363 5.24754 3.05363 4.93096 2.85837 4.7357L0.914865 2.7922C0.719603 2.59693 0.719603 2.28035 0.914865 2.08509L2.07053 0.929423C2.26579 0.734161 2.58238 0.734162 2.77764 0.929424L4.72114 2.87293C4.9164 3.06819 5.23298 3.06819 5.42825 2.87293L7.297 1.00417C7.49226 0.808906 7.80885 0.808906 8.00411 1.00417L9.28862 2.28868C9.48388 2.48394 9.48388 2.80052 9.28862 2.99578L7.41986 4.86454C7.2246 5.0598 7.2246 5.37639 7.41986 5.57165L9.07048 7.22227C9.26574 7.41753 9.26574 7.73411 9.07048 7.92937L7.91481 9.08504Z" fill="white"/>
|
||||
</svg>
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.91481 9.08504C7.71955 9.2803 7.40297 9.2803 7.20771 9.08504L5.55709 7.43442C5.36183 7.23916 5.04524 7.23916 4.84998 7.43442L2.99578 9.28862C2.80052 9.48389 2.48393 9.48389 2.28867 9.28863L1.00416 8.00412C0.808899 7.80885 0.808899 7.49227 1.00416 7.29701L2.85837 5.4428C3.05363 5.24754 3.05363 4.93096 2.85837 4.7357L0.914865 2.7922C0.719603 2.59693 0.719603 2.28035 0.914865 2.08509L2.07053 0.929423C2.26579 0.734161 2.58238 0.734162 2.77764 0.929424L4.72114 2.87293C4.9164 3.06819 5.23298 3.06819 5.42825 2.87293L7.297 1.00417C7.49226 0.808906 7.80885 0.808906 8.00411 1.00417L9.28862 2.28868C9.48388 2.48394 9.48388 2.80052 9.28862 2.99578L7.41986 4.86454C7.2246 5.0598 7.2246 5.37639 7.41986 5.57165L9.07048 7.22227C9.26574 7.41753 9.26574 7.73411 9.07048 7.92937L7.91481 9.08504Z" fill="white"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 915 B After Width: | Height: | Size: 918 B |
Before Width: | Height: | Size: 768 B After Width: | Height: | Size: 768 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 375 B After Width: | Height: | Size: 375 B |
Before Width: | Height: | Size: 652 B After Width: | Height: | Size: 652 B |
Before Width: | Height: | Size: 346 B After Width: | Height: | Size: 346 B |
@ -1 +0,0 @@
|
||||
<svg width="48" xmlns="http://www.w3.org/2000/svg" height="48" id="screenshot-e9346f42-72c8-800c-8004-507b356b7f18" viewBox="0 0 48 48" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-e9346f42-72c8-800c-8004-507b356b7f18" width="1em" height="1em" rx="0" ry="0" style="fill: rgb(0, 0, 0);"><g id="shape-e9346f42-72c8-800c-8004-507b356b7f1b"><defs style="fill: rgb(0, 0, 0);"/></g><g id="shape-e9346f42-72c8-800c-8004-507b356b7f1c"><defs><mask width="1.2" height="1.2" x="-0.1" id="render-3-ipSEnterKey0" data-old-y="-0.1" data-old-width="1.2" data-old-x="-0.1" y="-0.1" data-old-height="1.2"><g class="svg-mask-wrapper" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"><path fill="#fff" stroke="#fff" d="M44 44V4H24v16H4v24z"/><path stroke="#000" d="m21 28l-4 4l4 4"/><path stroke="#000" d="M34 23v9H17"/></g></g></mask></defs><g class="fills" id="fills-e9346f42-72c8-800c-8004-507b356b7f1c"><path d="M0.000,0.000L48.000,0.000L48.000,48.000L0.000,48.000ZZ" mask="url(#render-3-ipSEnterKey0)" style="fill: #696969; fill-opacity: 1;"/></g></g></g></svg>
|
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 2.1 KiB |
3
public/assets/icons/triangle-icon.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="13" height="16" viewBox="0 0 13 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.00044 7.42339L12.0005 1.07252C12.3193 0.888385 12.7271 0.997653 12.9111 1.31652C12.9697 1.41785 13.0005 1.53285 13.0005 1.64985L13.0005 14.3516C13.0005 14.7198 12.702 15.0182 12.3338 15.0182C12.2167 15.0182 12.1018 14.9874 12.0005 14.9289L1.00044 8.57805C0.681573 8.39399 0.572326 7.98625 0.756419 7.66739C0.814932 7.56605 0.899092 7.48185 1.00044 7.42339Z" fill="#4D4D4D"/>
|
||||
</svg>
|
After Width: | Height: | Size: 490 B |
3
public/assets/icons/x-button-gray.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.5859 10.0001L0.792969 2.20718L2.20718 0.792969L10.0001 8.58582L17.793 0.792969L19.2072 2.20718L11.4143 10.0001L19.2072 17.7929L17.793 19.2072L10.0001 11.4143L2.20718 19.2072L0.792969 17.7929L8.5859 10.0001Z" fill="#999999"/>
|
||||
</svg>
|
After Width: | Height: | Size: 340 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 274 B |
@ -1 +0,0 @@
|
||||
<svg width="170" xmlns="http://www.w3.org/2000/svg" height="275" id="screenshot-c7008730-586b-8052-8004-79a2f5807f5c" viewBox="0 0 170 275" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-c7008730-586b-8052-8004-79a2f5807f5c"><defs><linearGradient id="fill-color-gradient-render-1-0" x1="0.4238862507773219" y1="0.26897966364321624" x2="0.7513711107948178" y2="0.9146156628521559" gradientTransform=""><stop offset="0" stop-color="#ffffff" stop-opacity="0.4"/><stop offset="1" stop-color="#ffffff" stop-opacity="0"/></linearGradient><pattern patternUnits="userSpaceOnUse" x="0" y="0" width="170" height="275" patternTransform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)" id="fill-0-render-1"><g><rect width="170" height="275" style="fill: url("#fill-color-gradient-render-1-0");"/></g></pattern></defs><g class="fills" id="fills-c7008730-586b-8052-8004-79a2f5807f5c"><path d="M170.000,20.000L170.000,255.000C170.000,266.038,161.038,275.000,150.000,275.000L20.000,275.000C8.962,275.000,0.000,266.038,0.000,255.000L0.000,20.000C0.000,8.962,8.962,0.000,20.000,0.000L150.000,0.000C150.000,0.000,150.000,0.000,150.000,0.000C150.000,11.038,158.962,20.000,170.000,20.000Z" fill="url(#fill-0-render-1)"/></g><g id="strokes-c7008730-586b-8052-8004-79a2f5807f5c" class="strokes"><g class="inner-stroke-shape"><defs><clipPath id="inner-stroke-render-1-c7008730-586b-8052-8004-79a2f5807f5c-0"><use href="#stroke-shape-render-1-c7008730-586b-8052-8004-79a2f5807f5c-0"/></clipPath><path d="M170.000,20.000L170.000,255.000C170.000,266.038,161.038,275.000,150.000,275.000L20.000,275.000C8.962,275.000,0.000,266.038,0.000,255.000L0.000,20.000C0.000,8.962,8.962,0.000,20.000,0.000L150.000,0.000C150.000,0.000,150.000,0.000,150.000,0.000C150.000,11.038,158.962,20.000,170.000,20.000Z" id="stroke-shape-render-1-c7008730-586b-8052-8004-79a2f5807f5c-0" style="fill: none; stroke-width: 4; stroke: rgb(255, 255, 255); stroke-opacity: 1;"/></defs><use href="#stroke-shape-render-1-c7008730-586b-8052-8004-79a2f5807f5c-0" clip-path="url('#inner-stroke-render-1-c7008730-586b-8052-8004-79a2f5807f5c-0')"/></g></g></g></svg>
|
Before Width: | Height: | Size: 2.1 KiB |
@ -1 +0,0 @@
|
||||
<svg width="290" xmlns="http://www.w3.org/2000/svg" height="87" id="screenshot-e9942e24-155b-8096-8004-7eaff9882cd6" viewBox="0 0 290 87" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-e9942e24-155b-8096-8004-7eaff9882cd6"><g class="fills" id="fills-e9942e24-155b-8096-8004-7eaff9882cd6"><path d="M3.485,8.722C7.089,3.457,13.144,0.000,20.000,0.000L270.000,0.000C281.038,0.000,290.000,8.962,290.000,20.000L290.000,67.000C290.000,78.038,281.038,87.000,270.000,87.000L20.000,87.000C13.166,87.000,7.128,83.565,3.520,78.329C21.157,76.589,35.000,61.648,35.000,43.500C35.000,25.390,21.216,10.475,3.485,8.722ZM0.000,67.000L0.000,20.000"/></g><g id="strokes-e9942e24-155b-8096-8004-7eaff9882cd6" class="strokes"><g class="inner-stroke-shape"><defs><clipPath id="inner-stroke-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0"><use href="#stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0"/></clipPath><path d="M3.485,8.722C7.089,3.457,13.144,0.000,20.000,0.000L270.000,0.000C281.038,0.000,290.000,8.962,290.000,20.000L290.000,67.000C290.000,78.038,281.038,87.000,270.000,87.000L20.000,87.000C13.166,87.000,7.128,83.565,3.520,78.329C21.157,76.589,35.000,61.648,35.000,43.500C35.000,25.390,21.216,10.475,3.485,8.722ZM0.000,67.000L0.000,20.000" id="stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0" style="fill: none; stroke-width: 6; stroke: rgb(77 77 77); stroke-opacity: 1;"/></defs><use href="#stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0" clip-path="url('#inner-stroke-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0')"/></g></g></g></svg>
|
Before Width: | Height: | Size: 1.6 KiB |
@ -1 +0,0 @@
|
||||
<svg width="290" xmlns="http://www.w3.org/2000/svg" height="87" id="screenshot-e9942e24-155b-8096-8004-7eaff9882cd6" viewBox="0 0 290 87" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-e9942e24-155b-8096-8004-7eaff9882cd6"><g class="fills" id="fills-e9942e24-155b-8096-8004-7eaff9882cd6"><path d="M3.485,8.722C7.089,3.457,13.144,0.000,20.000,0.000L270.000,0.000C281.038,0.000,290.000,8.962,290.000,20.000L290.000,67.000C290.000,78.038,281.038,87.000,270.000,87.000L20.000,87.000C13.166,87.000,7.128,83.565,3.520,78.329C21.157,76.589,35.000,61.648,35.000,43.500C35.000,25.390,21.216,10.475,3.485,8.722ZM0.000,67.000L0.000,20.000" style="fill: rgb(127, 127, 127); fill-opacity: 0.7;"/></g><g id="strokes-e9942e24-155b-8096-8004-7eaff9882cd6" class="strokes"><g class="inner-stroke-shape"><defs><clipPath id="inner-stroke-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0"><use href="#stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0"/></clipPath><path d="M3.485,8.722C7.089,3.457,13.144,0.000,20.000,0.000L270.000,0.000C281.038,0.000,290.000,8.962,290.000,20.000L290.000,67.000C290.000,78.038,281.038,87.000,270.000,87.000L20.000,87.000C13.166,87.000,7.128,83.565,3.520,78.329C21.157,76.589,35.000,61.648,35.000,43.500C35.000,25.390,21.216,10.475,3.485,8.722ZM0.000,67.000L0.000,20.000" id="stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0" style="fill: none; stroke-width: 6; stroke: rgb(255, 255, 255); stroke-opacity: 1;"/></defs><use href="#stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0" clip-path="url('#inner-stroke-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0')"/></g></g></g></svg>
|
Before Width: | Height: | Size: 1.6 KiB |
@ -1 +0,0 @@
|
||||
<svg width="1508.086" xmlns="http://www.w3.org/2000/svg" height="1511.251" id="screenshot-0d120e2a-8725-8061-8004-79728483f7ea" viewBox="-201.784 -208.012 1508.086 1511.251" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-0d120e2a-8725-8061-8004-79728483f7ea" width="800px" height="800px" rx="0" ry="0" style="opacity: 0.3; fill: rgb(0, 0, 0);"><g id="shape-0d120e2a-8725-8061-8004-79728484b3fe"><g class="fills" id="fills-0d120e2a-8725-8061-8004-79728484b3fe"><path d="M1190.359,745.690L1043.367,575.945L1133.504,630.603C1099.180,585.722,1047.978,532.622,975.722,469.519L780.783,401.898L896.468,404.538C851.234,371.382,797.068,339.090,738.130,311.073L601.350,337.338L689.349,289.039C627.425,263.088,562.143,241.922,497.713,229.160C430.172,215.674,363.453,211.534,303.512,221.641L314.753,151.382C271.664,177.012,239.130,209.992,214.226,251.017L204.390,177.710C166.181,212.950,148.095,250.172,143.131,301.343C69.092,307.974,-2.300,327.925,-73.861,347.005L-63.628,384.898C361.675,238.903,753.109,407.667,987.467,615.054L960.305,643.506C749.259,458.743,490.332,358.712,193.406,380.541C209.110,415.226,228.858,447.126,251.288,474.785L371.998,430.671L277.417,504.449C294.635,521.771,312.591,536.670,331.111,548.765L396.081,470.998L358.366,564.253C377.625,573.924,397.183,580.217,416.674,582.837C534.164,599.232,652.310,618.566,782.785,703.535L773.618,601.955L831.163,737.792C852.261,754.489,876.370,771.191,897.168,782.701L861.169,684.190L960.409,811.519C976.589,817.512,992.991,822.477,1008.953,826.486C1066.083,840.287,1120.015,842.594,1157.256,819.002C1175.975,807.393,1189.205,786.963,1190.791,762.853C1191.080,756.933,1190.907,750.762,1190.359,745.690ZZ" style="fill: rgb(13 109 105);"/></g></g></g></svg>
|
Before Width: | Height: | Size: 1.7 KiB |
BIN
public/assets/tlogo.png
Normal file
After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 470 KiB After Width: | Height: | Size: 470 KiB |
Before Width: | Height: | Size: 471 KiB After Width: | Height: | Size: 471 KiB |
Before Width: | Height: | Size: 598 KiB After Width: | Height: | Size: 598 KiB |
25
public/assets/ui-elements/character-select-ui-shape.svg
Normal file
@ -0,0 +1,25 @@
|
||||
<svg width="190" height="202" viewBox="0 0 190 202" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 8H8V0C7.40741 4.14815 4.14815 7.40741 0 8Z" fill="#1A1A1A"/>
|
||||
<path d="M7.55 0V0C7.55 4.16975 4.16975 7.55 0 7.55V7.55" stroke="#4D4D4D"/>
|
||||
<mask id="path-3-inside-1_632_705" fill="white">
|
||||
<path d="M0 8H8V194H0V8Z"/>
|
||||
</mask>
|
||||
<path d="M0 8H8V194H0V8Z" fill="#1A1A1A"/>
|
||||
<path d="M1 194V8H-1V194H1Z" fill="#4D4D4D" mask="url(#path-3-inside-1_632_705)"/>
|
||||
<path d="M0 194H8V202C7.40741 197.852 4.14815 194.593 0 194Z" fill="#1A1A1A"/>
|
||||
<path d="M7.55 202V202C7.55 197.83 4.16975 194.45 0 194.45V194.45" stroke="#4D4D4D"/>
|
||||
<mask id="path-7-inside-2_632_705" fill="white">
|
||||
<path d="M8 0H182V202H8V0Z"/>
|
||||
</mask>
|
||||
<path d="M8 0H182V202H8V0Z" fill="#1A1A1A"/>
|
||||
<path d="M8 1H182V-1H8V1ZM182 201H8V203H182V201Z" fill="#4D4D4D" mask="url(#path-7-inside-2_632_705)"/>
|
||||
<path d="M190 8H182V0C182.593 4.14815 185.852 7.40741 190 8Z" fill="#1A1A1A"/>
|
||||
<path d="M182.45 0V0C182.45 4.16975 185.83 7.55 190 7.55V7.55" stroke="#4D4D4D"/>
|
||||
<mask id="path-11-inside-3_632_705" fill="white">
|
||||
<path d="M190 8H182V194H190V8Z"/>
|
||||
</mask>
|
||||
<path d="M190 8H182V194H190V8Z" fill="#1A1A1A"/>
|
||||
<path d="M189 194V8H191V194H189Z" fill="#4D4D4D" mask="url(#path-11-inside-3_632_705)"/>
|
||||
<path d="M190 194H182V202C182.593 197.852 185.852 194.593 190 194Z" fill="#1A1A1A"/>
|
||||
<path d="M182.45 202V202C182.45 197.83 185.83 194.45 190 194.45V194.45" stroke="#4D4D4D"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 470 KiB After Width: | Height: | Size: 470 KiB |
23
public/assets/ui-elements/login-ui-box-inner.svg
Normal file
After Width: | Height: | Size: 302 KiB |
23
public/assets/ui-elements/login-ui-box-outer.svg
Normal file
After Width: | Height: | Size: 400 KiB |
Before Width: | Height: | Size: 301 KiB |
Before Width: | Height: | Size: 400 KiB |
Before Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 2.1 MiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 1.1 MiB |
24
src/App.vue
@ -1,20 +1,20 @@
|
||||
<template>
|
||||
<Notifications />
|
||||
<GmTools v-if="gameStore.character?.role === 'gm'" />
|
||||
<BackgroundImageLoader />
|
||||
<GmPanel v-if="gameStore.character?.role === 'gm'" />
|
||||
<component :is="currentScreen" />
|
||||
</template>
|
||||
|
||||
<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 Login from '@/components/screens/Login.vue'
|
||||
import Characters from '@/components/screens/Characters.vue'
|
||||
import Game from '@/components/screens/Game.vue'
|
||||
import Login from '@/components/screens/Login.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'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
@ -44,4 +44,16 @@ addEventListener('click', (event) => {
|
||||
const audio = new Audio('/assets/music/click-btn.mp3')
|
||||
audio.play()
|
||||
})
|
||||
|
||||
// Watch for "G" key press and toggle the gm panel
|
||||
addEventListener('keydown', (event) => {
|
||||
if (gameStore.character?.role !== 'gm') return // Only allow toggling the gm panel if the character is a gm
|
||||
|
||||
// Check if no input is active or focus is on an input
|
||||
if (event.repeat || event.isComposing || event.defaultPrevented || document.activeElement?.tagName.toUpperCase() === 'INPUT' || document.activeElement?.tagName.toUpperCase() === 'TEXTAREA') return
|
||||
|
||||
if (event.key === 'G') {
|
||||
gameStore.toggleGmPanel()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
@ -1,8 +1,8 @@
|
||||
import config from '@/config'
|
||||
import config from '@/application/config'
|
||||
import type { AssetDataT } from '@/application/types'
|
||||
import Dexie from 'dexie'
|
||||
import type { AssetDataT } from '@/types'
|
||||
|
||||
export class AssetStorage {
|
||||
export class Assets {
|
||||
private db: Dexie
|
||||
|
||||
constructor() {
|
||||
@ -25,7 +25,19 @@ export class AssetStorage {
|
||||
const blob = await response.blob()
|
||||
|
||||
// Store the asset in the database
|
||||
await this.db.table('assets').put({ key: asset.key, data: blob, group: asset.group, updatedAt: asset.updatedAt, frameCount: asset.frameCount, frameWidth: asset.frameWidth, frameHeight: asset.frameHeight })
|
||||
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) {
|
||||
console.error(`Failed to add asset ${asset.key}:`, error)
|
||||
}
|
@ -1,22 +1,33 @@
|
||||
export type UUID = `${string}-${string}-${string}-${string}-${string}`
|
||||
|
||||
export type Notification = {
|
||||
id?: string
|
||||
title?: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
export type HttpResponse<T> = {
|
||||
success: boolean
|
||||
message?: string
|
||||
data?: T
|
||||
}
|
||||
|
||||
export type AssetDataT = {
|
||||
key: string
|
||||
data: string
|
||||
group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
|
||||
updatedAt: Date
|
||||
originX?: number
|
||||
originY?: number
|
||||
isAnimated?: boolean
|
||||
frameCount?: number
|
||||
frameRate?: number
|
||||
frameWidth?: number
|
||||
frameHeight?: number
|
||||
frameCount?: number
|
||||
}
|
||||
|
||||
export type Tile = {
|
||||
id: string
|
||||
id: UUID
|
||||
name: string
|
||||
tags: any | null
|
||||
createdAt: Date
|
||||
@ -24,13 +35,13 @@ export type Tile = {
|
||||
}
|
||||
|
||||
export type Object = {
|
||||
id: string
|
||||
id: UUID
|
||||
name: string
|
||||
tags: any | null
|
||||
originX: number
|
||||
originY: number
|
||||
isAnimated: boolean
|
||||
frameSpeed: number
|
||||
frameRate: number
|
||||
frameWidth: number
|
||||
frameHeight: number
|
||||
createdAt: Date
|
||||
@ -39,17 +50,23 @@ export type Object = {
|
||||
}
|
||||
|
||||
export type Item = {
|
||||
id: string
|
||||
id: UUID
|
||||
name: string
|
||||
description: string | null
|
||||
itemType: ItemType
|
||||
stackable: boolean
|
||||
rarity: ItemRarity
|
||||
spriteId: UUID | null
|
||||
sprite?: Sprite
|
||||
createdAt: 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 = {
|
||||
id: number
|
||||
id: UUID
|
||||
name: string
|
||||
width: number
|
||||
height: number
|
||||
@ -65,18 +82,18 @@ export type Zone = {
|
||||
}
|
||||
|
||||
export type ZoneEffect = {
|
||||
id: string
|
||||
zoneId: number
|
||||
id: UUID
|
||||
zoneId: UUID
|
||||
zone: Zone
|
||||
effect: string
|
||||
strength: number
|
||||
}
|
||||
|
||||
export type ZoneObject = {
|
||||
id: string
|
||||
zoneId: number
|
||||
id: UUID
|
||||
zoneId: UUID
|
||||
zone: Zone
|
||||
objectId: string
|
||||
objectId: UUID
|
||||
object: Object
|
||||
depth: number
|
||||
isRotated: boolean
|
||||
@ -92,8 +109,8 @@ export enum ZoneEventTileType {
|
||||
}
|
||||
|
||||
export type ZoneEventTile = {
|
||||
id: string
|
||||
zoneId: number
|
||||
id: UUID
|
||||
zoneId: UUID
|
||||
zone: Zone
|
||||
type: ZoneEventTileType
|
||||
positionX: number
|
||||
@ -102,10 +119,10 @@ export type ZoneEventTile = {
|
||||
}
|
||||
|
||||
export type ZoneEventTileTeleport = {
|
||||
id: string
|
||||
zoneEventTileId: string
|
||||
id: UUID
|
||||
zoneEventTileId: UUID
|
||||
zoneEventTile: ZoneEventTile
|
||||
toZoneId: number
|
||||
toZoneId: UUID
|
||||
toZone: Zone
|
||||
toPositionX: number
|
||||
toPositionY: number
|
||||
@ -113,7 +130,7 @@ export type ZoneEventTileTeleport = {
|
||||
}
|
||||
|
||||
export type User = {
|
||||
id: number
|
||||
id: UUID
|
||||
username: string
|
||||
password: string
|
||||
characters: Character[]
|
||||
@ -133,10 +150,11 @@ export enum CharacterRace {
|
||||
}
|
||||
|
||||
export type CharacterType = {
|
||||
id: number
|
||||
id: UUID
|
||||
name: string
|
||||
gender: CharacterGender
|
||||
race: CharacterRace
|
||||
isSelectable: boolean
|
||||
characters: Character[]
|
||||
spriteId?: string
|
||||
sprite?: Sprite
|
||||
@ -144,9 +162,17 @@ export type CharacterType = {
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
export type CharacterHair = {
|
||||
id: UUID
|
||||
name: string
|
||||
sprite: Sprite
|
||||
gender: CharacterGender
|
||||
isSelectable: boolean
|
||||
}
|
||||
|
||||
export type Character = {
|
||||
id: number
|
||||
userId: number
|
||||
id: UUID
|
||||
userId: UUID
|
||||
user: User
|
||||
name: string
|
||||
hitpoints: number
|
||||
@ -158,29 +184,49 @@ export type Character = {
|
||||
positionX: number
|
||||
positionY: number
|
||||
rotation: number
|
||||
zoneId: number
|
||||
characterTypeId: UUID | null
|
||||
characterType: CharacterType | null | string
|
||||
characterHairId: UUID | null
|
||||
characterHair: CharacterHair | null
|
||||
zoneId: UUID
|
||||
zone: Zone
|
||||
characterTypeId: number | null
|
||||
characterType: CharacterType | null
|
||||
chats: Chat[]
|
||||
items: CharacterItem[]
|
||||
equipment: CharacterEquipment[]
|
||||
}
|
||||
|
||||
export type ExtendedCharacter = Character & {
|
||||
export type ZoneCharacter = {
|
||||
character: Character
|
||||
isMoving?: boolean
|
||||
}
|
||||
|
||||
export type CharacterItem = {
|
||||
id: number
|
||||
characterId: number
|
||||
id: UUID
|
||||
characterId: UUID
|
||||
character: Character
|
||||
itemId: string
|
||||
itemId: UUID
|
||||
item: Item
|
||||
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 = {
|
||||
id: string
|
||||
id: UUID
|
||||
name: string
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
@ -189,8 +235,7 @@ export type Sprite = {
|
||||
}
|
||||
|
||||
export type SpriteAction = {
|
||||
id: string
|
||||
spriteId: string
|
||||
id: UUID
|
||||
sprite: Sprite
|
||||
action: string
|
||||
sprites: string[]
|
||||
@ -200,27 +245,34 @@ export type SpriteAction = {
|
||||
isLooping: boolean
|
||||
frameWidth: number
|
||||
frameHeight: number
|
||||
frameSpeed: number
|
||||
frameRate: number
|
||||
}
|
||||
|
||||
export type Chat = {
|
||||
id: number
|
||||
characterId: number
|
||||
id: UUID
|
||||
characterId: UUID
|
||||
character: Character
|
||||
zoneId: number
|
||||
zoneId: UUID
|
||||
zone: Zone
|
||||
message: string
|
||||
createdAt: Date
|
||||
}
|
||||
|
||||
export type ChatMessage = {
|
||||
character: Character
|
||||
message: string
|
||||
}
|
||||
|
||||
export type WorldSettings = {
|
||||
date: Date
|
||||
isRainEnabled: boolean
|
||||
isFogEnabled: boolean
|
||||
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
|
||||
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,
|
||||
@ -45,7 +53,7 @@ label {
|
||||
|
||||
button,
|
||||
a {
|
||||
@apply font-medium drop-shadow-20;
|
||||
@apply font-medium;
|
||||
}
|
||||
|
||||
button,
|
||||
@ -58,13 +66,20 @@ input {
|
||||
appearance: textfield;
|
||||
}
|
||||
&[type='number']::-webkit-inner-spin-button,
|
||||
&[type='number']::-webkit-outer-spin-button {
|
||||
&[type='number']::-webkit-outer-spin-button,
|
||||
&[type='radio'] {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
}
|
||||
|
||||
.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 {
|
||||
@apply bg-gray-600/50 hover:cursor-not-allowed;
|
||||
&::placeholder {
|
||||
@ -99,11 +114,11 @@ button {
|
||||
}
|
||||
|
||||
&.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,
|
||||
&: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;
|
||||
|
||||
&.active,
|
||||
&.selected,
|
||||
&:hover {
|
||||
@apply bg-gray-700 border-gray-700;
|
||||
@apply bg-gray border-gray;
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,6 +139,28 @@ button {
|
||||
&.eye-open {
|
||||
@apply bg-[url('/assets/icons/eye-closed.svg')] w-5 h-4 right-2.5;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
@apply bg-gray-500 border-gray-400;
|
||||
}
|
||||
}
|
||||
|
||||
.character.active {
|
||||
@apply 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 {
|
||||
@ -133,6 +171,15 @@ button {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
.scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
@apply block w-0.5 bg-gray-300 rounded-sm;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
@apply bg-gray-175;
|
||||
}
|
||||
}
|
||||
|
||||
canvas {
|
||||
image-rendering: -moz-crisp-edges;
|
||||
image-rendering: -webkit-crisp-edges;
|
||||
|
@ -1,110 +1,179 @@
|
||||
<template>
|
||||
<Scene name="effects" @preload="preloadScene" @create="createScene" @update="updateScene"> </Scene>
|
||||
<Scene name="effects" @preload="preloadScene" @create="createScene" @update="updateScene" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Scene } from 'phavuer'
|
||||
import type { WeatherState } from '@/application/types'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
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 sceneRef = ref<Phaser.Scene | null>(null)
|
||||
const zoneEffectsReady = ref(false)
|
||||
|
||||
// Effect-related refs
|
||||
const lightEffect = ref<Phaser.GameObjects.Graphics | null>(null)
|
||||
const rainEmitter = ref<Phaser.GameObjects.Particles.ParticleEmitter | null>(null)
|
||||
const fogSprite = ref<Phaser.GameObjects.Sprite | null>(null)
|
||||
// Effect objects
|
||||
const effects = {
|
||||
light: ref<Phaser.GameObjects.Graphics | 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('fog', 'assets/fog.png')
|
||||
}
|
||||
|
||||
const createScene = async (scene: Phaser.Scene) => {
|
||||
const createScene = (scene: Phaser.Scene) => {
|
||||
sceneRef.value = scene
|
||||
createLightEffect(scene)
|
||||
createRainEffect(scene)
|
||||
createFogEffect(scene)
|
||||
initializeEffects(scene)
|
||||
setupSocketListeners()
|
||||
}
|
||||
|
||||
const initializeEffects = (scene: Phaser.Scene) => {
|
||||
// Light
|
||||
effects.light.value = scene.add.graphics().setDepth(1000)
|
||||
|
||||
// Rain
|
||||
effects.rain.value = scene.add
|
||||
.particles(0, 0, 'raindrop', {
|
||||
x: { min: 0, max: window.innerWidth },
|
||||
y: -50,
|
||||
quantity: 5,
|
||||
lifespan: 2000,
|
||||
speedY: { min: 300, max: 500 },
|
||||
scale: { start: 0.005, end: 0.005 },
|
||||
alpha: { start: 0.5, end: 0 },
|
||||
blendMode: 'ADD'
|
||||
})
|
||||
.setDepth(900)
|
||||
effects.rain.value.stop()
|
||||
|
||||
// Fog
|
||||
effects.fog.value = scene.add
|
||||
.sprite(window.innerWidth / 2, window.innerHeight / 2, 'fog')
|
||||
.setScale(2)
|
||||
.setAlpha(0)
|
||||
.setDepth(950)
|
||||
}
|
||||
|
||||
// Effect updates
|
||||
const updateScene = () => {
|
||||
updateEffects()
|
||||
}
|
||||
const timeBasedLight = calculateLightStrength(gameStore.world.date)
|
||||
const zoneEffects = zoneStore.zone?.zoneEffects?.reduce(
|
||||
(acc, curr) => ({
|
||||
...acc,
|
||||
[curr.effect]: curr.strength
|
||||
}),
|
||||
{}
|
||||
) as { [key: string]: number }
|
||||
|
||||
const createLightEffect = (scene: Phaser.Scene) => {
|
||||
lightEffect.value = scene.add.graphics()
|
||||
lightEffect.value.setDepth(1000)
|
||||
}
|
||||
|
||||
const createRainEffect = (scene: Phaser.Scene) => {
|
||||
rainEmitter.value = scene.add.particles(0, 0, 'raindrop', {
|
||||
x: { min: 0, max: window.innerWidth },
|
||||
y: -50,
|
||||
quantity: 5,
|
||||
lifespan: 2000,
|
||||
speedY: { min: 300, max: 500 },
|
||||
scale: { start: 0.005, end: 0.005 },
|
||||
alpha: { start: 0.5, end: 0 },
|
||||
blendMode: 'ADD'
|
||||
})
|
||||
rainEmitter.value.setDepth(900)
|
||||
rainEmitter.value.stop()
|
||||
}
|
||||
|
||||
const createFogEffect = (scene: Phaser.Scene) => {
|
||||
fogSprite.value = scene.add.sprite(window.innerWidth / 2, window.innerHeight / 2, 'fog')
|
||||
fogSprite.value.setScale(2)
|
||||
fogSprite.value.setAlpha(0)
|
||||
fogSprite.value.setDepth(950)
|
||||
}
|
||||
|
||||
const updateEffects = () => {
|
||||
const effects = zoneStore.zone?.zoneEffects || []
|
||||
|
||||
effects.forEach((effect) => {
|
||||
switch (effect.effect) {
|
||||
case 'light':
|
||||
updateLightEffect(effect.strength)
|
||||
break
|
||||
case 'rain':
|
||||
updateRainEffect(effect.strength)
|
||||
break
|
||||
case 'fog':
|
||||
updateFogEffect(effect.strength)
|
||||
break
|
||||
// Only update effects once zoneEffects are loaded
|
||||
if (!zoneEffectsReady.value) {
|
||||
if (zoneEffects && Object.keys(zoneEffects).length) {
|
||||
zoneEffectsReady.value = true
|
||||
} else {
|
||||
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 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 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)
|
||||
}
|
||||
|
||||
const updateRainEffect = (strength: number) => {
|
||||
if (!rainEmitter.value) return
|
||||
if (strength > 0) {
|
||||
rainEmitter.value.start()
|
||||
rainEmitter.value.setQuantity(Math.floor((strength / 100) * 10))
|
||||
} else {
|
||||
rainEmitter.value.stop()
|
||||
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) => {
|
||||
if (!fogSprite.value) return
|
||||
fogSprite.value.setAlpha(strength / 100)
|
||||
const calculateLightStrength = (time: Date): number => {
|
||||
const hour = time.getHours()
|
||||
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(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
if (sceneRef.value) sceneRef.value.scene.remove('effects')
|
||||
gameStore.connection?.off('weather')
|
||||
})
|
||||
|
||||
// @TODO : Fix resize issue
|
||||
</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>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ZoneObject from '@/components/game/zone/partials/ZoneObject.vue'
|
||||
import { useZoneStore } from '@/stores/zoneStore'
|
||||
import ZoneObject from '@/components/zone/partials/ZoneObject.vue'
|
||||
|
||||
const zoneStore = useZoneStore()
|
||||
|
@ -1,29 +1,29 @@
|
||||
<template>
|
||||
<Controls :layer="tiles" :depth="0" />
|
||||
<Controls :layer="tileLayer" :depth="0" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import config from '@/config'
|
||||
import { useScene } from 'phavuer'
|
||||
import { useZoneStore } from '@/stores/zoneStore'
|
||||
import { onBeforeUnmount } from 'vue'
|
||||
import { FlattenZoneArray, setLayerTiles } from '@/composables/zoneComposable'
|
||||
import config from '@/application/config'
|
||||
import { unduplicateArray } from '@/application/utilities'
|
||||
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'])
|
||||
|
||||
const scene = useScene()
|
||||
const zoneStore = useZoneStore()
|
||||
const zoneTilemap = createTilemap()
|
||||
const tiles = createTileLayer()
|
||||
const tileMap = createTileMap()
|
||||
const tileLayer = createTileLayer()
|
||||
|
||||
/**
|
||||
* A Tilemap is a container for Tilemap data.
|
||||
* This isn't a display object, rather, it holds data about the map and allows you to add tilesets and tilemap layers to it.
|
||||
* A map can have one or more tilemap layers, which are the display objects that actually render the tiles.
|
||||
*/
|
||||
function createTilemap() {
|
||||
function createTileMap() {
|
||||
const zoneData = new Phaser.Tilemaps.MapData({
|
||||
width: zoneStore.zone?.width,
|
||||
height: zoneStore.zone?.height,
|
||||
@ -32,9 +32,11 @@ function createTilemap() {
|
||||
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
|
||||
format: Phaser.Tilemaps.Formats.ARRAY_2D
|
||||
})
|
||||
const tilemap = new Phaser.Tilemaps.Tilemap(scene, zoneData)
|
||||
emit('tilemap:create', tilemap)
|
||||
return tilemap
|
||||
|
||||
const newTileMap = new Phaser.Tilemaps.Tilemap(scene, zoneData)
|
||||
emit('tileMap:create', newTileMap)
|
||||
|
||||
return newTileMap
|
||||
}
|
||||
|
||||
/**
|
||||
@ -44,12 +46,12 @@ function createTileLayer() {
|
||||
const tilesArray = unduplicateArray(FlattenZoneArray(zoneStore.zone?.tiles ?? []))
|
||||
|
||||
const tilesetImages = Array.from(tilesArray).map((tile: any, index: number) => {
|
||||
return zoneTilemap.addTilesetImage(tile, tile, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y })
|
||||
return tileMap.addTilesetImage(tile, tile, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y })
|
||||
}) as any
|
||||
|
||||
// Add blank tile
|
||||
tilesetImages.push(zoneTilemap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 1, 2, 0, { x: 0, y: -config.tile_size.y }))
|
||||
const layer = zoneTilemap.createBlankLayer('tiles', tilesetImages, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
|
||||
tilesetImages.push(tileMap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 1, 2, 0, { x: 0, y: -config.tile_size.y }))
|
||||
const layer = tileMap.createBlankLayer('tiles', tilesetImages, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
|
||||
|
||||
layer.setDepth(0)
|
||||
layer.setCullPadding(2, 2)
|
||||
@ -57,11 +59,11 @@ function createTileLayer() {
|
||||
return layer
|
||||
}
|
||||
|
||||
setLayerTiles(zoneTilemap, tiles, zoneStore.zone?.tiles)
|
||||
setLayerTiles(tileMap, tileLayer, zoneStore.zone?.tiles)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
zoneTilemap.destroyLayer('tiles')
|
||||
zoneTilemap.removeAllLayers()
|
||||
zoneTilemap.destroy()
|
||||
tileMap.destroyLayer('tiles')
|
||||
tileMap.removeAllLayers()
|
||||
tileMap.destroy()
|
||||
})
|
||||
</script>
|
@ -3,12 +3,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { Image, useScene } from 'phavuer'
|
||||
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||
import type { AssetDataT, ZoneObject } from '@/application/types'
|
||||
import { loadTexture } from '@/composables/gameComposable'
|
||||
import type { AssetDataT, ZoneObject } from '@/types'
|
||||
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { Image, useScene } from 'phavuer'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
tilemap: Phaser.Tilemaps.Tilemap
|
@ -1,11 +1,12 @@
|
||||
<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>
|
||||
<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">Users</button>
|
||||
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Chats</button>
|
||||
<button @mousedown.stop class="btn-cyan active py-1.5 px-4 min-w-24">Asset manager</button>
|
||||
<button class="btn-cyan py-1.5 px-4 min-w-24" type="button" @click="() => zoneEditorStore.toggleActive()">Map editor</button>
|
||||
</div>
|
||||
</template>
|
||||
<template #modalBody>
|
||||
@ -17,12 +18,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import Modal from '@/components/utilities/Modal.vue'
|
||||
import AssetManager from '@/components/gameMaster/assetManager/AssetManager.vue'
|
||||
import Modal from '@/components/utilities/Modal.vue'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const zoneEditorStore = useZoneEditorStore()
|
||||
|
||||
let toggle = ref('asset-manager')
|
||||
</script>
|
||||
|
@ -1,44 +0,0 @@
|
||||
<template>
|
||||
<Modal :isModalOpen="true" :closable="false" :is-resizable="false" :modal-width="modalWidth" :modal-height="modalHeight" :modal-position-x="posXY.x" :modal-position-y="posXY.y">
|
||||
<template #modalHeader>
|
||||
<h3 class="m-0 font-medium shrink-0 text-white">GM tools</h3>
|
||||
</template>
|
||||
<template #modalBody>
|
||||
<div class="content flex flex-col gap-2.5 m-4 h-20">
|
||||
<button class="btn-cyan py-1.5 px-4 w-full" type="button" @click="gameStore.toggleGmPanel()">Toggle GM panel</button>
|
||||
<button class="btn-cyan py-1.5 px-4 w-full" type="button" @click="() => zoneEditorStore.toggleActive()">Zone manager</button>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import Modal from '@/components/utilities/Modal.vue'
|
||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const zoneEditorStore = useZoneEditorStore()
|
||||
const gameStore = useGameStore()
|
||||
const modalWidth = ref(200)
|
||||
const modalHeight = ref(170)
|
||||
|
||||
let posXY = ref({ x: 0, y: 0 })
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', () => {
|
||||
posXY.value = customPositionGmPanel(modalWidth.value)
|
||||
})
|
||||
})
|
||||
|
||||
const customPositionGmPanel = (modalWidth: number) => {
|
||||
const padding = 25
|
||||
const width = window.innerWidth
|
||||
|
||||
const x = width - (modalWidth + 4) - 25
|
||||
const y = padding
|
||||
|
||||
return { x, y }
|
||||
}
|
||||
|
||||
posXY.value = customPositionGmPanel(modalWidth.value)
|
||||
</script>
|
@ -1,85 +1,79 @@
|
||||
<template>
|
||||
<div class="flex h-full w-full relative">
|
||||
<div class="w-2/12 flex flex-col relative overflow-auto">
|
||||
<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 rounded-md default-border bg-gray p-2.5">
|
||||
<!-- Asset Categories -->
|
||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'tiles' }" @click="() => (selectedCategory = 'tiles')">
|
||||
<span :class="{ 'text-white': selectedCategory === 'tiles' }">Tiles</span>
|
||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'tiles' }" @click="() => (selectedCategory = 'tiles')">
|
||||
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'tiles' }">Tiles</span>
|
||||
</a>
|
||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'objects' }" @click="() => (selectedCategory = 'objects')">
|
||||
<span :class="{ 'text-white': selectedCategory === 'objects' }">Objects</span>
|
||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'objects' }" @click="() => (selectedCategory = 'objects')">
|
||||
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'objects' }">Objects</span>
|
||||
</a>
|
||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'sprites' }" @click="() => (selectedCategory = 'sprites')">
|
||||
<span :class="{ 'text-white': selectedCategory === 'sprites' }">Sprites</span>
|
||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'sprites' }" @click="() => (selectedCategory = 'sprites')">
|
||||
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'sprites' }">Sprites</span>
|
||||
</a>
|
||||
<a class="relative p-2.5 hover:cursor-pointer">
|
||||
<span>Items</span>
|
||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'items' }" @click="() => (selectedCategory = 'items')">
|
||||
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'items' }">Items</span>
|
||||
</a>
|
||||
<a class="relative p-2.5 hover:cursor-pointer">
|
||||
<span>NPC's</span>
|
||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group">
|
||||
<span class="group-hover:text-white">NPC's</span>
|
||||
</a>
|
||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'shops' }" @click="() => (selectedCategory = 'shops')">
|
||||
<span :class="{ 'text-white': selectedCategory === 'shops' }">Shops</span>
|
||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||
<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="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'shops' }">Shops</span>
|
||||
</a>
|
||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'characterTypes' }" @click="() => (selectedCategory = 'characterTypes')">
|
||||
<span :class="{ 'text-white': selectedCategory === 'characterTypes' }">Character types</span>
|
||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||
<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="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'characterTypes' }">Character types</span>
|
||||
</a>
|
||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': selectedCategory === 'characterHair' }" @click="() => (selectedCategory = 'characterHair')">
|
||||
<span :class="{ 'text-white': selectedCategory === 'characterHair' }">Character hair</span>
|
||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||
<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="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'characterHair' }">Character hair</span>
|
||||
</a>
|
||||
<a class="relative p-2.5 hover:cursor-pointer">
|
||||
<span>Mounts</span>
|
||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group">
|
||||
<span class="group-hover:text-white">Mounts</span>
|
||||
</a>
|
||||
<a class="relative p-2.5 hover:cursor-pointer">
|
||||
<span>Pets</span>
|
||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group">
|
||||
<span class="group-hover:text-white">Pets</span>
|
||||
</a>
|
||||
<a class="relative p-2.5 hover:cursor-pointer">
|
||||
<span>Emoticons</span>
|
||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group">
|
||||
<span class="group-hover:text-white">Emoticons</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="absolute w-px bg-gray-500 h-full top-0 left-1/6"></div>
|
||||
|
||||
<!-- Assets list -->
|
||||
<div class="overflow-auto h-full w-4/12 flex flex-col relative">
|
||||
<TileList v-if="selectedCategory === 'tiles'" />
|
||||
<ObjectList v-if="selectedCategory === 'objects'" />
|
||||
<SpriteList v-if="selectedCategory === 'sprites'" />
|
||||
<ItemList v-if="selectedCategory === 'items'" />
|
||||
<CharacterTypeList v-if="selectedCategory === 'characterTypes'" />
|
||||
<CharacterHairList v-if="selectedCategory === 'characterHair'" />
|
||||
</div>
|
||||
|
||||
<div class="absolute w-px bg-gray-500 h-full top-0 left-1/2"></div>
|
||||
|
||||
<!-- 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" />
|
||||
<ObjectDetails v-if="selectedCategory === 'objects' && assetManagerStore.selectedObject" />
|
||||
<SpriteDetails v-if="selectedCategory === 'sprites' && assetManagerStore.selectedSprite" />
|
||||
<ItemDetails v-if="selectedCategory === 'items' && assetManagerStore.selectedItem" />
|
||||
<CharacterTypeDetails v-if="selectedCategory === 'characterTypes' && assetManagerStore.selectedCharacterType" />
|
||||
<CharacterHairDetails v-if="selectedCategory === 'characterHair' && assetManagerStore.selectedCharacterHair" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
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 CharacterHairDetails from '@/components/gameMaster/assetManager/partials/characterHair/CharacterHairDetails.vue'
|
||||
import CharacterHairList from '@/components/gameMaster/assetManager/partials/characterHair/CharacterHairList.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 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>
|
||||
<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">
|
||||
<div class="form-field-full">
|
||||
<label for="name">Name</label>
|
||||
@ -19,8 +19,18 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="spriteId">Sprite ID</label>
|
||||
<input v-model="characterSpriteId" class="input-field" type="text" name="spriteId" placeholder="Sprite ID" />
|
||||
<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="removeCharacterType">Remove</button>
|
||||
@ -30,10 +40,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { CharacterType, CharacterGender, CharacterRace } from '@/types'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import type { CharacterGender, CharacterRace, CharacterType, 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()
|
||||
@ -43,7 +53,8 @@ const selectedCharacterType = computed(() => assetManagerStore.selectedCharacter
|
||||
const characterName = ref('')
|
||||
const characterGender = ref<CharacterGender>('MALE' as CharacterGender.MALE)
|
||||
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 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
|
||||
characterGender.value = selectedCharacterType.value.gender
|
||||
characterRace.value = selectedCharacterType.value.race
|
||||
characterIsSelectable.value = selectedCharacterType.value.isSelectable
|
||||
characterSpriteId.value = selectedCharacterType.value.spriteId
|
||||
}
|
||||
|
||||
@ -83,10 +95,11 @@ function refreshCharacterTypeList(unsetSelectedCharacterType = true) {
|
||||
|
||||
function saveCharacterType() {
|
||||
const characterTypeData = {
|
||||
id: selectedCharacterType.value?.id,
|
||||
id: selectedCharacterType.value!.id,
|
||||
name: characterName.value,
|
||||
gender: characterGender.value,
|
||||
race: characterRace.value,
|
||||
isSelectable: characterIsSelectable.value,
|
||||
spriteId: characterSpriteId.value
|
||||
}
|
||||
|
||||
@ -104,11 +117,16 @@ watch(selectedCharacterType, (characterType: CharacterType | null) => {
|
||||
characterName.value = characterType.name
|
||||
characterGender.value = characterType.gender
|
||||
characterRace.value = characterType.race
|
||||
characterIsSelectable.value = characterType.isSelectable
|
||||
characterSpriteId.value = characterType.spriteId
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (!selectedCharacterType.value) return
|
||||
|
||||
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
||||
assetManagerStore.setSpriteList(response)
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
|
@ -1,36 +1,42 @@
|
||||
<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" />
|
||||
<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">
|
||||
<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 class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></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">
|
||||
<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">
|
||||
<span>{{ characterType.name }}</span>
|
||||
<span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedCharacterType?.id === characterType.id }">{{ characterType.name }}</span>
|
||||
</div>
|
||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||
</a>
|
||||
</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">
|
||||
<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>
|
||||
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
||||
<button class="fixed min-w-[unset] w-12 h-12 rounded-md bg-cyan p-0 hover:bg-cyan-800" v-show="hasScrolled" @click="toTop">
|
||||
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/zoneEditor/chevron.svg" alt="" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { onMounted, ref, computed } from 'vue'
|
||||
import type { CharacterType } from '@/application/types'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import type { CharacterType } from '@/types'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useVirtualList } from '@vueuse/core'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
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>
|
||||
<div class="h-full overflow-auto">
|
||||
<div class="relative p-2.5 flex flex-col items-center justify-between h-72">
|
||||
<div class="filler"></div>
|
||||
<div class="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray">
|
||||
<img class="max-h-56" :src="`${config.server_endpoint}/assets/objects/${selectedObject?.id}.png`" :alt="'Object ' + selectedObject?.id" />
|
||||
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeObject">Remove</button>
|
||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||
</div>
|
||||
<div class="m-2.5 p-2.5 block">
|
||||
<div class="mt-5 block">
|
||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject">
|
||||
<div class="form-field-full">
|
||||
<label for="name">Name</label>
|
||||
@ -21,19 +18,19 @@
|
||||
<input v-model="objectOriginY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="origin-x">Tags</label>
|
||||
<label for="tags">Tags</label>
|
||||
<ChipsInput v-model="objectTags" @update:modelValue="objectTags = $event" />
|
||||
</div>
|
||||
<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">
|
||||
<option :value="false">No</option>
|
||||
<option :value="true">Yes</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<label for="frame-speed">Frame speed</label>
|
||||
<input v-model="objectFrameSpeed" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame speed" />
|
||||
<label for="frame-speed">Frame rate</label>
|
||||
<input v-model="objectFrameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
|
||||
</div>
|
||||
<div class="form-field-half">
|
||||
<label for="frame-width">Frame width</label>
|
||||
@ -43,20 +40,23 @@
|
||||
<label for="frame-height">Frame height</label>
|
||||
<input v-model="objectFrameHeight" class="input-field" type="number" step="any" name="frame-height" placeholder="Frame height" />
|
||||
</div>
|
||||
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
||||
<div class="flex gap-4">
|
||||
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
||||
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeObject">Delete</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Object } from '@/types'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import config from '@/config'
|
||||
import config from '@/application/config'
|
||||
import type { Object } from '@/application/types'
|
||||
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 assetManagerStore = useAssetManagerStore()
|
||||
@ -69,7 +69,7 @@ const objectTags = ref<string[]>([])
|
||||
const objectOriginX = ref(0)
|
||||
const objectOriginY = ref(0)
|
||||
const objectIsAnimated = ref(false)
|
||||
const objectFrameSpeed = ref(0)
|
||||
const objectFrameRate = ref(0)
|
||||
const objectFrameWidth = ref(0)
|
||||
const objectFrameHeight = ref(0)
|
||||
|
||||
@ -83,7 +83,7 @@ if (selectedObject.value) {
|
||||
objectOriginX.value = selectedObject.value.originX
|
||||
objectOriginY.value = selectedObject.value.originY
|
||||
objectIsAnimated.value = selectedObject.value.isAnimated
|
||||
objectFrameSpeed.value = selectedObject.value.frameSpeed
|
||||
objectFrameRate.value = selectedObject.value.frameRate
|
||||
objectFrameWidth.value = selectedObject.value.frameWidth
|
||||
objectFrameHeight.value = selectedObject.value.frameHeight
|
||||
}
|
||||
@ -127,7 +127,7 @@ function saveObject() {
|
||||
originX: objectOriginX.value,
|
||||
originY: objectOriginY.value,
|
||||
isAnimated: objectIsAnimated.value,
|
||||
frameSpeed: objectFrameSpeed.value,
|
||||
frameRate: objectFrameRate.value,
|
||||
frameWidth: objectFrameWidth.value,
|
||||
frameHeight: objectFrameHeight.value
|
||||
},
|
||||
@ -148,7 +148,7 @@ watch(selectedObject, (object: Object | null) => {
|
||||
objectOriginX.value = object.originX
|
||||
objectOriginY.value = object.originY
|
||||
objectIsAnimated.value = object.isAnimated
|
||||
objectFrameSpeed.value = object.frameSpeed
|
||||
objectFrameRate.value = object.frameRate
|
||||
objectFrameWidth.value = object.frameWidth
|
||||
objectFrameHeight.value = object.frameHeight
|
||||
})
|
||||
|
@ -1,39 +1,39 @@
|
||||
<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" />
|
||||
<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" />
|
||||
<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" />
|
||||
</svg>
|
||||
</label>
|
||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></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">
|
||||
<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="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" />
|
||||
</div>
|
||||
<span>{{ object.name }}</span>
|
||||
<span :class="{ 'text-white': assetManagerStore.selectedObject?.id === object.id }">{{ object.name }}</span>
|
||||
</div>
|
||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||
</a>
|
||||
</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">
|
||||
<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>
|
||||
<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 config from '@/config'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { onMounted, ref, computed } from 'vue'
|
||||
import config from '@/application/config'
|
||||
import type { Object } from '@/application/types'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import type { Object } from '@/types'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useVirtualList } from '@vueuse/core'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const objectUploadField = ref(null)
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="h-full overflow-auto">
|
||||
<div class="relative p-4 flex flex-col">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div class="relative flex flex-col">
|
||||
<div class="flex flex-wrap gap-2 p-2.5 rounded-md default-border bg-gray">
|
||||
<div class="w-full flex flex-col">
|
||||
<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" />
|
||||
@ -10,7 +10,11 @@
|
||||
<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-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>
|
||||
|
||||
@ -51,8 +55,8 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field-full" v-if="action.isAnimated">
|
||||
<label for="frame-speed">Frame speed</label>
|
||||
<input v-model.number="action.frameSpeed" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame speed" />
|
||||
<label for="frame-speed">Frame rate</label>
|
||||
<input v-model.number="action.frameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
|
||||
</div>
|
||||
<div class="form-field-full">
|
||||
<SpriteActionsInput v-model="action.sprites" />
|
||||
@ -65,13 +69,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Sprite, SpriteAction } from '@/types'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import type { Sprite, SpriteAction } from '@/application/types'
|
||||
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 { useGameStore } from '@/stores/gameStore'
|
||||
import Accordion from '@/components/utilities/Accordion.vue'
|
||||
import SpriteActionsInput from '@/components/gameMaster/assetManager/partials/sprite/partials/SpriteImagesInput.vue'
|
||||
import { uuidv4 } from '@/utilities'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const assetManagerStore = useAssetManagerStore()
|
||||
@ -87,7 +91,7 @@ if (!selectedSprite.value) {
|
||||
|
||||
if (selectedSprite.value) {
|
||||
spriteName.value = selectedSprite.value.name
|
||||
spriteActions.value = selectedSprite.value.spriteActions
|
||||
spriteActions.value = sortSpriteActions(selectedSprite.value.spriteActions)
|
||||
}
|
||||
|
||||
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) {
|
||||
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
||||
assetManagerStore.setSpriteList(response)
|
||||
@ -128,7 +142,7 @@ function saveSprite() {
|
||||
originY: action.originY,
|
||||
isAnimated: action.isAnimated,
|
||||
isLooping: action.isLooping,
|
||||
frameSpeed: action.frameSpeed,
|
||||
frameRate: action.frameRate,
|
||||
frameWidth: action.frameWidth,
|
||||
frameHeight: action.frameHeight
|
||||
}
|
||||
@ -148,7 +162,7 @@ function addNewImage() {
|
||||
if (!selectedSprite.value) return
|
||||
|
||||
const newImage: SpriteAction = {
|
||||
id: uuidv4(), // Temporary ID, should be replaced by server-generated ID
|
||||
id: uuidv4(),
|
||||
spriteId: selectedSprite.value.id,
|
||||
sprite: selectedSprite.value,
|
||||
action: 'new_action',
|
||||
@ -157,7 +171,7 @@ function addNewImage() {
|
||||
originY: 0,
|
||||
isAnimated: false,
|
||||
isLooping: false,
|
||||
frameSpeed: 0,
|
||||
frameRate: 0,
|
||||
frameWidth: 0,
|
||||
frameHeight: 0
|
||||
}
|
||||
@ -166,13 +180,17 @@ function addNewImage() {
|
||||
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) => {
|
||||
if (!sprite) return
|
||||
spriteName.value = sprite.name
|
||||
spriteActions.value = sprite.spriteActions
|
||||
spriteActions.value = sortSpriteActions(sprite.spriteActions)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
|
@ -1,35 +1,35 @@
|
||||
<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" />
|
||||
<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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></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">
|
||||
<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">
|
||||
<span>{{ sprite.name }}</span>
|
||||
<span :class="{ 'text-white': assetManagerStore.selectedSprite?.id === sprite.id }">{{ sprite.name }}</span>
|
||||
</div>
|
||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||
</a>
|
||||
</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">
|
||||
<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>
|
||||
<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 config from '@/config'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { onMounted, ref, computed } from 'vue'
|
||||
import config from '@/application/config'
|
||||
import type { Sprite } from '@/application/types'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useVirtualList } from '@vueuse/core'
|
||||
import type { Sprite } from '@/types'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const assetManagerStore = useAssetManagerStore()
|
||||
|
@ -1,14 +1,21 @@
|
||||
<template>
|
||||
<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" />
|
||||
<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">
|
||||
<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" />
|
||||
</svg>
|
||||
</button>
|
||||
<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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="bg-blue-500 text-white rounded-full w-6 h-6 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" aria-label="Scope image">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-20 w-20 p-4 bg-gray-100 bg-opacity-50 rounded justify-center items-center flex hover:cursor-pointer" @click="triggerFileInput" @drop.prevent="onDrop" @dragover.prevent>
|
||||
<div class="h-20 w-20 p-4 bg-gray-200 bg-opacity-50 rounded justify-center items-center flex hover:cursor-pointer" @click="triggerFileInput" @drop.prevent="onDrop" @dragover.prevent>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 invert" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
|
@ -1,12 +1,9 @@
|
||||
<template>
|
||||
<div class="h-full overflow-auto">
|
||||
<div class="relative p-2.5 flex flex-col items-center justify-between h-72">
|
||||
<div class="filler"></div>
|
||||
<div class="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray">
|
||||
<img class="max-h-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 class="m-2.5 p-2.5 block">
|
||||
<div class="mt-5 block">
|
||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveTile">
|
||||
<div class="form-field-full">
|
||||
<label for="name">Name</label>
|
||||
@ -16,20 +13,23 @@
|
||||
<label for="origin-x">Tags</label>
|
||||
<ChipsInput v-model="tileTags" @update:modelValue="tileTags = $event" />
|
||||
</div>
|
||||
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
||||
<div class="flex gap-4">
|
||||
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
||||
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="deleteTile">Delete</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Tile } from '@/types'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import config from '@/config'
|
||||
import config from '@/application/config'
|
||||
import type { Tile } from '@/application/types'
|
||||
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 assetManagerStore = useAssetManagerStore()
|
||||
|
@ -1,39 +1,39 @@
|
||||
<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" />
|
||||
<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" />
|
||||
<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" />
|
||||
</svg>
|
||||
</label>
|
||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></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">
|
||||
<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="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" />
|
||||
</div>
|
||||
<span>{{ tile.name }}</span>
|
||||
<span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedTile?.id === tile.id }">{{ tile.name }}</span>
|
||||
</div>
|
||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||
</a>
|
||||
</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">
|
||||
<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>
|
||||
<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 config from '@/config'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { onMounted, ref, computed } from 'vue'
|
||||
import config from '@/application/config'
|
||||
import type { Tile } from '@/application/types'
|
||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||
import type { Tile } from '@/types'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useVirtualList } from '@vueuse/core'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const tileUploadField = ref(null)
|
||||
|
@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<ZoneTiles @tilemap:create="tileMap = $event" />
|
||||
<ZoneTiles @tileMap:create="tileMap = $event" />
|
||||
<ZoneObjects v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
||||
<ZoneEventTiles v-if="tileMap" :tilemap="tileMap as Phaser.Tilemaps.Tilemap" />
|
||||
|
||||
<Toolbar @save="save" />
|
||||
<Toolbar @save="save" @clear="clear" />
|
||||
|
||||
<ZoneList />
|
||||
<TileList />
|
||||
@ -14,26 +14,35 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onUnmounted, ref } from 'vue'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
import { type Zone } from '@/types'
|
||||
|
||||
import { type Zone } from '@/application/types'
|
||||
import ObjectList from '@/components/gameMaster/zoneEditor/partials/ObjectList.vue'
|
||||
import TeleportModal from '@/components/gameMaster/zoneEditor/partials/TeleportModal.vue'
|
||||
import TileList from '@/components/gameMaster/zoneEditor/partials/TileList.vue'
|
||||
// Components
|
||||
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 TeleportModal from '@/components/gameMaster/zoneEditor/partials/TeleportModal.vue'
|
||||
import ZoneTiles from '@/components/gameMaster/zoneEditor/ZoneTiles.vue'
|
||||
import ZoneObjects from '@/components/gameMaster/zoneEditor/ZoneObjects.vue'
|
||||
import ZoneEventTiles from '@/components/gameMaster/zoneEditor/ZoneEventTiles.vue'
|
||||
import ZoneSettings from '@/components/gameMaster/zoneEditor/partials/ZoneSettings.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 zoneEditorStore = useZoneEditorStore()
|
||||
|
||||
const tileMap = ref(null as Phaser.Tilemaps.Tilemap | null)
|
||||
|
||||
function clear() {
|
||||
if (!zoneEditorStore.zone) return
|
||||
|
||||
// Clear objects, event tiles and tiles
|
||||
zoneEditorStore.zone.zoneObjects = []
|
||||
zoneEditorStore.zone.zoneEventTiles = []
|
||||
zoneEditorStore.triggerClearTiles()
|
||||
}
|
||||
|
||||
function save() {
|
||||
if (!zoneEditorStore.zone) return
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
<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>
|
||||
<h3 class="m-0 font-medium shrink-0 text-white">Create new zone</h3>
|
||||
</template>
|
||||
@ -36,11 +36,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import type { Zone } from '@/application/types'
|
||||
import Modal from '@/components/utilities/Modal.vue'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
import type { Zone } from '@/types'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const zoneEditorStore = useZoneEditorStore()
|
||||
|
@ -1,5 +1,5 @@
|
||||
<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>
|
||||
<h3 class="text-lg text-white">Objects</h3>
|
||||
</template>
|
||||
@ -41,12 +41,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import config from '@/config'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import config from '@/application/config'
|
||||
import type { Object, ZoneObject } from '@/application/types'
|
||||
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 isModalOpen = ref(false)
|
||||
|
@ -11,7 +11,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ZoneObject } from '@/types'
|
||||
import type { ZoneObject } from '@/application/types'
|
||||
|
||||
const props = defineProps<{
|
||||
zoneObject: ZoneObject
|
||||
|
@ -1,5 +1,5 @@
|
||||
<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>
|
||||
<h3 class="m-0 font-medium shrink-0 text-white">Teleport settings</h3>
|
||||
</template>
|
||||
@ -39,11 +39,11 @@
|
||||
</template>
|
||||
|
||||
<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 { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
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 zoneEditorStore = useZoneEditorStore()
|
||||
|
@ -1,5 +1,5 @@
|
||||
<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>
|
||||
<h3 class="text-lg text-white">Tiles</h3>
|
||||
</template>
|
||||
@ -81,12 +81,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import config from '@/config'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import config from '@/application/config'
|
||||
import type { Tile } from '@/application/types'
|
||||
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 isModalOpen = ref(false)
|
||||
@ -223,7 +223,7 @@ function selectTile(tile: string) {
|
||||
}
|
||||
|
||||
function isActiveTile(tile: Tile): boolean {
|
||||
return zoneEditorStore.selectedTile?.id === tile.id
|
||||
return zoneEditorStore.selectedTile === tile.id
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
|
@ -82,9 +82,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
import { onClickOutside } from '@vueuse/core'
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
const zoneEditorStore = useZoneEditorStore()
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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>
|
||||
<h3 class="text-lg text-white">Zones</h3>
|
||||
</template>
|
||||
@ -15,7 +15,7 @@
|
||||
<div class="flex gap-3 items-center w-full" @click="() => loadZone(zone.id)">
|
||||
<span>{{ zone.name }}</span>
|
||||
<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>
|
||||
</div>
|
||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
||||
@ -26,12 +26,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import Modal from '@/components/utilities/Modal.vue'
|
||||
import type { Zone } from '@/types'
|
||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
import type { Zone } from '@/application/types'
|
||||
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 zoneEditorStore = useZoneEditorStore()
|
||||
|
@ -1,5 +1,5 @@
|
||||
<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>
|
||||
<h3 class="m-0 font-medium shrink-0 text-white">Zone settings</h3>
|
||||
</template>
|
||||
@ -47,9 +47,9 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import Modal from '@/components/utilities/Modal.vue'
|
||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
const zoneEditorStore = useZoneEditorStore()
|
||||
const screen = ref('settings')
|
||||
|
@ -3,11 +3,11 @@
|
||||
</template>
|
||||
|
||||
<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 { Image, useScene } from 'phavuer'
|
||||
import { getTile, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||
import { uuidv4 } from '@/utilities'
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const scene = useScene()
|
@ -3,12 +3,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { Image, useScene } from 'phavuer'
|
||||
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||
import type { AssetDataT, ZoneObject } from '@/application/types'
|
||||
import { loadTexture } from '@/composables/gameComposable'
|
||||
import type { AssetDataT, ZoneObject } from '@/types'
|
||||
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { Image, useScene } from 'phavuer'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
tilemap: Phaser.Tilemaps.Tilemap
|
||||
|
@ -4,14 +4,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { uuidv4 } from '@/utilities'
|
||||
import { getTile } from '@/composables/zoneComposable'
|
||||
import { useScene } from 'phavuer'
|
||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
import type { ZoneObject as ZoneObjectT } from '@/application/types'
|
||||
import { uuidv4 } from '@/application/utilities'
|
||||
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 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 zoneEditorStore = useZoneEditorStore()
|
||||
@ -56,7 +56,7 @@ function pencil(pointer: Phaser.Input.Pointer) {
|
||||
id: uuidv4(),
|
||||
zoneId: zoneEditorStore.zone.id,
|
||||
zone: zoneEditorStore.zone,
|
||||
objectId: zoneEditorStore.selectedObject,
|
||||
objectId: zoneEditorStore.selectedObject.id,
|
||||
object: zoneEditorStore.selectedObject,
|
||||
depth: 0,
|
||||
isRotated: false,
|
||||
@ -65,7 +65,7 @@ function pencil(pointer: Phaser.Input.Pointer) {
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@ -134,7 +134,7 @@ function moveZoneObject(id: string) {
|
||||
// Check if zone is set
|
||||
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) {
|
||||
if (!movingZoneObject.value) return
|