Compare commits
339 Commits
feature/#2
...
main
Author | SHA1 | Date | |
---|---|---|---|
af5e2449b5 | |||
5392093d71 | |||
27c775821a | |||
0d5acd48ce | |||
fc34a488d9 | |||
0b1e95f80f | |||
9f176aae45 | |||
4903a83c71 | |||
ba3ed8c099 | |||
2881d5f251 | |||
32ca61cc50 | |||
6897ad0f1e | |||
db1766026e | |||
3f28d85c30 | |||
a85ad94f15 | |||
e6c684e066 | |||
142d991265 | |||
9e5dcc31fa | |||
b5c5837105 | |||
57b503142e | |||
208b58d05f | |||
87c04b6de5 | |||
84f8db5e10 | |||
84b34c4f85 | |||
2495e14ece | |||
febb924f75 | |||
dc2afba82b | |||
ed0a02a795 | |||
3670eb8736 | |||
d85bf4846b | |||
51e885cfdf | |||
ffc7efb17c | |||
a0da0266d3 | |||
2281c2c5e0 | |||
0e3a0e3dba | |||
ed992e1c2d | |||
65b011982a | |||
489c6c3ba0 | |||
db650449ac | |||
2d7d598c94 | |||
7097eb1580 | |||
d51fbc8030 | |||
b5b6d0adcc | |||
4b7b6e4885 | |||
4042808d4e | |||
a6d6d894a9 | |||
0c61fe77de | |||
bfb2bcb939 | |||
af5a97f66d | |||
79fa54b1bb | |||
dbb4cae154 | |||
9a8220e4e0 | |||
bc0db8b32b | |||
ad611ef593 | |||
d819a84a37 | |||
15dc331a43 | |||
920baaebde | |||
b569888682 | |||
94eab073e6 | |||
d843b954ab | |||
337446497b | |||
d8805dd775 | |||
4c040c21d6 | |||
d0af83ec60 | |||
2de34d2034 | |||
132121c082 | |||
201f628bfa | |||
af99d66595 | |||
56f30093f6 | |||
8f26a40a0e | |||
110fd4e608 | |||
c1edf31ca0 | |||
90c0ed3141 | |||
bcf0d2832d | |||
8bf67ab168 | |||
f83e2bf8c8 | |||
8b0bf6534e | |||
5e243e5201 | |||
c82db9813e | |||
579749f4e0 | |||
ddc26a021b | |||
2d6b1ff1e0 | |||
16720777c9 | |||
41e7832cbe | |||
e6412d8a65 | |||
faa8e5def9 | |||
beed1d6903 | |||
2ebcc24390 | |||
2e3ff803f6 | |||
dd1cc795de | |||
59243e0e17 | |||
87ffc98cce | |||
0c450b24ed | |||
9459639497 | |||
5f2c7a09b1 | |||
13e8c1b4dd | |||
b27a2e8779 | |||
b3c9e3ca3d | |||
31a91c3f9f | |||
5d4de60f90 | |||
4070bcf048 | |||
04203cb9c1 | |||
592d1df9bf | |||
9413fdbb2f | |||
34caac562c | |||
52dafb8643 | |||
390187f353 | |||
cbd111a05b | |||
5ef11f3157 | |||
c56c2796c4 | |||
c228af7bb6 | |||
f45a51c230 | |||
790a62c600 | |||
82a854e647 | |||
3bcb16fa9c | |||
f79ebedc62 | |||
44b0368276 | |||
b8b985470f | |||
39e00c6feb | |||
6de0bb200d | |||
2a00e206eb | |||
8f9b19ba8b | |||
d997a33b86 | |||
9749b02ccf | |||
f83d5eabee | |||
a9cedba4e0 | |||
49dcd92a9e | |||
d010159989 | |||
275dd95c69 | |||
e3c3d4d420 | |||
87e7f14469 | |||
723aa59142 | |||
c369719564 | |||
2d8c421ac6 | |||
1137c95ff3 | |||
4b56da0fa0 | |||
c21e78c2ec | |||
fcf96a25ae | |||
cf9deebc94 | |||
ca307d4de3 | |||
4c4e8ffe02 | |||
369522fda3 | |||
dc7e20842a | |||
75c9d5f349 | |||
b35794d6d3 | |||
6ba4c1b843 | |||
6a52546a08 | |||
8133bd02df | |||
e720a1098e | |||
48d1d920be | |||
7542fd70ed | |||
9f866fea72 | |||
ec6f3031b8 | |||
838610d041 | |||
fb3a59aa59 | |||
ccb64fc048 | |||
db52bcfff3 | |||
12735756d7 | |||
6383320e8c | |||
557b8aaabb | |||
c09e9ea841 | |||
c2d41a63a7 | |||
122a178feb | |||
909dbf4280 | |||
8add054f63 | |||
04d55f994e | |||
b83c340385 | |||
d5984f1c3f | |||
7071d934b4 | |||
15b212160d | |||
2a2841cf16 | |||
a545018639 | |||
90f3056e08 | |||
7730fd81bd | |||
b195f1399f | |||
3c06f7db97 | |||
6c7864b4d4 | |||
0c9a41c286 | |||
dffdd0542f | |||
d2abf8fda8 | |||
fdbc101f96 | |||
7ff1de4018 | |||
f258c65403 | |||
bab13646ed | |||
adc3eba237 | |||
2097a51f07 | |||
50daf01a01 | |||
14474f7665 | |||
f14d9baaa1 | |||
d2b6d8dcb3 | |||
027fdd7dac | |||
2b40741ca7 | |||
aee18956f3 | |||
cf54ab842a | |||
d25100c810 | |||
cd1daf9345 | |||
0ecd951710 | |||
ff9dcb91b0 | |||
841ec0f3df | |||
90d7252784 | |||
554497ecbc | |||
efeae337ab | |||
ad47b37279 | |||
5e11b67774 | |||
7daefb74eb | |||
4adcf8d61d | |||
e53e154d16 | |||
d65ceba66a | |||
db426bb03e | |||
af26ca5e89 | |||
e4b9bb4d61 | |||
d7f60d7bfc | |||
cfdfa98379 | |||
63889a537a | |||
99bb1555a0 | |||
ac1396304f | |||
09ee9bf01d | |||
09b458eeef | |||
9d95562679 | |||
a9de031673 | |||
8e81ce716b | |||
2c1db56cc4 | |||
4fba3678d6 | |||
d29ca10ba9 | |||
67f83c3447 | |||
8f82bad3fa | |||
d665ac989c | |||
e389534e30 | |||
7d3946e274 | |||
0f46e3b6d2 | |||
6ca82733eb | |||
eb61f45535 | |||
a181fc7fe3 | |||
507d4226ac | |||
5dd9d1e7af | |||
15f9e9861e | |||
7fd334d414 | |||
c7d4b5f2c3 | |||
5747166822 | |||
c010373e5b | |||
57ad9d4889 | |||
f268ac9e5b | |||
fb6e2aa742 | |||
8befce7ffb | |||
e530f69311 | |||
144a513cb6 | |||
2a6321b06b | |||
ba90982e35 | |||
014c08b17a | |||
bdbda6456c | |||
85537840ab | |||
2b7082ac92 | |||
abc58bfa38 | |||
027325f2bf | |||
517e92b07b | |||
6bede8c44e | |||
9e652868ca | |||
35f0dcca64 | |||
9618e07bc6 | |||
791830fd6f | |||
37acf1782b | |||
14aa696197 | |||
cfac1d508b | |||
82cfe5902f | |||
284ca6f64e | |||
967cb1893d | |||
18db005bc1 | |||
c8473fc206 | |||
b5e84c133a | |||
3f75e4acd8 | |||
765d0986bf | |||
95c3a1af61 | |||
45a9d8cfdb | |||
e0a48a089a | |||
69f9944dc7 | |||
9cdfcbcc56 | |||
a614ee6241 | |||
7a51323682 | |||
807bc2066e | |||
fab0b08425 | |||
b074270c75 | |||
30845b80e9 | |||
bde0f74f19 | |||
bc685c63ef | |||
7a922261e3 | |||
3936676f2c | |||
9744083dea | |||
176f31d84a | |||
faad00b2a5 | |||
a61e05592d | |||
5202251ac7 | |||
5e2781b265 | |||
ebd6d96e54 | |||
41005735f9 | |||
78f1c6e6a0 | |||
2d48f83802 | |||
7dccb73698 | |||
7171112881 | |||
9a601b7e2e | |||
5c68b02fff | |||
b86d9dd4ce | |||
93baa10acf | |||
419cf319be | |||
1cd7f28402 | |||
0657dbcb1b | |||
5cf7423a5c | |||
4d88917526 | |||
8f07cf5093 | |||
367d536c52 | |||
3f8c911e9d | |||
689e443b3d | |||
4fead371d7 | |||
b9bcfc719f | |||
9de7af961e | |||
4067ec2585 | |||
fb18841c91 | |||
7b1dcf7ce3 | |||
7546116878 | |||
03fef60621 | |||
574777da80 | |||
2b84bfcad2 | |||
f829cfb883 | |||
c2db9b5469 | |||
6e30a8530a | |||
41f82897a8 | |||
37b97b0aac | |||
c1d9cc3a11 | |||
b54b825422 | |||
0142850983 | |||
2d09715dc4 | |||
ef807982a5 | |||
ae0841889b | |||
bdd2f93175 | |||
10f6dc3802 | |||
700bd57e67 | |||
145143cdc5 | |||
201853a3ec | |||
40c87f0ee3 | |||
736ddddc54 |
@ -1,5 +1,6 @@
|
|||||||
VITE_NAME=Noxious
|
VITE_NAME=Noxious
|
||||||
VITE_DEVELOPMENT=true
|
VITE_DOMAIN=localhost
|
||||||
|
VITE_ENVIRONMENT=development
|
||||||
VITE_SERVER_ENDPOINT=http://localhost:4000
|
VITE_SERVER_ENDPOINT=http://localhost:4000
|
||||||
VITE_TILE_SIZE_X=64
|
VITE_TILE_SIZE_WIDTH=64
|
||||||
VITE_TILE_SIZE_Y=32
|
VITE_TILE_SIZE_HEIGHT=32
|
@ -1,13 +0,0 @@
|
|||||||
/* eslint-env node */
|
|
||||||
require('@rushstack/eslint-patch/modern-module-resolution')
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
extends: ['plugin:vue/vue3-essential', 'eslint:recommended', '@vue/eslint-config-typescript', '@vue/eslint-config-prettier/skip-formatting'],
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 'latest'
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'vue/multi-word-component-names': 'off'
|
|
||||||
}
|
|
||||||
}
|
|
1
.vscode/extensions.json
vendored
@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"Vue.volar",
|
"Vue.volar",
|
||||||
"dbaeumer.vscode-eslint",
|
|
||||||
"esbenp.prettier-vscode"
|
"esbenp.prettier-vscode"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
70
Caddyfile
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
# Global options
|
||||||
|
admin off # Disable admin API
|
||||||
|
|
||||||
|
# Global logging configuration
|
||||||
|
log {
|
||||||
|
output file /var/log/caddy/access.log
|
||||||
|
format json
|
||||||
|
level INFO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
noxious.gg {
|
||||||
|
# Root directory for your Vue app
|
||||||
|
root * ./dist
|
||||||
|
|
||||||
|
# Enable compression with optimal settings
|
||||||
|
encode zstd gzip
|
||||||
|
|
||||||
|
# Handle SPA routing
|
||||||
|
try_files {path} /index.html
|
||||||
|
|
||||||
|
# Serve static files with optimizations
|
||||||
|
file_server
|
||||||
|
|
||||||
|
# Enhanced security headers
|
||||||
|
header {
|
||||||
|
# Existing headers with improvements
|
||||||
|
X-Frame-Options "SAMEORIGIN"
|
||||||
|
X-XSS-Protection "1; mode=block"
|
||||||
|
X-Content-Type-Options "nosniff"
|
||||||
|
Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
|
||||||
|
# Additional security headers
|
||||||
|
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||||
|
Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"
|
||||||
|
|
||||||
|
# Remove server information
|
||||||
|
-Server
|
||||||
|
}
|
||||||
|
|
||||||
|
# Improved cache configuration for static assets
|
||||||
|
@static {
|
||||||
|
file
|
||||||
|
path *.js *.css *.png *.jpg *.jpeg *.gif *.ico *.svg *.woff *.woff2 *.ttf *.eot
|
||||||
|
}
|
||||||
|
header @static {
|
||||||
|
Cache-Control "public, max-age=31536000, immutable"
|
||||||
|
Vary Accept-Encoding
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cache control for HTML files
|
||||||
|
@html {
|
||||||
|
file
|
||||||
|
path *.html
|
||||||
|
}
|
||||||
|
header @html {
|
||||||
|
Cache-Control "no-cache, must-revalidate"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Handle errors
|
||||||
|
handle_errors {
|
||||||
|
respond "{http.error.status_code} {http.error.status_text}" {http.error.status_code}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Improved redirect configuration
|
||||||
|
www.noxious.gg {
|
||||||
|
redir https://noxious.gg{uri} permanent
|
||||||
|
}
|
32
Dockerfile
@ -1,32 +0,0 @@
|
|||||||
# Build stage
|
|
||||||
FROM node:22.4.1-alpine as builder
|
|
||||||
WORKDIR /usr/src/app
|
|
||||||
COPY package*.json ./
|
|
||||||
RUN npm ci
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Set environment variables
|
|
||||||
ARG VITE_NAME=${VITE_NAME}
|
|
||||||
ENV VITE_NAME=${VITE_NAME}
|
|
||||||
|
|
||||||
ARG VITE_DEVELOPMENT=${VITE_DEVELOPMENT}
|
|
||||||
ENV VITE_DEVELOPMENT=${VITE_DEVELOPMENT}
|
|
||||||
|
|
||||||
ARG VITE_SERVER_ENDPOINT=${VITE_SERVER_ENDPOINT}
|
|
||||||
ENV VITE_SERVER_ENDPOINT=${VITE_SERVER_ENDPOINT}
|
|
||||||
|
|
||||||
ARG VITE_TILE_SIZE_X=${VITE_TILE_SIZE_X}
|
|
||||||
ENV VITE_TILE_SIZE_X=${VITE_TILE_SIZE_X}
|
|
||||||
|
|
||||||
ARG VITE_TILE_SIZE_Y=${VITE_TILE_SIZE_Y}
|
|
||||||
ENV VITE_TILE_SIZE_Y=${VITE_TILE_SIZE_Y}
|
|
||||||
|
|
||||||
# Build the application
|
|
||||||
RUN npm run build-ntc
|
|
||||||
|
|
||||||
# Production stage
|
|
||||||
FROM nginx:1.26.1-alpine
|
|
||||||
COPY --from=builder /usr/src/app/dist /usr/share/nginx/html
|
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
|
||||||
EXPOSE 80
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
|
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"schemaVersion": 2,
|
|
||||||
"dockerfilePath" :"./Dockerfile"
|
|
||||||
}
|
|
16
nginx.conf
@ -1,16 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name localhost;
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
# Redirect example
|
|
||||||
location /discord {
|
|
||||||
return 301 https://discord.gg/JTev3nzeDa;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Serve static files
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
}
|
|
||||||
}
|
|
4748
package-lock.json
generated
28
package.json
@ -11,39 +11,36 @@
|
|||||||
"test:unit": "vitest",
|
"test:unit": "vitest",
|
||||||
"build-only": "vite build",
|
"build-only": "vite build",
|
||||||
"type-check": "vue-tsc --build --force",
|
"type-check": "vue-tsc --build --force",
|
||||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
|
||||||
"format": "prettier --write src/"
|
"format": "prettier --write src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vueuse/core": "^10.5.0",
|
"@vueuse/core": "^10.5.0",
|
||||||
"@vueuse/integrations": "^10.5.0",
|
"@vueuse/integrations": "^10.5.0",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.9",
|
||||||
"dexie": "^4.0.8",
|
"dexie": "^4.0.11",
|
||||||
"phaser": "^3.86.0",
|
"phaser": "^3.88.2",
|
||||||
"pinia": "^2.1.6",
|
"phaser3-rex-plugins": "^1.80.13",
|
||||||
"socket.io-client": "^4.8.0",
|
"phavuer": "^0.16.5",
|
||||||
|
"pinia": "^2.3.1",
|
||||||
|
"sharp": "^0.33.5",
|
||||||
|
"socket.io-client": "^4.8.1",
|
||||||
"universal-cookie": "^6.1.3",
|
"universal-cookie": "^6.1.3",
|
||||||
"vue": "^3.5.12",
|
"vite-plugin-image-optimizer": "^1.1.8",
|
||||||
"zod": "^3.22.2"
|
"vue": "^3.5.13",
|
||||||
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ianvs/prettier-plugin-sort-imports": "^4.4.0",
|
"@ianvs/prettier-plugin-sort-imports": "^4.4.0",
|
||||||
"@rushstack/eslint-patch": "^1.10.3",
|
"@tauri-apps/cli": "^2.2.7",
|
||||||
"@tsconfig/node20": "^20.1.4",
|
"@tsconfig/node20": "^20.1.4",
|
||||||
"@types/jsdom": "^21.1.7",
|
"@types/jsdom": "^21.1.7",
|
||||||
"@types/node": "^20.14.11",
|
"@types/node": "^20.14.11",
|
||||||
"@vitejs/plugin-vue": "^5.0.5",
|
"@vitejs/plugin-vue": "^5.0.5",
|
||||||
"@vue/eslint-config-prettier": "^9.0.0",
|
|
||||||
"@vue/eslint-config-typescript": "^13.0.0",
|
|
||||||
"@vue/test-utils": "^2.4.6",
|
"@vue/test-utils": "^2.4.6",
|
||||||
"@vue/tsconfig": "^0.5.1",
|
"@vue/tsconfig": "^0.5.1",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"eslint": "^8.57.0",
|
|
||||||
"eslint-plugin-vue": "^9.27.0",
|
|
||||||
"jsdom": "^24.1.1",
|
"jsdom": "^24.1.1",
|
||||||
"npm-run-all2": "^6.2.3",
|
"npm-run-all2": "^6.2.3",
|
||||||
"phaser3-rex-plugins": "^1.80.8",
|
|
||||||
"phavuer": "^0.16.1",
|
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"sass": "^1.79.4",
|
"sass": "^1.79.4",
|
||||||
@ -51,7 +48,6 @@
|
|||||||
"typescript": "~5.6.2",
|
"typescript": "~5.6.2",
|
||||||
"vite": "^5.4.9",
|
"vite": "^5.4.9",
|
||||||
"vite-plugin-compression": "^0.5.1",
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vite-plugin-vue-devtools": "^7.5.2",
|
|
||||||
"vitest": "^2.0.3",
|
"vitest": "^2.0.3",
|
||||||
"vue-tsc": "^1.6.5"
|
"vue-tsc": "^1.6.5"
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 325 B After Width: | Height: | Size: 325 B |
4
public/assets/icons/mapEditor/dropdown-chevron.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M17 9.5L12 14.5L7 9.5" stroke="#fff" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 325 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 847 B After Width: | Height: | Size: 847 B |
Before Width: | Height: | Size: 745 B After Width: | Height: | Size: 745 B |
3
public/assets/icons/mapEditor/search.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="M16.3333 17.5L11.0833 12.25C10.6667 12.5833 10.1875 12.8472 9.64583 13.0417C9.10417 13.2361 8.52778 13.3333 7.91667 13.3333C6.40278 13.3333 5.12153 12.809 4.07292 11.7604C3.02431 10.7118 2.5 9.43056 2.5 7.91667C2.5 6.40278 3.02431 5.12153 4.07292 4.07292C5.12153 3.02431 6.40278 2.5 7.91667 2.5C9.43056 2.5 10.7118 3.02431 11.7604 4.07292C12.809 5.12153 13.3333 6.40278 13.3333 7.91667C13.3333 8.52778 13.2361 9.10417 13.0417 9.64583C12.8472 10.1875 12.5833 10.6667 12.25 11.0833L17.5 16.3333L16.3333 17.5ZM7.91667 11.6667C8.95833 11.6667 9.84375 11.3021 10.5729 10.5729C11.3021 9.84375 11.6667 8.95833 11.6667 7.91667C11.6667 6.875 11.3021 5.98958 10.5729 5.26042C9.84375 4.53125 8.95833 4.16667 7.91667 4.16667C6.875 4.16667 5.98958 4.53125 5.26042 5.26042C4.53125 5.98958 4.16667 6.875 4.16667 7.91667C4.16667 8.95833 4.53125 9.84375 5.26042 10.5729C5.98958 11.3021 6.875 11.6667 7.91667 11.6667Z" fill="#808080"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
59
public/assets/icons/mapEditor/settings.svg
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M490.667,405.333h-56.811C424.619,374.592,396.373,352,362.667,352s-61.931,22.592-71.189,53.333H21.333
|
||||||
|
C9.557,405.333,0,414.891,0,426.667S9.557,448,21.333,448h270.144c9.237,30.741,37.483,53.333,71.189,53.333
|
||||||
|
s61.931-22.592,71.189-53.333h56.811c11.797,0,21.333-9.557,21.333-21.333S502.464,405.333,490.667,405.333z M362.667,458.667
|
||||||
|
c-17.643,0-32-14.357-32-32s14.357-32,32-32s32,14.357,32,32S380.309,458.667,362.667,458.667z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M490.667,64h-56.811c-9.259-30.741-37.483-53.333-71.189-53.333S300.736,33.259,291.477,64H21.333
|
||||||
|
C9.557,64,0,73.557,0,85.333s9.557,21.333,21.333,21.333h270.144C300.736,137.408,328.96,160,362.667,160
|
||||||
|
s61.931-22.592,71.189-53.333h56.811c11.797,0,21.333-9.557,21.333-21.333S502.464,64,490.667,64z M362.667,117.333
|
||||||
|
c-17.643,0-32-14.357-32-32c0-17.643,14.357-32,32-32s32,14.357,32,32C394.667,102.976,380.309,117.333,362.667,117.333z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M490.667,234.667H220.523c-9.259-30.741-37.483-53.333-71.189-53.333s-61.931,22.592-71.189,53.333H21.333
|
||||||
|
C9.557,234.667,0,244.224,0,256c0,11.776,9.557,21.333,21.333,21.333h56.811c9.259,30.741,37.483,53.333,71.189,53.333
|
||||||
|
s61.931-22.592,71.189-53.333h270.144c11.797,0,21.333-9.557,21.333-21.333C512,244.224,502.464,234.667,490.667,234.667z
|
||||||
|
M149.333,288c-17.643,0-32-14.357-32-32s14.357-32,32-32c17.643,0,32,14.357,32,32S166.976,288,149.333,288z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 946 KiB |
Before Width: | Height: | Size: 109 B After Width: | Height: | Size: 109 B |
Before Width: | Height: | Size: 696 B After Width: | Height: | Size: 696 B |
Before Width: | Height: | Size: 708 B After Width: | Height: | Size: 708 B |
BIN
public/assets/music/intro.mp3
Normal file
BIN
public/assets/sounds/attack.wav
Normal file
BIN
public/assets/sounds/button-click.wav
Normal file
BIN
public/assets/sounds/connect.wav
Normal file
BIN
public/assets/sounds/walk.wav
Normal file
Before Width: | Height: | Size: 453 KiB After Width: | Height: | Size: 454 KiB |
4
src-tauri/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
/target/
|
||||||
|
/gen/schemas
|
5149
src-tauri/Cargo.lock
generated
Normal file
25
src-tauri/Cargo.toml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
[package]
|
||||||
|
name = "app"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "A Tauri App"
|
||||||
|
authors = ["you"]
|
||||||
|
license = ""
|
||||||
|
repository = ""
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.77.2"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "app_lib"
|
||||||
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "2.0.4", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde_json = "1.0"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
log = "0.4"
|
||||||
|
tauri = { version = "2.2.4", features = [] }
|
||||||
|
tauri-plugin-log = "2.0.0-rc"
|
3
src-tauri/build.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
11
src-tauri/capabilities/default.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "enables the default permissions",
|
||||||
|
"windows": [
|
||||||
|
"main"
|
||||||
|
],
|
||||||
|
"permissions": [
|
||||||
|
"core:default"
|
||||||
|
]
|
||||||
|
}
|
BIN
src-tauri/icons/128x128.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
After Width: | Height: | Size: 9.0 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
After Width: | Height: | Size: 7.4 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
src-tauri/icons/icon.png
Normal file
After Width: | Height: | Size: 49 KiB |
16
src-tauri/src/lib.rs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
pub fn run() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.setup(|app| {
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
app.handle().plugin(
|
||||||
|
tauri_plugin_log::Builder::default()
|
||||||
|
.level(log::LevelFilter::Info)
|
||||||
|
.build(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
|
}
|
6
src-tauri/src/main.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
app_lib::run();
|
||||||
|
}
|
37
src-tauri/tauri.conf.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||||
|
"productName": "noxious",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"identifier": "com.noxious.app",
|
||||||
|
"build": {
|
||||||
|
"frontendDist": "../dist",
|
||||||
|
"devUrl": "http://localhost:5173",
|
||||||
|
"beforeDevCommand": "npm run dev",
|
||||||
|
"beforeBuildCommand": "npm run build-only"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"title": "Noxious",
|
||||||
|
"width": 800,
|
||||||
|
"height": 600,
|
||||||
|
"resizable": true,
|
||||||
|
"fullscreen": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": "all",
|
||||||
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
41
src/App.vue
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<Debug />
|
||||||
<Notifications />
|
<Notifications />
|
||||||
<BackgroundImageLoader />
|
|
||||||
<GmPanel v-if="gameStore.character?.role === 'gm'" />
|
<GmPanel v-if="gameStore.character?.role === 'gm'" />
|
||||||
<component :is="currentScreen" />
|
<component :is="currentScreen" />
|
||||||
</template>
|
</template>
|
||||||
@ -9,40 +9,47 @@
|
|||||||
import GmPanel from '@/components/gameMaster/GmPanel.vue'
|
import GmPanel from '@/components/gameMaster/GmPanel.vue'
|
||||||
import Characters from '@/components/screens/Characters.vue'
|
import Characters from '@/components/screens/Characters.vue'
|
||||||
import Game from '@/components/screens/Game.vue'
|
import Game from '@/components/screens/Game.vue'
|
||||||
|
import Loading from '@/components/screens/Loading.vue'
|
||||||
import Login from '@/components/screens/Login.vue'
|
import Login from '@/components/screens/Login.vue'
|
||||||
import ZoneEditor from '@/components/screens/ZoneEditor.vue'
|
import MapEditor from '@/components/screens/MapEditor.vue'
|
||||||
import BackgroundImageLoader from '@/components/utilities/BackgroundImageLoader.vue'
|
import Debug from '@/components/utilities/Debug.vue'
|
||||||
import Notifications from '@/components/utilities/Notifications.vue'
|
import Notifications from '@/components/utilities/Notifications.vue'
|
||||||
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
|
import { useSoundComposable } from '@/composables/useSoundComposable'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
|
||||||
import { computed, watch } from 'vue'
|
import { computed, watch } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
|
||||||
|
const mapEditor = useMapEditorComposable()
|
||||||
|
const { playSound } = useSoundComposable()
|
||||||
|
|
||||||
const currentScreen = computed(() => {
|
const currentScreen = computed(() => {
|
||||||
if (!gameStore.connection) return Login
|
if (!gameStore.game.isLoaded) return Loading
|
||||||
if (!gameStore.token) return Login
|
if (!socketManager.connection) return Login
|
||||||
|
if (!socketManager.token) return Login
|
||||||
if (!gameStore.character) return Characters
|
if (!gameStore.character) return Characters
|
||||||
if (zoneEditorStore.active) return ZoneEditor
|
if (mapEditor.active.value) return MapEditor
|
||||||
return Game
|
return Game
|
||||||
})
|
})
|
||||||
|
|
||||||
// Watch zoneEditorStore.active and empty gameStore.game.loadedAssets
|
// Watch mapEditor.active and empty gameStore.game.loadedAssets
|
||||||
watch(
|
watch(
|
||||||
() => zoneEditorStore.active,
|
() => mapEditor.active.value,
|
||||||
() => {
|
() => {
|
||||||
gameStore.game.loadedAssets = []
|
gameStore.game.loadedTextures = []
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// #209: Play sound when a button is pressed
|
// #209: Play sound when a button is pressed
|
||||||
addEventListener('click', (event) => {
|
addEventListener('click', (event) => {
|
||||||
if (!(event.target instanceof HTMLButtonElement)) {
|
const classList = ['btn-cyan', 'btn-red', 'btn-indigo', 'btn-empty', 'btn-sound']
|
||||||
return
|
const target = event.target as HTMLElement
|
||||||
|
// console.log(target) // Uncomment to log the clicked element
|
||||||
|
if (classList.some((className) => target.classList.contains(className))) {
|
||||||
|
playSound('/assets/sounds/button-click.wav')
|
||||||
}
|
}
|
||||||
const audio = new Audio('/assets/music/click-btn.mp3')
|
|
||||||
audio.play()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Watch for "G" key press and toggle the gm panel
|
// Watch for "G" key press and toggle the gm panel
|
||||||
@ -50,7 +57,9 @@ addEventListener('keydown', (event) => {
|
|||||||
if (gameStore.character?.role !== 'gm') return // Only allow toggling the gm panel if the character is a gm
|
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
|
// 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.repeat || event.isComposing || event.defaultPrevented || document.activeElement?.tagName.toUpperCase() === 'INPUT' || document.activeElement?.tagName.toUpperCase() === 'TEXTAREA') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (event.key === 'G') {
|
if (event.key === 'G') {
|
||||||
gameStore.toggleGmPanel()
|
gameStore.toggleGmPanel()
|
||||||
|
@ -1,81 +0,0 @@
|
|||||||
import config from '@/application/config'
|
|
||||||
import type { AssetDataT } from '@/application/types'
|
|
||||||
import Dexie from 'dexie'
|
|
||||||
|
|
||||||
export class Assets {
|
|
||||||
private db: Dexie
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.db = new Dexie('assets')
|
|
||||||
this.db.version(1).stores({
|
|
||||||
assets: 'key, group'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async download(asset: AssetDataT) {
|
|
||||||
try {
|
|
||||||
// Check if the asset already exists, then check if updatedAt is newer
|
|
||||||
const _asset = await this.db.table('assets').get(asset.key)
|
|
||||||
if (_asset && _asset.updatedAt > asset.updatedAt) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Download the asset
|
|
||||||
const response = await fetch(config.server_endpoint + asset.data)
|
|
||||||
const blob = await response.blob()
|
|
||||||
|
|
||||||
// Store the asset in the database
|
|
||||||
await this.db.table('assets').put({
|
|
||||||
key: asset.key,
|
|
||||||
data: blob,
|
|
||||||
group: asset.group,
|
|
||||||
updatedAt: asset.updatedAt,
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async get(key: string) {
|
|
||||||
try {
|
|
||||||
const asset = await this.db.table('assets').get(key)
|
|
||||||
if (asset) {
|
|
||||||
return {
|
|
||||||
...asset,
|
|
||||||
data: URL.createObjectURL(asset.data) // Convert blob to data URL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to retrieve asset ${key}:`, error)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
async getByGroup(group: string) {
|
|
||||||
try {
|
|
||||||
const assets = await this.db.table('assets').where('group').equals(group).toArray()
|
|
||||||
return assets.map((asset) => ({
|
|
||||||
...asset,
|
|
||||||
data: URL.createObjectURL(asset.data) // Convert blob to data URL
|
|
||||||
}))
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to retrieve assets for group ${group}:`, error)
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(key: string) {
|
|
||||||
try {
|
|
||||||
await this.db.table('assets').delete(key)
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to delete asset ${key}:`, error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +1,10 @@
|
|||||||
export default {
|
export default {
|
||||||
name: import.meta.env.VITE_NAME,
|
name: import.meta.env.VITE_NAME,
|
||||||
development: import.meta.env.VITE_DEVELOPMENT === 'true',
|
domain: import.meta.env.VITE_DOMAIN,
|
||||||
|
environment: import.meta.env.VITE_ENVIRONMENT,
|
||||||
server_endpoint: import.meta.env.VITE_SERVER_ENDPOINT,
|
server_endpoint: import.meta.env.VITE_SERVER_ENDPOINT,
|
||||||
tile_size: {
|
tile_size: {
|
||||||
x: Number(import.meta.env.VITE_TILE_SIZE_X),
|
width: Number(import.meta.env.VITE_TILE_SIZE_WIDTH),
|
||||||
y: Number(import.meta.env.VITE_TILE_SIZE_Y)
|
height: Number(import.meta.env.VITE_TILE_SIZE_HEIGHT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
63
src/application/enums.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
export enum Direction {
|
||||||
|
POSITIVE,
|
||||||
|
NEGATIVE,
|
||||||
|
UNCHANGED
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SocketEvent {
|
||||||
|
CONNECT_ERROR = 'connect_error',
|
||||||
|
RECONNECT_FAILED = 'reconnect_failed',
|
||||||
|
CLOSE = '52',
|
||||||
|
DATA = '51',
|
||||||
|
CHARACTER_CONNECT = '50',
|
||||||
|
CHARACTER_CREATE = '49',
|
||||||
|
CHARACTER_DELETE = '48',
|
||||||
|
CHARACTER_LIST = '47',
|
||||||
|
GM_CHARACTERHAIR_CREATE = '46',
|
||||||
|
GM_CHARACTERHAIR_REMOVE = '45',
|
||||||
|
GM_CHARACTERHAIR_LIST = '44',
|
||||||
|
GM_CHARACTERHAIR_UPDATE = '43',
|
||||||
|
GM_CHARACTERTYPE_CREATE = '42',
|
||||||
|
GM_CHARACTERTYPE_REMOVE = '41',
|
||||||
|
GM_CHARACTERTYPE_LIST = '40',
|
||||||
|
GM_CHARACTERTYPE_UPDATE = '39',
|
||||||
|
GM_ITEM_CREATE = '38',
|
||||||
|
GM_ITEM_REMOVE = '37',
|
||||||
|
GM_ITEM_LIST = '36',
|
||||||
|
GM_ITEM_UPDATE = '35',
|
||||||
|
GM_MAPOBJECT_LIST = '34',
|
||||||
|
GM_MAPOBJECT_REMOVE = '33',
|
||||||
|
GM_MAPOBJECT_UPDATE = '32',
|
||||||
|
GM_MAPOBJECT_UPLOAD = '31',
|
||||||
|
GM_SPRITE_COPY = '30',
|
||||||
|
GM_SPRITE_CREATE = '29',
|
||||||
|
GM_SPRITE_DELETE = '28',
|
||||||
|
GM_SPRITE_LIST = '27',
|
||||||
|
GM_SPRITE_UPDATE = '26',
|
||||||
|
GM_TILE_DELETE = '25',
|
||||||
|
GM_TILE_LIST = '24',
|
||||||
|
GM_TILE_UPDATE = '23',
|
||||||
|
GM_TILE_UPLOAD = '22',
|
||||||
|
GM_MAP_CREATE = '21',
|
||||||
|
GM_MAP_DELETE = '20',
|
||||||
|
GM_MAP_REQUEST = '19',
|
||||||
|
GM_MAP_UPDATE = '18',
|
||||||
|
MAP_CHARACTER_MOVEERROR = '17',
|
||||||
|
DISCONNECT = 'disconnect',
|
||||||
|
USER_DISCONNECT = '15',
|
||||||
|
LOGIN = '14',
|
||||||
|
LOGGED_IN = '13',
|
||||||
|
NOTIFICATION = '12',
|
||||||
|
DATE = '11',
|
||||||
|
FAILED = '10',
|
||||||
|
COMPLETED = '9',
|
||||||
|
CONNECTION = 'connection',
|
||||||
|
WEATHER = '7',
|
||||||
|
CHARACTER_DISCONNECT = '6',
|
||||||
|
MAP_CHARACTER_ATTACK = '5',
|
||||||
|
MAP_CHARACTER_TELEPORT = '4',
|
||||||
|
MAP_CHARACTER_JOIN = '3',
|
||||||
|
MAP_CHARACTER_LEAVE = '2',
|
||||||
|
MAP_CHARACTER_MOVE = '1',
|
||||||
|
CHAT_MESSAGE = '0'
|
||||||
|
}
|
@ -12,14 +12,13 @@ export type HttpResponse<T> = {
|
|||||||
data?: T
|
data?: T
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AssetDataT = {
|
export type TextureData = {
|
||||||
key: string
|
key: string
|
||||||
data: string
|
data: string // URL or Base64 encoded blob
|
||||||
group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
|
group: 'tiles' | 'map_objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
originX?: number
|
originX?: number
|
||||||
originY?: number
|
originY?: number
|
||||||
isAnimated?: boolean
|
|
||||||
frameRate?: number
|
frameRate?: number
|
||||||
frameWidth?: number
|
frameWidth?: number
|
||||||
frameHeight?: number
|
frameHeight?: number
|
||||||
@ -27,36 +26,34 @@ export type AssetDataT = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Tile = {
|
export type Tile = {
|
||||||
id: UUID
|
id: string
|
||||||
name: string
|
name: string
|
||||||
tags: any | null
|
tags: any | null
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Object = {
|
export type MapObject = {
|
||||||
id: UUID
|
id: string
|
||||||
name: string
|
name: string
|
||||||
tags: any | null
|
tags: string[]
|
||||||
|
depthOffsets: number[]
|
||||||
originX: number
|
originX: number
|
||||||
originY: number
|
originY: number
|
||||||
isAnimated: boolean
|
|
||||||
frameRate: number
|
frameRate: number
|
||||||
frameWidth: number
|
frameWidth: number
|
||||||
frameHeight: number
|
frameHeight: number
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
ZoneObject: ZoneObject[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Item = {
|
export type Item = {
|
||||||
id: UUID
|
id: string
|
||||||
name: string
|
name: string
|
||||||
description: string | null
|
description: string | null
|
||||||
itemType: ItemType
|
itemType: ItemType
|
||||||
stackable: boolean
|
stackable: boolean
|
||||||
rarity: ItemRarity
|
rarity: ItemRarity
|
||||||
spriteId: UUID | null
|
|
||||||
sprite?: Sprite
|
sprite?: Sprite
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
@ -65,72 +62,63 @@ export type Item = {
|
|||||||
export type ItemType = 'WEAPON' | 'HELMET' | 'CHEST' | 'LEGS' | 'BOOTS' | 'GLOVES' | 'RING' | 'NECKLACE'
|
export type ItemType = 'WEAPON' | 'HELMET' | 'CHEST' | 'LEGS' | 'BOOTS' | 'GLOVES' | 'RING' | 'NECKLACE'
|
||||||
export type ItemRarity = 'COMMON' | 'UNCOMMON' | 'RARE' | 'EPIC' | 'LEGENDARY'
|
export type ItemRarity = 'COMMON' | 'UNCOMMON' | 'RARE' | 'EPIC' | 'LEGENDARY'
|
||||||
|
|
||||||
export type Zone = {
|
export type Map = {
|
||||||
id: UUID
|
id: string
|
||||||
name: string
|
name: string
|
||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
tiles: any | null
|
tiles: string[][]
|
||||||
pvp: boolean
|
pvp: boolean
|
||||||
zoneEffects: ZoneEffect[]
|
mapEffects: MapEffect[]
|
||||||
zoneEventTiles: ZoneEventTile[]
|
mapEventTiles: MapEventTile[]
|
||||||
zoneObjects: ZoneObject[]
|
placedMapObjects: PlacedMapObject[]
|
||||||
characters: Character[]
|
characters: Character[]
|
||||||
chats: Chat[]
|
chats: Chat[]
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ZoneEffect = {
|
export type MapEffect = {
|
||||||
id: UUID
|
id: string
|
||||||
zoneId: UUID
|
|
||||||
zone: Zone
|
|
||||||
effect: string
|
effect: string
|
||||||
strength: number
|
strength: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ZoneObject = {
|
export type PlacedMapObject = {
|
||||||
id: UUID
|
id: string
|
||||||
zoneId: UUID
|
mapObject: MapObject | string
|
||||||
zone: Zone
|
|
||||||
objectId: UUID
|
|
||||||
object: Object
|
|
||||||
depth: number
|
|
||||||
isRotated: boolean
|
isRotated: boolean
|
||||||
positionX: number
|
positionX: number
|
||||||
positionY: number
|
positionY: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ZoneEventTileType {
|
export enum MapEventTileType {
|
||||||
BLOCK = 'BLOCK',
|
BLOCK = 'BLOCK',
|
||||||
TELEPORT = 'TELEPORT',
|
TELEPORT = 'TELEPORT',
|
||||||
NPC = 'NPC',
|
NPC = 'NPC',
|
||||||
ITEM = 'ITEM'
|
ITEM = 'ITEM'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ZoneEventTile = {
|
export type MapEventTile = {
|
||||||
id: UUID
|
id: string
|
||||||
zoneId: UUID
|
map: string
|
||||||
zone: Zone
|
type: MapEventTileType
|
||||||
type: ZoneEventTileType
|
|
||||||
positionX: number
|
positionX: number
|
||||||
positionY: number
|
positionY: number
|
||||||
teleport?: ZoneEventTileTeleport
|
teleport?: MapEventTileTeleport
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ZoneEventTileTeleport = {
|
export type MapEventTileTeleport = {
|
||||||
id: UUID
|
id: string
|
||||||
zoneEventTileId: UUID
|
mapEventTile: MapEventTile
|
||||||
zoneEventTile: ZoneEventTile
|
toMap: Map
|
||||||
toZoneId: UUID
|
|
||||||
toZone: Zone
|
|
||||||
toPositionX: number
|
toPositionX: number
|
||||||
toPositionY: number
|
toPositionY: number
|
||||||
toRotation: number
|
toRotation: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
id: UUID
|
id: string
|
||||||
username: string
|
username: string
|
||||||
password: string
|
password: string
|
||||||
characters: Character[]
|
characters: Character[]
|
||||||
@ -150,29 +138,28 @@ export enum CharacterRace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type CharacterType = {
|
export type CharacterType = {
|
||||||
id: UUID
|
id: string
|
||||||
name: string
|
name: string
|
||||||
gender: CharacterGender
|
gender: CharacterGender
|
||||||
race: CharacterRace
|
race: CharacterRace
|
||||||
isSelectable: boolean
|
isSelectable: boolean
|
||||||
characters: Character[]
|
|
||||||
spriteId?: string
|
|
||||||
sprite?: Sprite
|
sprite?: Sprite
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CharacterHair = {
|
export type CharacterHair = {
|
||||||
id: UUID
|
id: string
|
||||||
name: string
|
name: string
|
||||||
sprite: Sprite
|
sprite: string | Sprite
|
||||||
gender: CharacterGender
|
gender: CharacterGender
|
||||||
|
color: string
|
||||||
isSelectable: boolean
|
isSelectable: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Character = {
|
export type Character = {
|
||||||
id: UUID
|
id: string
|
||||||
userId: UUID
|
userid: string
|
||||||
user: User
|
user: User
|
||||||
name: string
|
name: string
|
||||||
hitpoints: number
|
hitpoints: number
|
||||||
@ -184,35 +171,30 @@ export type Character = {
|
|||||||
positionX: number
|
positionX: number
|
||||||
positionY: number
|
positionY: number
|
||||||
rotation: number
|
rotation: number
|
||||||
characterTypeId: UUID | null
|
characterType: UUID | null
|
||||||
characterType: CharacterType | null | string
|
characterHair: UUID | null
|
||||||
characterHairId: UUID | null
|
map: UUID
|
||||||
characterHair: CharacterHair | null
|
|
||||||
zoneId: UUID
|
|
||||||
zone: Zone
|
|
||||||
chats: Chat[]
|
chats: Chat[]
|
||||||
items: CharacterItem[]
|
items: CharacterItem[]
|
||||||
equipment: CharacterEquipment[]
|
equipment: CharacterEquipment[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ZoneCharacter = {
|
export type MapCharacter = {
|
||||||
character: Character
|
character: Character
|
||||||
isMoving?: boolean
|
isMoving: boolean
|
||||||
|
isAttacking?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CharacterItem = {
|
export type CharacterItem = {
|
||||||
id: UUID
|
id: string
|
||||||
characterId: UUID
|
|
||||||
character: Character
|
character: Character
|
||||||
itemId: UUID
|
|
||||||
item: Item
|
item: Item
|
||||||
quantity: number
|
quantity: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CharacterEquipment = {
|
export type CharacterEquipment = {
|
||||||
id: UUID
|
id: string
|
||||||
slot: CharacterEquipmentSlotType
|
slot: CharacterEquipmentSlotType
|
||||||
characterItemId: UUID
|
|
||||||
characterItem: CharacterItem
|
characterItem: CharacterItem
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,53 +208,55 @@ export enum CharacterEquipmentSlotType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Sprite = {
|
export type Sprite = {
|
||||||
id: UUID
|
id: string
|
||||||
name: string
|
name: string
|
||||||
|
width: number | null
|
||||||
|
height: number | null
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
spriteActions: SpriteAction[]
|
spriteActions: SpriteAction[]
|
||||||
characterTypes: CharacterType[]
|
characterTypes: CharacterType[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SpriteImage {
|
||||||
|
url: string
|
||||||
|
offset: {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type SpriteAction = {
|
export type SpriteAction = {
|
||||||
id: UUID
|
id: string
|
||||||
sprite: Sprite
|
sprite: string
|
||||||
action: string
|
action: string
|
||||||
sprites: string[]
|
sprites: SpriteImage[]
|
||||||
originX: number
|
originX: number
|
||||||
originY: number
|
originY: number
|
||||||
isAnimated: boolean
|
|
||||||
isLooping: boolean
|
|
||||||
frameWidth: number
|
frameWidth: number
|
||||||
frameHeight: number
|
frameHeight: number
|
||||||
frameRate: number
|
frameRate: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Chat = {
|
export type Chat = {
|
||||||
id: UUID
|
id: string
|
||||||
characterId: UUID
|
|
||||||
character: Character
|
character: Character
|
||||||
zoneId: UUID
|
map: Map
|
||||||
zone: Zone
|
|
||||||
message: string
|
message: string
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorldSettings = {
|
export type WorldSettings = {
|
||||||
date: Date
|
date: Date
|
||||||
isRainEnabled: boolean
|
weatherState: WeatherState
|
||||||
isFogEnabled: boolean
|
|
||||||
fogDensity: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WeatherState = {
|
export type WeatherState = {
|
||||||
isRainEnabled: boolean
|
|
||||||
rainPercentage: number
|
rainPercentage: number
|
||||||
isFogEnabled: boolean
|
|
||||||
fogDensity: number
|
fogDensity: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type zoneLoadData = {
|
export type mapLoadData = {
|
||||||
zone: Zone
|
mapId: string
|
||||||
characters: ZoneCharacter[]
|
characters: MapCharacter[]
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,45 @@
|
|||||||
|
import config from '@/application/config'
|
||||||
|
import type { HttpResponse } from '@/application/types'
|
||||||
|
import type { BaseStorage } from '@/storage/baseStorage'
|
||||||
|
|
||||||
export function uuidv4() {
|
export function uuidv4() {
|
||||||
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) => (+c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))).toString(16))
|
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) => (+c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))).toString(16))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unduplicateArray(array: any[]) {
|
export function unduplicateArray(array: any[]) {
|
||||||
return [...new Set(array.flat())]
|
const arrayToProcess = typeof array.flat === 'function' ? array.flat() : array
|
||||||
|
return [...new Set(arrayToProcess)]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDomain() {
|
export async function downloadCache<T extends { id: string; updatedAt: Date }>(endpoint: string, storage: BaseStorage<T>) {
|
||||||
// Check if not localhost
|
const request = await fetch(`${config.server_endpoint}/cache/${endpoint}`)
|
||||||
if (window.location.hostname !== 'localhost') {
|
const response = (await request.json()) as HttpResponse<T[]>
|
||||||
return window.location.hostname
|
|
||||||
|
if (!response.success) {
|
||||||
|
console.error(`Failed to download ${endpoint}:`, response.message)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if not IP address
|
const items = response.data ?? []
|
||||||
if (window.location.hostname.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) {
|
const serverItemIds = new Set(items.map((item) => item.id))
|
||||||
return window.location.hostname
|
|
||||||
|
// Remove items that don't exist on server
|
||||||
|
const existingItems = await storage.getAll()
|
||||||
|
for (const existingItem of existingItems) {
|
||||||
|
if (!serverItemIds.has(existingItem.id)) {
|
||||||
|
await storage.delete(existingItem.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.location.hostname.split('.').length < 3) {
|
// Update or add new items
|
||||||
return window.location.hostname
|
for (const item of items) {
|
||||||
|
let overwrite = false
|
||||||
|
const existingItem = await storage.getById(item.id)
|
||||||
|
|
||||||
|
if (!existingItem || item.updatedAt > existingItem.updatedAt) {
|
||||||
|
overwrite = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return window.location.hostname.split('.').slice(-2).join('.')
|
await storage.add(item, overwrite)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,7 +73,7 @@ input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.input-field {
|
.input-field {
|
||||||
@apply px-4 py-2.5 text-base leading-5 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 font-default;
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
@apply outline-none border-cyan rounded bg-gray-900;
|
@apply outline-none border-cyan rounded bg-gray-900;
|
||||||
}
|
}
|
||||||
@ -88,6 +88,12 @@ input {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
&.input-field {
|
||||||
|
@apply appearance-none bg-[url('/assets/icons/mapEditor/dropdown-chevron.svg')] bg-no-repeat bg-[calc(100%_-_10px)_center] bg-[length:20px] text-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.form-field-full {
|
.form-field-full {
|
||||||
@apply w-full flex flex-col mb-5;
|
@apply w-full flex flex-col mb-5;
|
||||||
label {
|
label {
|
||||||
@ -118,7 +124,16 @@ button {
|
|||||||
|
|
||||||
&.active,
|
&.active,
|
||||||
&:hover {
|
&:hover {
|
||||||
@apply bg-red-400;
|
@apply bg-red-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-indigo {
|
||||||
|
@apply bg-indigo-500 text-gray-50 text-base leading-5 rounded py-2.5;
|
||||||
|
|
||||||
|
&.active,
|
||||||
|
&:hover {
|
||||||
|
@apply bg-indigo-600;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,6 +164,10 @@ button {
|
|||||||
@apply bg-gray bg-none;
|
@apply bg-gray bg-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-open {
|
||||||
|
@apply w-[calc(75%_-_40px)] max-xl:w-[calc(100%_-_360px)];
|
||||||
|
}
|
||||||
|
|
||||||
.hair-deselect:has(:checked) {
|
.hair-deselect:has(:checked) {
|
||||||
img {
|
img {
|
||||||
@apply brightness-200;
|
@apply brightness-200;
|
||||||
|
@ -1,179 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Scene name="effects" @preload="preloadScene" @create="createScene" @update="updateScene" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { WeatherState } from '@/application/types'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import { useZoneStore } from '@/stores/zoneStore'
|
|
||||||
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 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 = (scene: Phaser.Scene) => {
|
|
||||||
sceneRef.value = 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 = () => {
|
|
||||||
const timeBasedLight = calculateLightStrength(gameStore.world.date)
|
|
||||||
const zoneEffects = zoneStore.zone?.zoneEffects?.reduce(
|
|
||||||
(acc, curr) => ({
|
|
||||||
...acc,
|
|
||||||
[curr.effect]: curr.strength
|
|
||||||
}),
|
|
||||||
{}
|
|
||||||
) as { [key: string]: number }
|
|
||||||
|
|
||||||
// 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 applyEffects = (effectValues: any) => {
|
|
||||||
if (effects.light.value) {
|
|
||||||
const darkness = 1 - (effectValues.light ?? 100) / 100
|
|
||||||
effects.light.value.clear().fillStyle(0x000000, darkness).fillRect(0, 0, window.innerWidth, window.innerHeight)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (effects.rain.value) {
|
|
||||||
const strength = effectValues.rain ?? 0
|
|
||||||
strength > 0 ? effects.rain.value.start().setQuantity(Math.floor((strength / 100) * 10)) : effects.rain.value.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (effects.fog.value) {
|
|
||||||
effects.fog.value.setAlpha((effectValues.fog ?? 0) / 100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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')
|
|
||||||
})
|
|
||||||
</script>
|
|
@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-wrap items-center input-field gap-1">
|
<div class="flex flex-wrap items-center input-field gap-1" @click="focusInput">
|
||||||
<div v-for="(chip, i) in internalValue" :key="i" class="flex gap-2.5 items-center bg-cyan rounded py-1 px-2">
|
<div v-for="(chip, i) in internalValue" :key="i" class="flex gap-2.5 items-center bg-cyan rounded py-1 px-2" role="listitem">
|
||||||
<span class="text-xs text-white">{{ chip }}</span>
|
<span class="text-xs text-white">{{ chip }}</span>
|
||||||
<button type="button" class="text-xs cursor-pointer text-white font-light font-default not-italic hover:text-gray-50" @click="deleteChip(i)" aria-label="Remove chip">×</button>
|
<button type="button" class="text-xs cursor-pointer text-white font-light font-default not-italic hover:text-gray-50" @click.stop="deleteChip(i)" aria-label="Remove tag">×</button>
|
||||||
</div>
|
</div>
|
||||||
<input class="outline-none border-none p-1 text-gray-300" placeholder="Tag name" v-model="currentInput" @keypress.enter.prevent="addChip" @keydown.backspace="handleBackspace" />
|
<input ref="inputRef" class="outline-none border-none p-1 text-gray-300 min-w-[60px] flex-grow" :placeholder="placeholder" v-model.trim="currentInput" @keydown="handleKeydown" @paste="handlePaste" :maxlength="maxChipLength" aria-label="Add new tag" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -14,20 +14,29 @@ import type { Ref } from 'vue'
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue?: string[]
|
modelValue?: string[]
|
||||||
|
maxChips?: number
|
||||||
|
maxChipLength?: number
|
||||||
|
placeholder?: string
|
||||||
|
allowDuplicates?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
modelValue: () => []
|
modelValue: () => [],
|
||||||
|
maxChips: 10,
|
||||||
|
maxChipLength: 20,
|
||||||
|
placeholder: 'Add tag',
|
||||||
|
allowDuplicates: false
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: string[]): void
|
(e: 'update:modelValue', value: string[]): void
|
||||||
|
(e: 'error', message: string): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const currentInput: Ref<string> = ref('')
|
const currentInput: Ref<string> = ref('')
|
||||||
const internalValue = ref<string[]>([])
|
const internalValue = ref<string[]>([])
|
||||||
|
const inputRef = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
// Initialize internalValue with props.modelValue
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
@ -36,9 +45,27 @@ watch(
|
|||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const validateChip = (chip: string): boolean => {
|
||||||
|
if (!chip) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.allowDuplicates && internalValue.value.includes(chip)) {
|
||||||
|
emit('error', 'Duplicate tags are not allowed')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (internalValue.value.length >= props.maxChips) {
|
||||||
|
emit('error', `Maximum ${props.maxChips} tags allowed`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
const addChip = () => {
|
const addChip = () => {
|
||||||
const trimmedInput = currentInput.value.trim()
|
const trimmedInput = currentInput.value.trim()
|
||||||
if (trimmedInput && !internalValue.value.includes(trimmedInput)) {
|
if (validateChip(trimmedInput)) {
|
||||||
internalValue.value.push(trimmedInput)
|
internalValue.value.push(trimmedInput)
|
||||||
emit('update:modelValue', internalValue.value)
|
emit('update:modelValue', internalValue.value)
|
||||||
currentInput.value = ''
|
currentInput.value = ''
|
||||||
@ -50,10 +77,36 @@ const deleteChip = (index: number) => {
|
|||||||
emit('update:modelValue', internalValue.value)
|
emit('update:modelValue', internalValue.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleBackspace = (event: KeyboardEvent) => {
|
const handleKeydown = (event: KeyboardEvent) => {
|
||||||
if (event.key === 'Backspace' && currentInput.value === '' && internalValue.value.length > 0) {
|
switch (event.key) {
|
||||||
internalValue.value.pop()
|
case 'Enter':
|
||||||
emit('update:modelValue', internalValue.value)
|
event.preventDefault()
|
||||||
|
addChip()
|
||||||
|
break
|
||||||
|
case 'Backspace':
|
||||||
|
if (currentInput.value === '' && internalValue.value.length > 0) {
|
||||||
|
deleteChip(internalValue.value.length - 1)
|
||||||
}
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePaste = (event: ClipboardEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
const pastedText = event.clipboardData?.getData('text')
|
||||||
|
if (pastedText) {
|
||||||
|
const chips = pastedText
|
||||||
|
.split(/[,\n]/)
|
||||||
|
.map((chip) => chip.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
chips.forEach((chip) => {
|
||||||
|
currentInput.value = chip
|
||||||
|
addChip()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const focusInput = () => {
|
||||||
|
inputRef.value?.focus()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,181 +1,103 @@
|
|||||||
<template>
|
<template>
|
||||||
<ChatBubble :zoneCharacter="props.zoneCharacter" :currentX="currentX" :currentY="currentY" />
|
<Container ref="characterContainer" :x="currentPositionX" :y="currentPositionY" :depth="isometricDepth">
|
||||||
<Healthbar :zoneCharacter="props.zoneCharacter" :currentX="currentX" :currentY="currentY" />
|
<ChatBubble :mapCharacter="props.mapCharacter" />
|
||||||
<Container ref="charContainer" :depth="isometricDepth" :x="currentX" :y="currentY">
|
<HealthBar :mapCharacter="props.mapCharacter" />
|
||||||
<CharacterHair :zoneCharacter="props.zoneCharacter" :currentX="currentX" :currentY="currentY" />
|
<CharacterHair :mapCharacter="props.mapCharacter" :flipX="isFlippedX" />
|
||||||
<!-- <CharacterChest :zoneCharacter="props.zoneCharacter" :currentX="currentX" :currentY="currentY" />-->
|
<Sprite ref="characterSprite" :flipX="isFlippedX" />
|
||||||
<Sprite ref="charSprite" :origin-y="1" :flipX="isFlippedX" />
|
|
||||||
</Container>
|
</Container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import config from '@/application/config'
|
import { type MapCharacter } from '@/application/types'
|
||||||
import { type Sprite as SpriteT, type ZoneCharacter } from '@/application/types'
|
|
||||||
import CharacterHair from '@/components/game/character/partials/CharacterHair.vue'
|
import CharacterHair from '@/components/game/character/partials/CharacterHair.vue'
|
||||||
import ChatBubble from '@/components/game/character/partials/ChatBubble.vue'
|
import ChatBubble from '@/components/game/character/partials/ChatBubble.vue'
|
||||||
import Healthbar from '@/components/game/character/partials/Healthbar.vue'
|
import HealthBar from '@/components/game/character/partials/HealthBar.vue'
|
||||||
import { loadSpriteTextures } from '@/composables/gameComposable'
|
import { useCharacterSpriteComposable } from '@/composables/useCharacterSpriteComposable'
|
||||||
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/zoneComposable'
|
import { useSoundComposable } from '@/composables/useSoundComposable'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useZoneStore } from '@/stores/zoneStore'
|
import { useMapStore } from '@/stores/mapStore'
|
||||||
import { Container, refObj, Sprite, useScene } from 'phavuer'
|
import { Container, Sprite, useScene } from 'phavuer'
|
||||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
import { onMounted, onUnmounted, watch } from 'vue'
|
||||||
|
|
||||||
// import CharacterChest from '@/components/game/character/partials/CharacterChest.vue'
|
|
||||||
|
|
||||||
enum Direction {
|
|
||||||
POSITIVE,
|
|
||||||
NEGATIVE,
|
|
||||||
UNCHANGED
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
tilemap: Phaser.Tilemaps.Tilemap
|
tileMap: Phaser.Tilemaps.Tilemap
|
||||||
zoneCharacter: ZoneCharacter
|
mapCharacter: MapCharacter
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const charContainer = refObj<Phaser.GameObjects.Container>()
|
|
||||||
const charSprite = refObj<Phaser.GameObjects.Sprite>()
|
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const zoneStore = useZoneStore()
|
const mapStore = useMapStore()
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
|
|
||||||
const currentX = ref(0)
|
const { characterContainer, characterSprite, currentPositionX, currentPositionY, isometricDepth, isFlippedX, updatePosition, playAnimation, updateSprite, initializeSprite, cleanup } = useCharacterSpriteComposable(scene, props.tileMap, props.mapCharacter)
|
||||||
const currentY = ref(0)
|
const { playSound, stopSound } = useSoundComposable()
|
||||||
const isometricDepth = ref(1)
|
|
||||||
const isInitialPosition = ref(true)
|
|
||||||
const tween = ref<Phaser.Tweens.Tween | null>(null)
|
|
||||||
|
|
||||||
const updateIsometricDepth = (x: number, y: number) => {
|
const handlePositionUpdate = (newValues: any, oldValues: any) => {
|
||||||
isometricDepth.value = calculateIsometricDepth(x, y, 28, 94, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (tween.value?.isPlaying()) {
|
|
||||||
tween.value.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
const distance = Math.sqrt(Math.pow(targetX - currentX.value, 2) + Math.pow(targetY - currentY.value, 2))
|
|
||||||
|
|
||||||
if (distance >= config.tile_size.x / 1.1) {
|
|
||||||
currentX.value = targetX
|
|
||||||
currentY.value = targetY
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const duration = distance * 5.7
|
|
||||||
|
|
||||||
tween.value = props.tilemap.scene.tweens.add({
|
|
||||||
targets: { x: currentX.value, y: currentY.value },
|
|
||||||
x: targetX,
|
|
||||||
y: targetY,
|
|
||||||
duration,
|
|
||||||
ease: 'Linear',
|
|
||||||
onStart: () => {
|
|
||||||
if (direction === Direction.POSITIVE) {
|
|
||||||
updateIsometricDepth(x, y)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onUpdate: (tween) => {
|
|
||||||
currentX.value = tween.targets[0].x
|
|
||||||
currentY.value = tween.targets[0].y
|
|
||||||
},
|
|
||||||
onComplete: () => {
|
|
||||||
if (direction === Direction.NEGATIVE) {
|
|
||||||
updateIsometricDepth(x, y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (!newValues) return
|
||||||
|
|
||||||
if (!oldValues || newValues.x !== oldValues.x || newValues.y !== oldValues.y) {
|
if (!oldValues || newValues.positionX !== oldValues.positionX || newValues.positionY !== oldValues.positionY) {
|
||||||
const direction = !oldValues ? Direction.POSITIVE : calcDirection(oldValues.x, oldValues.y, newValues.x, newValues.y)
|
updatePosition(newValues.positionX, newValues.positionY)
|
||||||
updatePosition(newValues.x, newValues.y, direction)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle animation updates
|
|
||||||
if (newValues.isMoving !== oldValues?.isMoving || newValues.rotation !== oldValues?.rotation) {
|
if (newValues.isMoving !== oldValues?.isMoving || newValues.rotation !== oldValues?.rotation) {
|
||||||
updateSprite()
|
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)
|
/**
|
||||||
|
* Plays walk sound when character is moving
|
||||||
|
*/
|
||||||
|
watch(
|
||||||
|
() => props.mapCharacter.isMoving,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
playSound('/assets/sounds/walk.wav', false, true)
|
||||||
|
} else {
|
||||||
|
stopSound('/assets/sounds/walk.wav')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plays attack animation and sound when character is attacking
|
||||||
|
*/
|
||||||
|
watch(
|
||||||
|
() => props.mapCharacter.isAttacking,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
playAnimation('attack')
|
||||||
|
playSound('/assets/sounds/attack.wav', false, true)
|
||||||
|
} else {
|
||||||
|
stopSound('/assets/sounds/attack.wav')
|
||||||
|
}
|
||||||
|
mapStore.updateCharacterProperty(props.mapCharacter.character.id, 'isAttacking', false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles position updates and movement delay
|
||||||
|
*/
|
||||||
|
watch(
|
||||||
|
() => ({
|
||||||
|
positionX: props.mapCharacter.character.positionX,
|
||||||
|
positionY: props.mapCharacter.character.positionY,
|
||||||
|
isMoving: props.mapCharacter.isMoving,
|
||||||
|
rotation: props.mapCharacter.character.rotation,
|
||||||
|
isAttacking: props.mapCharacter.isAttacking
|
||||||
|
}),
|
||||||
|
async (oldValues, newValues) => {
|
||||||
|
handlePositionUpdate(oldValues, newValues)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await initializeSprite()
|
||||||
|
|
||||||
|
if (props.mapCharacter.character.id === gameStore.character!.id) {
|
||||||
|
scene.cameras.main.startFollow(characterContainer.value as Phaser.GameObjects.Container)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
tween.value?.stop()
|
cleanup()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,51 +0,0 @@
|
|||||||
<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>
|
|
@ -1,51 +1,63 @@
|
|||||||
<template>
|
<template>
|
||||||
<Image v-bind="imageProps" v-if="gameStore.getLoadedAsset(texture)" />
|
<Image ref="image" v-if="hairSpriteId" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Sprite as SpriteT, ZoneCharacter } from '@/application/types'
|
import type { MapCharacter, Sprite as SpriteT } from '@/application/types'
|
||||||
import { loadSpriteTextures } from '@/composables/gameComposable'
|
import { loadSpriteTextures } from '@/services/textureService'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { CharacterHairStorage, CharacterTypeStorage, SpriteStorage } from '@/storage/storages'
|
||||||
import { Image, useScene } from 'phavuer'
|
import { Image, refObj, useScene } from 'phavuer'
|
||||||
import { computed } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
zoneCharacter: ZoneCharacter
|
mapCharacter: MapCharacter
|
||||||
currentX: number
|
|
||||||
currentY: number
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
|
const hairSpriteId = ref('')
|
||||||
|
const hairSprite = ref<SpriteT | null>(null)
|
||||||
|
const characterSpriteHeight = ref(0)
|
||||||
|
const image = refObj<Phaser.GameObjects.Image>()
|
||||||
|
|
||||||
|
const flipX = computed(() => [6, 0].includes(props.mapCharacter.character.rotation ?? 0))
|
||||||
const texture = computed(() => {
|
const texture = computed(() => {
|
||||||
const { rotation, characterHair } = props.zoneCharacter.character
|
const direction = flipX.value ? 'back' : 'front'
|
||||||
const spriteId = characterHair?.sprite?.id
|
|
||||||
const direction = [0, 6].includes(rotation) ? 'back' : 'front'
|
|
||||||
|
|
||||||
return `${spriteId}-${direction}`
|
return `${hairSpriteId.value}-${direction}`
|
||||||
})
|
})
|
||||||
|
|
||||||
const isFlippedX = computed(() => [6, 4].includes(props.zoneCharacter.character.rotation ?? 0))
|
watch(
|
||||||
|
() => props.mapCharacter.character,
|
||||||
|
(newValue) => {
|
||||||
|
if (!image.value) return
|
||||||
|
image.value.setTexture(texture.value)
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
const imageProps = computed(() => {
|
onMounted(async () => {
|
||||||
// Get the current sprite action based on direction
|
if (!props.mapCharacter.character.characterType || !props.mapCharacter.character.characterHair) return
|
||||||
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 {
|
const characterTypeStorage = new CharacterTypeStorage()
|
||||||
depth: 1,
|
const characterHairStorage = new CharacterHairStorage()
|
||||||
originX: Number(spriteAction?.originX) ?? 0,
|
const spriteStorage = new SpriteStorage()
|
||||||
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)
|
const characterType = await characterTypeStorage.getById(props.mapCharacter.character.characterType!)
|
||||||
.then(() => {})
|
if (!characterType) return
|
||||||
.catch((error) => {
|
characterSpriteHeight.value = 100
|
||||||
console.error('Error loading texture:', error)
|
|
||||||
|
hairSpriteId.value = await characterHairStorage.getSpriteId(props.mapCharacter.character.characterHair)
|
||||||
|
if (!hairSpriteId.value) return
|
||||||
|
|
||||||
|
hairSprite.value = await spriteStorage.getById(hairSpriteId.value)
|
||||||
|
if (!hairSprite.value) return
|
||||||
|
|
||||||
|
await loadSpriteTextures(scene, hairSpriteId.value)
|
||||||
|
|
||||||
|
if (!image.value) return
|
||||||
|
|
||||||
|
image.value.setOrigin(0.5, 2.15)
|
||||||
|
image.value.setTexture(texture.value)
|
||||||
|
image.value.setSize(30, 40)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,33 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<Container ref="charChatContainer" :depth="999" :x="currentX" :y="currentY">
|
<Container ref="characterChatContainer">
|
||||||
<RoundRectangle @create="createChatBubble" :origin-x="0.5" :origin-y="7.5" :fillColor="0xffffff" :width="194" :height="21" :radius="20" />
|
<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' }" />
|
<Text @create="createChatText" :style="{ fontSize: 13, fontFamily: 'Arial', color: '#000' }" />
|
||||||
</Container>
|
</Container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ZoneCharacter } from '@/application/types'
|
import type { MapCharacter } from '@/application/types'
|
||||||
import { Container, refObj, RoundRectangle, Text, useGame } from 'phavuer'
|
import { Container, refObj, RoundRectangle, Text, useGame } from 'phavuer'
|
||||||
import { onMounted } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
zoneCharacter: ZoneCharacter
|
mapCharacter: MapCharacter
|
||||||
currentX: number
|
|
||||||
currentY: number
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const game = useGame()
|
const game = useGame()
|
||||||
const charChatContainer = refObj<Phaser.GameObjects.Container>()
|
const characterChatContainer = refObj<Phaser.GameObjects.Container>()
|
||||||
|
|
||||||
const createChatBubble = (container: Phaser.GameObjects.Container) => {
|
const createChatBubble = (container: Phaser.GameObjects.Container) => {
|
||||||
container.setName(`${props.zoneCharacter.character.name}_chatBubble`)
|
container.setName(`${props.mapCharacter.character.name}_chatBubble`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const createChatText = (text: Phaser.GameObjects.Text) => {
|
const createChatText = (text: Phaser.GameObjects.Text) => {
|
||||||
text.setName(`${props.zoneCharacter.character.name}_chatText`)
|
text.setName(`${props.mapCharacter.character.name}_chatText`)
|
||||||
text.setFontSize(13)
|
text.setFontSize(13)
|
||||||
text.setFontFamily('Arial')
|
text.setFontFamily('Arial')
|
||||||
text.setOrigin(0.5, 10.9)
|
text.setOrigin(0.5, 10.9)
|
||||||
|
text.setResolution(2)
|
||||||
|
|
||||||
// Fix text alignment on Windows and Android
|
// Fix text alignment on Windows and Android
|
||||||
if (game.device.os.windows || game.device.os.android) {
|
if (game.device.os.windows || game.device.os.android) {
|
||||||
@ -40,7 +39,7 @@ const createChatText = (text: Phaser.GameObjects.Text) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
charChatContainer.value!.setName(`${props.zoneCharacter.character!.name}_chatContainer`)
|
characterChatContainer.value!.setName(`${props.mapCharacter.character!.name}_chatContainer`)
|
||||||
charChatContainer.value!.setVisible(false)
|
characterChatContainer.value!.setVisible(false)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,19 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<Container :depth="999" :x="currentX" :y="currentY">
|
<Container :depth="999">
|
||||||
<Text @create="createNicknameText" :text="props.zoneCharacter.character.name" />
|
<Text @create="createNicknameText" :text="props.mapCharacter.character.name" />
|
||||||
<RoundRectangle :origin-x="0.5" :origin-y="18.5" :fillColor="0xffffff" :width="74" :height="6" :radius="5" />
|
<RoundRectangle :origin-x="0.5" :origin-y="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" />
|
<RoundRectangle :origin-x="0.5" :origin-y="36.4" :fillColor="0x00b3b3" :width="70" :height="3" :radius="5" />
|
||||||
</Container>
|
</Container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ZoneCharacter } from '@/application/types'
|
import type { MapCharacter } from '@/application/types'
|
||||||
import { Container, RoundRectangle, Text, useGame } from 'phavuer'
|
import { Container, RoundRectangle, Text, useGame } from 'phavuer'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
zoneCharacter: ZoneCharacter
|
mapCharacter: MapCharacter
|
||||||
currentX: number
|
|
||||||
currentY: number
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const game = useGame()
|
const game = useGame()
|
||||||
@ -22,6 +20,7 @@ const createNicknameText = (text: Phaser.GameObjects.Text) => {
|
|||||||
text.setFontSize(13)
|
text.setFontSize(13)
|
||||||
text.setFontFamily('Arial')
|
text.setFontFamily('Arial')
|
||||||
text.setOrigin(0.5, 9)
|
text.setOrigin(0.5, 9)
|
||||||
|
text.setResolution(2)
|
||||||
|
|
||||||
// Fix text alignment on Windows and Android
|
// Fix text alignment on Windows and Android
|
||||||
if (game.device.os.windows || game.device.os.android) {
|
if (game.device.os.windows || game.device.os.android) {
|
@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="absolute" v-if="gameStore.uiSettings.isCharacterProfileOpen" :style="modalStyle">
|
<div class="absolute" v-if="gameStore.uiSettings.isCharacterProfileOpen" :style="modalStyle">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<img src="/assets/ui-elements/profile-ui-box-outer.svg" class="absolute w-full h-full" />
|
<img src="/assets/ui-elements/profile-ui-box-outer.svg" class="absolute w-full h-full" alt="" />
|
||||||
<img src="/assets/ui-elements/profile-ui-box-inner.svg" class="absolute left-2 bottom-2 w-[calc(100%_-_16px)] h-[calc(100%_-_40px)]" />
|
<img src="/assets/ui-elements/profile-ui-box-inner.svg" class="absolute left-2 bottom-2 w-[calc(100%_-_16px)] h-[calc(100%_-_40px)]" alt="" />
|
||||||
<div @mousedown="startDrag" class="cursor-move px-5 py-2.5 flex justify-between items-center relative">
|
<div @mousedown="startDrag" class="cursor-move px-5 py-2.5 flex justify-between items-center relative">
|
||||||
<span class="text-xs text-white font-thin">Character Profile [Alt+C]</span>
|
<span class="text-xs text-white font-thin">Character Profile [Alt+C]</span>
|
||||||
<button @click="gameStore.uiSettings.isCharacterProfileOpen = false" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
|
<button @click="gameStore.uiSettings.isCharacterProfileOpen = false" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
|
||||||
<img alt="close" draggable="false" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" />
|
<img draggable="false" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" alt="Close button icon" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="py-4 px-6 flex flex-col gap-7 relative z-10">
|
<div class="py-4 px-6 flex flex-col gap-7 relative z-10">
|
||||||
@ -17,7 +17,7 @@
|
|||||||
<span class="text-xs">{{ gameStore.character?.experience }} / 18.600XP</span>
|
<span class="text-xs">{{ gameStore.character?.experience }} / 18.600XP</span>
|
||||||
</div>
|
</div>
|
||||||
<a class="hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured-small.svg')] bg-no-repeat block w-8 h-8 relative mx-[3px]">
|
<a class="hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured-small.svg')] bg-no-repeat block w-8 h-8 relative mx-[3px]">
|
||||||
<img class="hover:drop-shadow-default w-3.5 h-3.5 m-[9px] object-contain" draggable="false" src="/assets/icons/plus-green-icon.svg" />
|
<img class="hover:drop-shadow-default w-3.5 h-3.5 m-[9px] object-contain" draggable="false" src="/assets/icons/plus-green-icon.svg" alt="Plus button icon" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
@ -37,20 +37,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<img src="/assets/placeholders/inventory_player.png" class="w-8 h-auto" />
|
<img src="/assets/placeholders/inventory_player.png" class="w-8 h-auto" alt="Player character sprite" />
|
||||||
<div class="flex flex-col items-end gap-0.5">
|
<div class="flex flex-col items-end gap-0.5">
|
||||||
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
|
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
|
||||||
<img class="w-6 h-6 center-element" src="/assets/icons/profile/helmet.svg" />
|
<img class="w-6 h-6 center-element" src="/assets/icons/profile/helmet.svg" alt="Helmet icon" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
|
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
|
||||||
<img class="w-6 h-6 center-element" src="/assets/icons/profile/chestplate.svg" />
|
<img class="w-6 h-6 center-element" src="/assets/icons/profile/chestplate.svg" alt="Chestplate icon" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-0.5 items-end">
|
<div class="flex gap-0.5 items-end">
|
||||||
<div class="w-6 h-6 default-border rounded-sm bg-gray relative hover:bg-gray-600">
|
<div class="w-6 h-6 default-border rounded-sm bg-gray relative hover:bg-gray-600">
|
||||||
<img class="w-4 h-4 center-element" src="/assets/icons/profile/boots.svg" />
|
<img class="w-4 h-4 center-element" src="/assets/icons/profile/boots.svg" alt="Boots icon" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
|
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
|
||||||
<img class="w-6 h-6 center-element" src="/assets/icons/profile/legs.svg" />
|
<img class="w-6 h-6 center-element" src="/assets/icons/profile/legs.svg" alt="Legs icon" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -119,111 +119,44 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
|
||||||
let startX = 0
|
const width = ref(286)
|
||||||
let startY = 0
|
const height = ref(483)
|
||||||
let initialX = 0
|
const x = ref(window.innerWidth / 2 - 143)
|
||||||
let initialY = 0
|
const y = ref(window.innerHeight / 2 - 241)
|
||||||
let modalPositionX = 0
|
|
||||||
let modalPositionY = 0
|
|
||||||
let modalWidth = 286
|
|
||||||
let modalHeight = 483
|
|
||||||
|
|
||||||
const width = ref(modalWidth)
|
|
||||||
const height = ref(modalHeight)
|
|
||||||
const x = ref(0)
|
|
||||||
const y = ref(0)
|
|
||||||
const isDragging = ref(false)
|
const isDragging = ref(false)
|
||||||
|
|
||||||
const modalStyle = computed(() => ({
|
const modalStyle = computed(() => ({
|
||||||
top: `${y.value}px`,
|
top: `${y.value}px`,
|
||||||
left: `${x.value}px`,
|
left: `${x.value}px`,
|
||||||
width: `${width.value}px`,
|
width: `${width.value}px`,
|
||||||
height: `${height.value}px`,
|
height: `${height.value}px`
|
||||||
maxWidth: '100vw',
|
|
||||||
maxHeight: '100vh'
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
function startDrag(event: MouseEvent) {
|
function startDrag(event: MouseEvent) {
|
||||||
isDragging.value = true
|
isDragging.value = true
|
||||||
startX = event.clientX
|
const startX = event.clientX - x.value
|
||||||
startY = event.clientY
|
const startY = event.clientY - y.value
|
||||||
initialX = x.value
|
|
||||||
initialY = y.value
|
|
||||||
event.preventDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
function drag(event: MouseEvent) {
|
function drag(event: MouseEvent) {
|
||||||
if (!isDragging.value) return
|
if (!isDragging.value) return
|
||||||
const dx = event.clientX - startX
|
x.value = event.clientX - startX
|
||||||
const dy = event.clientY - startY
|
y.value = event.clientY - startY
|
||||||
x.value = initialX + dx
|
|
||||||
y.value = initialY + dy
|
|
||||||
adjustPosition()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopDrag() {
|
function stopDrag() {
|
||||||
isDragging.value = false
|
isDragging.value = false
|
||||||
|
removeEventListener('mousemove', drag)
|
||||||
|
removeEventListener('mouseup', stopDrag)
|
||||||
}
|
}
|
||||||
|
|
||||||
function adjustPosition() {
|
addEventListener('mousemove', drag)
|
||||||
x.value = Math.min(x.value, window.innerWidth - width.value)
|
addEventListener('mouseup', stopDrag)
|
||||||
y.value = Math.min(y.value, window.innerHeight - height.value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializePosition() {
|
|
||||||
width.value = Math.min(modalWidth, window.innerWidth)
|
|
||||||
height.value = Math.min(modalHeight, window.innerHeight)
|
|
||||||
if (modalPositionX !== 0 && modalPositionY !== 0) {
|
|
||||||
x.value = modalPositionX
|
|
||||||
y.value = modalPositionY
|
|
||||||
} else {
|
|
||||||
x.value = (window.innerWidth - width.value) / 2
|
|
||||||
y.value = (window.innerHeight - height.value) / 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => gameStore.uiSettings.isCharacterProfileOpen,
|
|
||||||
(value) => {
|
|
||||||
gameStore.uiSettings.isCharacterProfileOpen = value
|
|
||||||
if (value) {
|
|
||||||
initializePosition()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => modalWidth,
|
|
||||||
(value) => {
|
|
||||||
width.value = Math.min(value, window.innerWidth)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => modalHeight,
|
|
||||||
(value) => {
|
|
||||||
height.value = Math.min(value, window.innerHeight)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => modalPositionX,
|
|
||||||
(value) => {
|
|
||||||
x.value = value
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => modalPositionY,
|
|
||||||
(value) => {
|
|
||||||
y.value = value
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function keyPress(event: KeyboardEvent) {
|
function keyPress(event: KeyboardEvent) {
|
||||||
if (event.altKey && event.key === 'c') {
|
if (event.altKey && event.key === 'c') {
|
||||||
gameStore.toggleCharacterProfile()
|
gameStore.toggleCharacterProfile()
|
||||||
@ -232,14 +165,9 @@ function keyPress(event: KeyboardEvent) {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
addEventListener('keydown', keyPress)
|
addEventListener('keydown', keyPress)
|
||||||
addEventListener('mousemove', drag)
|
|
||||||
addEventListener('mouseup', stopDrag)
|
|
||||||
initializePosition()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
removeEventListener('keydown', keyPress)
|
removeEventListener('keydown', keyPress)
|
||||||
removeEventListener('mousemove', drag)
|
|
||||||
removeEventListener('mouseup', stopDrag)
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
@ -2,12 +2,12 @@
|
|||||||
<div class="w-full md:min-w-[350px] max-w-[750px] flex flex-col absolute left-1/2 -translate-x-1/2 bottom-5">
|
<div class="w-full md:min-w-[350px] max-w-[750px] flex flex-col absolute left-1/2 -translate-x-1/2 bottom-5">
|
||||||
<div ref="chatWindow" class="w-full overflow-auto h-32 mb-5 bg-gray rounded-md border-2 border-solid border-gray-500 text-gray-300" v-show="gameStore.uiSettings.isChatOpen">
|
<div ref="chatWindow" class="w-full overflow-auto h-32 mb-5 bg-gray rounded-md border-2 border-solid border-gray-500 text-gray-300" v-show="gameStore.uiSettings.isChatOpen">
|
||||||
<div v-for="message in chats" class="flex-col py-2 items-center p-3">
|
<div v-for="message in chats" class="flex-col py-2 items-center p-3">
|
||||||
<span class="text-ellipsis overflow-hidden whitespace-nowrap text-sm" :class="{ 'text-cyan-300': gameStore.character?.role == 'gm' }">{{ message.character.name }}</span>
|
<span class="text-ellipsis overflow-hidden whitespace-nowrap text-sm" :class="{ 'text-cyan-300': gameStore.character?.role == 'gm' }">{{ message.character }}</span>
|
||||||
<p class="text-gray-50 m-0">{{ message.message }}</p>
|
<p class="text-gray-50 m-0">{{ message.message }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-96 mx-auto relative">
|
<div class="w-96 mx-auto relative">
|
||||||
<img src="/assets/icons/ingameUI/chat-icon.svg" class="absolute top-1/2 -translate-y-1/2 left-2.5 h-4 w-4 opacity-50" />
|
<img src="/assets/icons/ingameUI/chat-icon.svg" class="absolute top-1/2 -translate-y-1/2 left-2.5 h-4 w-4 opacity-50" alt="" />
|
||||||
<input
|
<input
|
||||||
class="w-[332px] h-8 rounded-sm text-xs font-default pl-8 pr-4 py-0 bg-gray-600 border-2 border-solid border-gray-500 text-gray-300 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover focus:outline-none focus:ring-0 focus:border-cyan-800"
|
class="w-[332px] h-8 rounded-sm text-xs font-default pl-8 pr-4 py-0 bg-gray-600 border-2 border-solid border-gray-500 text-gray-300 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover focus:outline-none focus:ring-0 focus:border-cyan-800"
|
||||||
placeholder="Type something..."
|
placeholder="Type something..."
|
||||||
@ -21,19 +21,19 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Chat } from '@/application/types'
|
import { SocketEvent } from '@/application/enums'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useZoneStore } from '@/stores/zoneStore'
|
import { useMapStore } from '@/stores/mapStore'
|
||||||
import { onClickOutside, useFocus } from '@vueuse/core'
|
import { onClickOutside, useFocus } from '@vueuse/core'
|
||||||
import { useScene } from 'phavuer'
|
import { useScene } from 'phavuer'
|
||||||
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const zoneStore = useZoneStore()
|
|
||||||
|
|
||||||
const message = ref('')
|
const message = ref('')
|
||||||
const chats = ref([] as Chat[])
|
const chats = ref<{ character: string; message: string }[]>([])
|
||||||
const chatWindow = ref<HTMLElement | null>(null)
|
const chatWindow = ref<HTMLElement | null>(null)
|
||||||
const chatInput = ref<HTMLElement | null>(null)
|
const chatInput = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ function unfocusChat(event: Event, targetElement: HTMLElement) {
|
|||||||
|
|
||||||
const sendMessage = () => {
|
const sendMessage = () => {
|
||||||
if (!message.value.trim()) return
|
if (!message.value.trim()) return
|
||||||
gameStore.connection?.emit('chat:message', { message: message.value }, (response: boolean) => {})
|
socketManager.emit(SocketEvent.CHAT_MESSAGE, { message: message.value }, (response: boolean) => {})
|
||||||
message.value = ''
|
message.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,18 +79,30 @@ const scrollToBottom = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection!.on('chat:message', (data: Chat) => {
|
socketManager.on(SocketEvent.CHAT_MESSAGE, (data: { character: string; message: string }) => {
|
||||||
chats.value.push(data)
|
if (!data.character || !data.message) return
|
||||||
|
|
||||||
|
chats.value.push({ character: data.character, message: data.message })
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
|
|
||||||
if (!zoneStore.characterLoaded) return
|
const characterContainer = scene.children.getByName(data.character) as Phaser.GameObjects.Container
|
||||||
|
if (!characterContainer) {
|
||||||
|
console.log('No character container found')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const charChatContainer = scene.children.getByName(data.character.name + '_chatContainer') as Phaser.GameObjects.Container
|
const characterChatContainer = characterContainer.getByName(data.character + '_chatContainer') as Phaser.GameObjects.Container
|
||||||
if (!charChatContainer) return
|
if (!characterChatContainer) {
|
||||||
|
console.log('No character chat container found')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const chatBubble = charChatContainer.getByName(data.character.name + '_chatBubble') as Phaser.GameObjects.Container
|
const chatBubble = characterChatContainer.getByName(data.character + '_chatBubble') as Phaser.GameObjects.Container
|
||||||
const chatText = charChatContainer.getByName(data.character.name + '_chatText') as Phaser.GameObjects.Text
|
const chatText = characterChatContainer.getByName(data.character + '_chatText') as Phaser.GameObjects.Text
|
||||||
if (!chatText || !chatBubble) return
|
if (!chatText || !chatBubble) {
|
||||||
|
console.log('No chat text or bubble found')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
function calculateTextWidth(text: string, font: string, fontSize: number): number {
|
function calculateTextWidth(text: string, font: string, fontSize: number): number {
|
||||||
// Create a canvas element
|
// Create a canvas element
|
||||||
@ -115,24 +127,24 @@ gameStore.connection!.on('chat:message', (data: Chat) => {
|
|||||||
// setText but with max. char limit of 90
|
// setText but with max. char limit of 90
|
||||||
chatText.setText(data.message.substring(0, 90))
|
chatText.setText(data.message.substring(0, 90))
|
||||||
|
|
||||||
charChatContainer.setVisible(true)
|
characterChatContainer.setVisible(true)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hide chat bubble after a few seconds
|
* Hide chat bubble after a few seconds
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Clear any existing hide timer
|
// Clear any existing hide timer
|
||||||
if (charChatContainer.getData('hideTimer')) {
|
if (characterChatContainer.getData('hideTimer')) {
|
||||||
clearTimeout(charChatContainer.getData('hideTimer'))
|
clearTimeout(characterChatContainer.getData('hideTimer'))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set a new hide timer
|
// Set a new hide timer
|
||||||
const hideTimer = setTimeout(() => {
|
const hideTimer = setTimeout(() => {
|
||||||
charChatContainer.setVisible(false)
|
characterChatContainer.setVisible(false)
|
||||||
}, 3000)
|
}, 3000)
|
||||||
|
|
||||||
// Store the timer on the container itself
|
// Store the timer on the container itself
|
||||||
charChatContainer.setData('hideTimer', hideTimer)
|
characterChatContainer.setData('hideTimer', hideTimer)
|
||||||
})
|
})
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
|
|
||||||
@ -141,7 +153,7 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
gameStore.connection?.off('chat:message')
|
socketManager.off(SocketEvent.CHAT_MESSAGE)
|
||||||
removeEventListener('keydown', focusChat)
|
removeEventListener('keydown', focusChat)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
21
src/components/game/gui/Clock.vue
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<div class="absolute top-0 right-4 hidden lg:block" v-if="gameStore.world.date && typeof gameStore.world.date === 'object'">
|
||||||
|
<p class="text-white text-lg">
|
||||||
|
{{ useDateFormat(gameStore.world.date, 'YYYY/MM/DD HH:mm') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { SocketEvent } from '@/application/enums'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useDateFormat } from '@vueuse/core'
|
||||||
|
import { onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
socketManager.off(SocketEvent.DATE)
|
||||||
|
})
|
||||||
|
</script>
|
@ -6,7 +6,7 @@
|
|||||||
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
<a class="group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured.svg')] bg-no-repeat block w-[42px] h-[42px] relative">
|
<a class="group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured.svg')] bg-no-repeat block w-[42px] h-[42px] relative">
|
||||||
<img class="group-hover:drop-shadow-default w-6 h-5 mx-[9px] my-[11px] object-contain" draggable="false" src="/assets/icons/ingameUI/menu-icon.svg" />
|
<img class="group-hover:drop-shadow-default w-6 h-5 mx-[9px] my-[11px] object-contain" draggable="false" src="/assets/icons/ingameUI/menu-icon.svg" alt="Menu button icon" />
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="menu-item group relative" @click="gameStore.toggleCharacterProfile">
|
<li class="menu-item group relative" @click="gameStore.toggleCharacterProfile">
|
||||||
@ -15,7 +15,7 @@
|
|||||||
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
<a class="group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured.svg')] bg-no-repeat block w-[42px] h-[42px] relative">
|
<a class="group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured.svg')] bg-no-repeat block w-[42px] h-[42px] relative">
|
||||||
<img class="group-hover:drop-shadow-default w-8 h-8 m-[5px] object-contain" draggable="false" src="/assets/placeholders/head.png" />
|
<img class="group-hover:drop-shadow-default w-8 h-8 m-[5px] object-contain" draggable="false" src="/assets/placeholders/head.png" alt="User profile button icon" />
|
||||||
<p class="absolute bottom-0 -right-1.5 m-0 max-w-4 font-ui z-10 text-white text-[12px] leading-[6px] drop-shadow-pixel"><span class="font-ui text-white text-[8px] ml-0.5">LVL</span> {{ characterLevel }}</p>
|
<p class="absolute bottom-0 -right-1.5 m-0 max-w-4 font-ui z-10 text-white text-[12px] leading-[6px] drop-shadow-pixel"><span class="font-ui text-white text-[8px] ml-0.5">LVL</span> {{ characterLevel }}</p>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@ -25,7 +25,7 @@
|
|||||||
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
<a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border-2 border-solid rounded border-gray-500 block w-[34px] h-[31px]">
|
<a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border-2 border-solid rounded border-gray-500 block w-[34px] h-[31px]">
|
||||||
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/chat-icon.svg" />
|
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/chat-icon.svg" alt="Open chat button icon" />
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="menu-item group relative">
|
<li class="menu-item group relative">
|
||||||
@ -34,7 +34,7 @@
|
|||||||
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
<a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border-2 border-solid rounded border-gray-500 block w-[34px] h-[31px]">
|
<a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border-2 border-solid rounded border-gray-500 block w-[34px] h-[31px]">
|
||||||
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/map-icon.svg" />
|
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/map-icon.svg" alt="World map button icon" />
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="menu-item group relative">
|
<li class="menu-item group relative">
|
||||||
@ -43,7 +43,7 @@
|
|||||||
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
<div class="group-hover:block absolute -left-2 bg-gray-500 h-3.5 w-2 [clip-path:polygon(100%_0,_0_50%,_100%_100%)] top-1/2 -translate-y-1/2 hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
<a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border-2 border-solid rounded border-gray-500 block w-[34px] h-[31px]">
|
<a class="group-hover:bg-gray-800 bg-gray-900 group-hover:cursor-pointer border-2 border-solid rounded border-gray-500 block w-[34px] h-[31px]">
|
||||||
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/socials-icon.svg" />
|
<img class="group-hover:drop-shadow-default w-6 h-6 m-[5px] object-contain" draggable="false" src="/assets/icons/ingameUI/socials-icon.svg" alt="Users button icon" />
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
@ -5,12 +5,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="absolute -bottom-3 left-1/2 -translate-x-1/2 flex gap-1">
|
<div class="absolute -bottom-3 left-1/2 -translate-x-1/2 flex gap-1">
|
||||||
<button class="w-6 h-6 relative p-0">
|
<button class="w-6 h-6 relative p-0">
|
||||||
<img class="w-3 h-3 center-element" src="/assets/icons/plus-icon.svg" />
|
<img class="w-3 h-3 center-element" src="/assets/icons/plus-icon.svg" alt="Zoom-in button icon" />
|
||||||
<img class="w-full h-full" src="/assets/ui-elements/button-ui-box-textured.svg" />
|
<img class="w-full h-full" src="/assets/ui-elements/button-ui-box-textured.svg" alt="" />
|
||||||
</button>
|
</button>
|
||||||
<button class="w-6 h-6 relative p-0">
|
<button class="w-6 h-6 relative p-0">
|
||||||
<img class="w-3 h-3 center-element" src="/assets/icons/minus-icon.svg" />
|
<img class="w-3 h-3 center-element" src="/assets/icons/minus-icon.svg" alt="Zoom-out button icon" />
|
||||||
<img class="w-full h-full" src="/assets/ui-elements/button-ui-box-textured.svg" />
|
<img class="w-full h-full" src="/assets/ui-elements/button-ui-box-textured.svg" alt="" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
49
src/components/game/map/Characters.vue
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<Character v-for="item in mapStore.characters" :key="item.character.id" :tileMap :mapCharacter="item" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { SocketEvent } from '@/application/enums'
|
||||||
|
import type { MapCharacter, UUID } from '@/application/types'
|
||||||
|
import Character from '@/components/game/character/Character.vue'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useMapStore } from '@/stores/mapStore'
|
||||||
|
import { onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const mapStore = useMapStore()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
tileMap: Phaser.Tilemaps.Tilemap
|
||||||
|
}>()
|
||||||
|
|
||||||
|
socketManager.on(SocketEvent.MAP_CHARACTER_JOIN, (data: MapCharacter) => {
|
||||||
|
mapStore.addCharacter(data)
|
||||||
|
})
|
||||||
|
|
||||||
|
socketManager.on(SocketEvent.MAP_CHARACTER_LEAVE, (characterId: UUID) => {
|
||||||
|
mapStore.removeCharacter(characterId)
|
||||||
|
})
|
||||||
|
|
||||||
|
socketManager.on(SocketEvent.MAP_CHARACTER_MOVE, ([characterId, posX, posY, rot, isMoving]: [UUID, number, number, number, boolean]) => {
|
||||||
|
mapStore.updateCharacterPosition([characterId, posX, posY, rot, isMoving])
|
||||||
|
|
||||||
|
if (characterId === gameStore.character?.id) {
|
||||||
|
gameStore.character!.positionX = posX
|
||||||
|
gameStore.character!.positionY = posY
|
||||||
|
gameStore.character!.rotation = rot
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
socketManager.on(SocketEvent.MAP_CHARACTER_ATTACK, (characterId: UUID) => {
|
||||||
|
mapStore.updateCharacterProperty(characterId, 'isAttacking', true)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
socketManager.off(SocketEvent.MAP_CHARACTER_ATTACK)
|
||||||
|
socketManager.off(SocketEvent.MAP_CHARACTER_MOVE)
|
||||||
|
socketManager.off(SocketEvent.MAP_CHARACTER_JOIN)
|
||||||
|
socketManager.off(SocketEvent.MAP_CHARACTER_LEAVE)
|
||||||
|
})
|
||||||
|
</script>
|
71
src/components/game/map/Map.vue
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<MapTiles v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer />
|
||||||
|
<PlacedMapObjects v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer />
|
||||||
|
<Characters v-if="tileMap && mapStore.characters" :tileMap />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { SocketEvent } from '@/application/enums'
|
||||||
|
import type { mapLoadData } from '@/application/types'
|
||||||
|
import { unduplicateArray } from '@/application/utilities'
|
||||||
|
import Characters from '@/components/game/map/Characters.vue'
|
||||||
|
import MapTiles from '@/components/game/map/MapTiles.vue'
|
||||||
|
import PlacedMapObjects from '@/components/game/map/PlacedMapObjects.vue'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
|
import { createTileLayer, createTileMap, loadTileTexturesFromMapTileArray } from '@/services/mapService'
|
||||||
|
import { MapStorage } from '@/storage/storages'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useMapStore } from '@/stores/mapStore'
|
||||||
|
import { useScene } from 'phavuer'
|
||||||
|
import { onMounted, onUnmounted, shallowRef, watch } from 'vue'
|
||||||
|
|
||||||
|
const scene = useScene()
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const mapStore = useMapStore()
|
||||||
|
|
||||||
|
const mapStorage = new MapStorage()
|
||||||
|
|
||||||
|
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
|
||||||
|
const tileMapLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
socketManager.on(SocketEvent.MAP_CHARACTER_TELEPORT, (data: mapLoadData) => {
|
||||||
|
mapStore.setMapId(data.mapId)
|
||||||
|
mapStore.setCharacters(data.characters)
|
||||||
|
})
|
||||||
|
|
||||||
|
async function initialize() {
|
||||||
|
if (!mapStore.mapId) return
|
||||||
|
|
||||||
|
const map = await mapStorage.getById(mapStore.mapId)
|
||||||
|
if (!map) return
|
||||||
|
|
||||||
|
await loadTileTexturesFromMapTileArray(mapStore.mapId, scene)
|
||||||
|
|
||||||
|
tileMap.value = createTileMap(scene, map)
|
||||||
|
tileMapLayer.value = createTileLayer(tileMap.value, unduplicateArray(map.tiles))
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => mapStore.mapId,
|
||||||
|
async () => {
|
||||||
|
await initialize()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!mapStore.mapId) return
|
||||||
|
await initialize()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (tileMap.value) {
|
||||||
|
tileMap.value.destroyLayer('tiles')
|
||||||
|
tileMap.value.removeAllLayers()
|
||||||
|
tileMap.value.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
socketManager.off(SocketEvent.MAP_CHARACTER_TELEPORT)
|
||||||
|
})
|
||||||
|
</script>
|
32
src/components/game/map/MapTiles.vue
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<Controls v-if="tileMapLayer" :layer="tileMapLayer" :depth="0" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Controls from '@/components/utilities/Controls.vue'
|
||||||
|
import { loadTileTexturesFromMapTileArray, placeTiles } from '@/services/mapService'
|
||||||
|
import { MapStorage } from '@/storage/storages'
|
||||||
|
import { useMapStore } from '@/stores/mapStore'
|
||||||
|
import { useScene } from 'phavuer'
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
|
||||||
|
const scene = useScene()
|
||||||
|
const mapStore = useMapStore()
|
||||||
|
const mapStorage = new MapStorage()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
tileMap: Phaser.Tilemaps.Tilemap
|
||||||
|
tileMapLayer: Phaser.Tilemaps.TilemapLayer
|
||||||
|
}>()
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!mapStore.mapId) return
|
||||||
|
|
||||||
|
const map = await mapStorage.getById(mapStore.mapId)
|
||||||
|
if (!map) return
|
||||||
|
|
||||||
|
await loadTileTexturesFromMapTileArray(mapStore.mapId, scene)
|
||||||
|
|
||||||
|
placeTiles(props.tileMap, props.tileMapLayer, map.tiles)
|
||||||
|
})
|
||||||
|
</script>
|
31
src/components/game/map/PlacedMapObjects.vue
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<PlacedMapObject v-for="placedMapObject in items" :tileMap :tileMapLayer :placedMapObject />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { PlacedMapObject as PlacedMapObjectT } from '@/application/types'
|
||||||
|
import PlacedMapObject from '@/components/game/map/partials/PlacedMapObject.vue'
|
||||||
|
import { MapStorage } from '@/storage/storages'
|
||||||
|
import { useMapStore } from '@/stores/mapStore'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
tileMap: Phaser.Tilemaps.Tilemap
|
||||||
|
tileMapLayer: TilemapLayer
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const mapStore = useMapStore()
|
||||||
|
const mapStorage = new MapStorage()
|
||||||
|
const items = ref<PlacedMapObjectT[]>([])
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!mapStore.mapId) return
|
||||||
|
|
||||||
|
const map = await mapStorage.getById(mapStore.mapId)
|
||||||
|
if (!map) return
|
||||||
|
|
||||||
|
items.value = map.placedMapObjects
|
||||||
|
})
|
||||||
|
</script>
|
102
src/components/game/map/partials/ImageGroup.vue
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<template>
|
||||||
|
<Zone :depth="baseDepth" :origin-x="mapObj?.originX" :origin-y="mapObj?.originY" :width="mapObj?.frameWidth" :height="mapObj?.frameHeight" :x="x" :y="y" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { MapObject, PlacedMapObject } from '@/application/types'
|
||||||
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
|
import { calculateIsometricDepth } from '@/services/mapService'
|
||||||
|
import { onPreUpdate, useScene, Zone } from 'phavuer'
|
||||||
|
import { computed, onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
obj?: PlacedMapObject
|
||||||
|
mapObj?: MapObject
|
||||||
|
x?: number
|
||||||
|
y?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const mapEditor = useMapEditorComposable()
|
||||||
|
const scene = useScene()
|
||||||
|
|
||||||
|
const group = scene.add.group()
|
||||||
|
const partitionPoints = computed(() => {
|
||||||
|
if (!props.mapObj?.frameWidth || !props.mapObj?.depthOffsets.length) return []
|
||||||
|
|
||||||
|
const sliceCount = props.mapObj.depthOffsets.length
|
||||||
|
return Array.from({ length: sliceCount + 1 }, (_, i) => i * (props.mapObj!.frameWidth / sliceCount))
|
||||||
|
})
|
||||||
|
|
||||||
|
let baseDepth = 0
|
||||||
|
|
||||||
|
const createImagePartition = (startX: number, endX: number, depthOffset: number): void => {
|
||||||
|
if (!props.mapObj?.id) return
|
||||||
|
|
||||||
|
const img = scene.add.image(0, 0, props.mapObj.id)
|
||||||
|
img.setOrigin(props.mapObj.originX, props.mapObj.originY)
|
||||||
|
img.setCrop(startX, 0, endX, props.mapObj.frameHeight)
|
||||||
|
img.setDepth(baseDepth + depthOffset)
|
||||||
|
group.add(img)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateGroupProperties = (): void => {
|
||||||
|
if (!props.obj || !props.x || !props.y) return
|
||||||
|
|
||||||
|
const isMoving = mapEditor.movingPlacedObject.value?.id === props.obj.id
|
||||||
|
const isSelected = mapEditor.selectedMapObject.value?.id === props.obj.id
|
||||||
|
const isPlacedSelected = mapEditor.selectedPlacedObject.value?.id === props.obj.id
|
||||||
|
|
||||||
|
baseDepth = calculateIsometricDepth(props.obj.positionX, props.obj.positionY)
|
||||||
|
|
||||||
|
group.setXY(props.x, props.y)
|
||||||
|
group.setAlpha(isMoving || isSelected ? 0.5 : 1)
|
||||||
|
group.setTint(isPlacedSelected ? 0x00ff00 : 0xffffff)
|
||||||
|
group.setDepth(baseDepth)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateImageProperties = (): void => {
|
||||||
|
const orderedImages = group.getChildren() as Phaser.GameObjects.Image[]
|
||||||
|
|
||||||
|
orderedImages.forEach((image, index) => {
|
||||||
|
if (!props.obj || !props.mapObj || !props.x) return
|
||||||
|
|
||||||
|
image.flipX = props.obj.isRotated
|
||||||
|
|
||||||
|
if (props.obj.isRotated) {
|
||||||
|
const offsetNum = props.mapObj.depthOffsets.length
|
||||||
|
const xOffset = props.mapObj.frameWidth / offsetNum
|
||||||
|
image.x = props.x + (index < offsetNum / 2 ? -xOffset : xOffset)
|
||||||
|
image.setDepth(baseDepth - props.mapObj.depthOffsets[index])
|
||||||
|
} else {
|
||||||
|
image.x = props.x
|
||||||
|
image.setDepth(baseDepth + props.mapObj.depthOffsets[index])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onPreUpdate(() => {
|
||||||
|
updateGroupProperties()
|
||||||
|
updateImageProperties()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Initial setup
|
||||||
|
const initializeGroup = (): void => {
|
||||||
|
if (!props.mapObj || !props.x || !props.y || !props.obj) return
|
||||||
|
|
||||||
|
baseDepth = calculateIsometricDepth(props.obj.positionX, props.obj.positionY)
|
||||||
|
group.setXY(props.x, props.y)
|
||||||
|
group.setOrigin(props.mapObj.originX, props.mapObj.originY)
|
||||||
|
|
||||||
|
const points = partitionPoints.value
|
||||||
|
for (let i = 0; i < points.length - 1; i++) {
|
||||||
|
createImagePartition(points[i], points[i + 1], props.mapObj.depthOffsets[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeGroup()
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
group.destroy(true, true)
|
||||||
|
})
|
||||||
|
</script>
|
70
src/components/game/map/partials/PlacedMapObject.vue
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<template>
|
||||||
|
<ImageGroup v-bind="groupProps" v-if="mapObject && gameStore.isTextureLoaded(props.placedMapObject.mapObject as string)" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { MapObject, PlacedMapObject } from '@/application/types'
|
||||||
|
import ImageGroup from '@/components/game/map/partials/ImageGroup.vue'
|
||||||
|
import { loadMapObjectTextures, tileToWorldXY } from '@/services/mapService'
|
||||||
|
import { MapObjectStorage } from '@/storage/storages'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useScene } from 'phavuer'
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
import Tilemap = Phaser.Tilemaps.Tilemap
|
||||||
|
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
placedMapObject: PlacedMapObject
|
||||||
|
tileMap: Tilemap
|
||||||
|
tileMapLayer: TilemapLayer
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const scene = useScene()
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
|
||||||
|
const mapObject = ref<MapObject>()
|
||||||
|
|
||||||
|
const groupProps = computed(() => ({
|
||||||
|
...calculateObjectPlacement(props.placedMapObject),
|
||||||
|
mapObj: mapObject.value,
|
||||||
|
obj: props.placedMapObject
|
||||||
|
}))
|
||||||
|
|
||||||
|
async function initialize() {
|
||||||
|
if (!props.placedMapObject.mapObject) return
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if mapObject is an string or object, if its an object we assume its a mapObject and change it to a string
|
||||||
|
* We do this because this component is shared with the map editor, which gets sent the mapObject as an object by the server
|
||||||
|
*/
|
||||||
|
if (typeof props.placedMapObject.mapObject === 'object') {
|
||||||
|
// @ts-ignore
|
||||||
|
props.placedMapObject.mapObject = props.placedMapObject.mapObject.id
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapObjectStorage = new MapObjectStorage()
|
||||||
|
const _mapObject = await mapObjectStorage.getById(props.placedMapObject.mapObject as string)
|
||||||
|
if (!_mapObject) return
|
||||||
|
|
||||||
|
console.log(_mapObject)
|
||||||
|
|
||||||
|
mapObject.value = _mapObject
|
||||||
|
|
||||||
|
await loadMapObjectTextures([_mapObject], scene)
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateObjectPlacement(mapObj: PlacedMapObject): { x: number; y: number } {
|
||||||
|
let position = tileToWorldXY(props.tileMapLayer, mapObj.positionX, mapObj.positionY)
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: position.worldPositionX,
|
||||||
|
y: position.worldPositionY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await initialize()
|
||||||
|
})
|
||||||
|
</script>
|
@ -1,14 +0,0 @@
|
|||||||
<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>
|
|
@ -1,50 +0,0 @@
|
|||||||
<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>
|
|
@ -1,14 +0,0 @@
|
|||||||
<template>
|
|
||||||
<ZoneObject v-for="zoneObject in zoneStore.zone?.zoneObjects" :tilemap="tilemap" :zoneObject />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import ZoneObject from '@/components/game/zone/partials/ZoneObject.vue'
|
|
||||||
import { useZoneStore } from '@/stores/zoneStore'
|
|
||||||
|
|
||||||
const zoneStore = useZoneStore()
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
tilemap: Phaser.Tilemaps.Tilemap
|
|
||||||
}>()
|
|
||||||
</script>
|
|
@ -1,69 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Controls :layer="tileLayer" :depth="0" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import config from '@/application/config'
|
|
||||||
import { unduplicateArray } from '@/application/utilities'
|
|
||||||
import Controls from '@/components/utilities/Controls.vue'
|
|
||||||
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 scene = useScene()
|
|
||||||
const zoneStore = useZoneStore()
|
|
||||||
const tileMap = createTileMap()
|
|
||||||
const tileLayer = createTileLayer()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Tilemap is a container for Tilemap data.
|
|
||||||
* This isn't a display object, rather, it holds data about the map and allows you to add tilesets and tilemap layers to it.
|
|
||||||
* A map can have one or more tilemap layers, which are the display objects that actually render the tiles.
|
|
||||||
*/
|
|
||||||
function createTileMap() {
|
|
||||||
const zoneData = new Phaser.Tilemaps.MapData({
|
|
||||||
width: zoneStore.zone?.width,
|
|
||||||
height: zoneStore.zone?.height,
|
|
||||||
tileWidth: config.tile_size.x,
|
|
||||||
tileHeight: config.tile_size.y,
|
|
||||||
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
|
|
||||||
format: Phaser.Tilemaps.Formats.ARRAY_2D
|
|
||||||
})
|
|
||||||
|
|
||||||
const newTileMap = new Phaser.Tilemaps.Tilemap(scene, zoneData)
|
|
||||||
emit('tileMap:create', newTileMap)
|
|
||||||
|
|
||||||
return newTileMap
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Tileset is a combination of a single image containing the tiles and a container for data about each tile.
|
|
||||||
*/
|
|
||||||
function createTileLayer() {
|
|
||||||
const tilesArray = unduplicateArray(FlattenZoneArray(zoneStore.zone?.tiles ?? []))
|
|
||||||
|
|
||||||
const tilesetImages = Array.from(tilesArray).map((tile: any, index: number) => {
|
|
||||||
return tileMap.addTilesetImage(tile, tile, config.tile_size.x, config.tile_size.y, 1, 2, index + 1, { x: 0, y: -config.tile_size.y })
|
|
||||||
}) as any
|
|
||||||
|
|
||||||
// Add blank tile
|
|
||||||
tilesetImages.push(tileMap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.x, config.tile_size.y, 1, 2, 0, { x: 0, y: -config.tile_size.y }))
|
|
||||||
const layer = tileMap.createBlankLayer('tiles', tilesetImages, 0, config.tile_size.y) as Phaser.Tilemaps.TilemapLayer
|
|
||||||
|
|
||||||
layer.setDepth(0)
|
|
||||||
layer.setCullPadding(2, 2)
|
|
||||||
|
|
||||||
return layer
|
|
||||||
}
|
|
||||||
|
|
||||||
setLayerTiles(tileMap, tileLayer, zoneStore.zone?.tiles)
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
tileMap.destroyLayer('tiles')
|
|
||||||
tileMap.removeAllLayers()
|
|
||||||
tileMap.destroy()
|
|
||||||
})
|
|
||||||
</script>
|
|
@ -1,41 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Image v-if="gameStore.getLoadedAsset(props.zoneObject.object.id)" v-bind="imageProps" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { AssetDataT, ZoneObject } from '@/application/types'
|
|
||||||
import { loadTexture } from '@/composables/gameComposable'
|
|
||||||
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
|
|
||||||
zoneObject: ZoneObject
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const scene = useScene()
|
|
||||||
|
|
||||||
const imageProps = computed(() => ({
|
|
||||||
depth: calculateIsometricDepth(props.zoneObject.positionX, props.zoneObject.positionY, props.zoneObject.object.frameWidth, props.zoneObject.object.frameHeight),
|
|
||||||
x: tileToWorldX(props.tilemap, props.zoneObject.positionX, props.zoneObject.positionY),
|
|
||||||
y: tileToWorldY(props.tilemap, props.zoneObject.positionX, props.zoneObject.positionY),
|
|
||||||
flipX: props.zoneObject.isRotated,
|
|
||||||
texture: props.zoneObject.object.id,
|
|
||||||
originY: Number(props.zoneObject.object.originX),
|
|
||||||
originX: Number(props.zoneObject.object.originY)
|
|
||||||
}))
|
|
||||||
|
|
||||||
loadTexture(scene, {
|
|
||||||
key: props.zoneObject.object.id,
|
|
||||||
data: '/assets/objects/' + props.zoneObject.object.id + '.png',
|
|
||||||
group: 'objects',
|
|
||||||
updatedAt: props.zoneObject.object.updatedAt,
|
|
||||||
frameWidth: props.zoneObject.object.frameWidth,
|
|
||||||
frameHeight: props.zoneObject.object.frameHeight
|
|
||||||
} as AssetDataT).catch((error) => {
|
|
||||||
console.error('Error loading texture:', error)
|
|
||||||
})
|
|
||||||
</script>
|
|
@ -6,7 +6,7 @@
|
|||||||
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Users</button>
|
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Users</button>
|
||||||
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Chats</button>
|
<button @mousedown.stop class="btn-cyan py-1.5 px-4 min-w-24">Chats</button>
|
||||||
<button @mousedown.stop class="btn-cyan active py-1.5 px-4 min-w-24">Asset manager</button>
|
<button @mousedown.stop class="btn-cyan active py-1.5 px-4 min-w-24">Asset manager</button>
|
||||||
<button class="btn-cyan py-1.5 px-4 min-w-24" type="button" @click="() => zoneEditorStore.toggleActive()">Map editor</button>
|
<button class="btn-cyan py-1.5 px-4 min-w-24" type="button" @click="mapEditor.toggleActive()">Map editor</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #modalBody>
|
<template #modalBody>
|
||||||
@ -20,12 +20,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import AssetManager from '@/components/gameMaster/assetManager/AssetManager.vue'
|
import AssetManager from '@/components/gameMaster/assetManager/AssetManager.vue'
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const mapEditor = useMapEditorComposable()
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
|
||||||
|
|
||||||
let toggle = ref('asset-manager')
|
let toggle = ref('asset-manager')
|
||||||
</script>
|
</script>
|
||||||
|
@ -5,8 +5,8 @@
|
|||||||
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'tiles' }" @click="() => (selectedCategory = 'tiles')">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'tiles' }" @click="() => (selectedCategory = 'tiles')">
|
||||||
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'tiles' }">Tiles</span>
|
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'tiles' }">Tiles</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'objects' }" @click="() => (selectedCategory = 'objects')">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'map_objects' }" @click="() => (selectedCategory = 'map_objects')">
|
||||||
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'objects' }">Objects</span>
|
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'map_objects' }">Map objects</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'sprites' }" @click="() => (selectedCategory = 'sprites')">
|
<a class="relative p-2.5 hover:cursor-pointer hover:bg-cyan rounded group" :class="{ 'bg-cyan': selectedCategory === 'sprites' }" @click="() => (selectedCategory = 'sprites')">
|
||||||
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'sprites' }">Sprites</span>
|
<span class="group-hover:text-white" :class="{ 'text-white': selectedCategory === 'sprites' }">Sprites</span>
|
||||||
@ -40,7 +40,7 @@
|
|||||||
<!-- Assets list -->
|
<!-- Assets list -->
|
||||||
<div class="overflow-auto h-full w-4/12 flex flex-col relative">
|
<div class="overflow-auto h-full w-4/12 flex flex-col relative">
|
||||||
<TileList v-if="selectedCategory === 'tiles'" />
|
<TileList v-if="selectedCategory === 'tiles'" />
|
||||||
<ObjectList v-if="selectedCategory === 'objects'" />
|
<MapObjectList v-if="selectedCategory === 'map_objects'" />
|
||||||
<SpriteList v-if="selectedCategory === 'sprites'" />
|
<SpriteList v-if="selectedCategory === 'sprites'" />
|
||||||
<ItemList v-if="selectedCategory === 'items'" />
|
<ItemList v-if="selectedCategory === 'items'" />
|
||||||
<CharacterTypeList v-if="selectedCategory === 'characterTypes'" />
|
<CharacterTypeList v-if="selectedCategory === 'characterTypes'" />
|
||||||
@ -50,7 +50,7 @@
|
|||||||
<!-- Asset details -->
|
<!-- Asset details -->
|
||||||
<div class="flex w-7/12 after:hidden flex-col relative overflow-auto">
|
<div class="flex w-7/12 after:hidden flex-col relative overflow-auto">
|
||||||
<TileDetails v-if="selectedCategory === 'tiles' && assetManagerStore.selectedTile" />
|
<TileDetails v-if="selectedCategory === 'tiles' && assetManagerStore.selectedTile" />
|
||||||
<ObjectDetails v-if="selectedCategory === 'objects' && assetManagerStore.selectedObject" />
|
<MapObjectDetails v-if="selectedCategory === 'map_objects' && assetManagerStore.selectedMapObject" />
|
||||||
<SpriteDetails v-if="selectedCategory === 'sprites' && assetManagerStore.selectedSprite" />
|
<SpriteDetails v-if="selectedCategory === 'sprites' && assetManagerStore.selectedSprite" />
|
||||||
<ItemDetails v-if="selectedCategory === 'items' && assetManagerStore.selectedItem" />
|
<ItemDetails v-if="selectedCategory === 'items' && assetManagerStore.selectedItem" />
|
||||||
<CharacterTypeDetails v-if="selectedCategory === 'characterTypes' && assetManagerStore.selectedCharacterType" />
|
<CharacterTypeDetails v-if="selectedCategory === 'characterTypes' && assetManagerStore.selectedCharacterType" />
|
||||||
@ -66,8 +66,8 @@ import CharacterTypeDetails from '@/components/gameMaster/assetManager/partials/
|
|||||||
import CharacterTypeList from '@/components/gameMaster/assetManager/partials/characterType/CharacterTypeList.vue'
|
import CharacterTypeList from '@/components/gameMaster/assetManager/partials/characterType/CharacterTypeList.vue'
|
||||||
import ItemDetails from '@/components/gameMaster/assetManager/partials/item/itemDetails.vue'
|
import ItemDetails from '@/components/gameMaster/assetManager/partials/item/itemDetails.vue'
|
||||||
import ItemList from '@/components/gameMaster/assetManager/partials/item/itemList.vue'
|
import ItemList from '@/components/gameMaster/assetManager/partials/item/itemList.vue'
|
||||||
import ObjectDetails from '@/components/gameMaster/assetManager/partials/object/ObjectDetails.vue'
|
import MapObjectDetails from '@/components/gameMaster/assetManager/partials/mapObject/MapObjectDetails.vue'
|
||||||
import ObjectList from '@/components/gameMaster/assetManager/partials/object/ObjectList.vue'
|
import MapObjectList from '@/components/gameMaster/assetManager/partials/mapObject/MapObjectList.vue'
|
||||||
import SpriteDetails from '@/components/gameMaster/assetManager/partials/sprite/SpriteDetails.vue'
|
import SpriteDetails from '@/components/gameMaster/assetManager/partials/sprite/SpriteDetails.vue'
|
||||||
import SpriteList from '@/components/gameMaster/assetManager/partials/sprite/SpriteList.vue'
|
import SpriteList from '@/components/gameMaster/assetManager/partials/sprite/SpriteList.vue'
|
||||||
import TileDetails from '@/components/gameMaster/assetManager/partials/tile/TileDetails.vue'
|
import TileDetails from '@/components/gameMaster/assetManager/partials/tile/TileDetails.vue'
|
||||||
|
@ -20,12 +20,29 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
|
<div class="space-x-6 flex items-center">
|
||||||
|
<label for="color">Color</label>
|
||||||
|
<input v-model="characterColor" class="input-field" type="text" name="color" placeholder="Character Hair Color" />
|
||||||
|
<div class="h-[38px] w-[38px] rounded" :style="{ backgroundColor: characterColor }"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-field-half">
|
||||||
<label for="spriteId">Sprite</label>
|
<label for="spriteId">Sprite</label>
|
||||||
<select v-model="characterSpriteId" class="input-field" name="spriteId">
|
<select v-model="characterSpriteId" class="input-field" name="spriteId">
|
||||||
<option disabled selected value="">Select sprite</option>
|
<option disabled selected value="">Select sprite</option>
|
||||||
<option v-for="sprite in assetManagerStore.spriteList" :key="sprite.id" :value="sprite.id">{{ sprite.name }}</option>
|
<option v-for="sprite in assetManagerStore.spriteList" :key="sprite.id" :value="sprite.id">{{ sprite.name }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-field-half">
|
||||||
|
<label>Preview</label>
|
||||||
|
<div v-if="characterSpriteId" class="flex flex-col">
|
||||||
|
<div class="p-3 pb-5 min-h-32 block rounded-md default-border bg-gray-800">
|
||||||
|
<div class="flex items-center justify-center p-1 h-full bg-gray-700 rounded">
|
||||||
|
<img :src="config.server_endpoint + '/textures/sprites/' + characterSpriteId + '/front.png'" class="max-w-[200px] max-h-[200px] object-contain" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
||||||
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeCharacterHair">Remove</button>
|
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeCharacterHair">Remove</button>
|
||||||
</form>
|
</form>
|
||||||
@ -34,48 +51,50 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import config from '@/application/config'
|
||||||
|
import { SocketEvent } from '@/application/enums'
|
||||||
import type { CharacterGender, CharacterHair, Sprite } from '@/application/types'
|
import type { CharacterGender, CharacterHair, Sprite } from '@/application/types'
|
||||||
|
import { downloadCache } from '@/application/utilities'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
|
import { CharacterHairStorage } from '@/storage/storages'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const assetManagerStore = useAssetManagerStore()
|
const assetManagerStore = useAssetManagerStore()
|
||||||
|
|
||||||
const selectedCharacterHair = computed(() => assetManagerStore.selectedCharacterHair)
|
const selectedCharacterHair = computed(() => assetManagerStore.selectedCharacterHair)
|
||||||
|
|
||||||
const characterName = ref('')
|
const characterName = ref('')
|
||||||
const characterGender = ref<CharacterGender>('MALE' as CharacterGender.MALE)
|
const characterGender = ref<CharacterGender>('MALE' as CharacterGender.MALE)
|
||||||
|
const characterColor = ref<string>('#000000')
|
||||||
const characterIsSelectable = ref<boolean>(false)
|
const characterIsSelectable = ref<boolean>(false)
|
||||||
const characterSpriteId = ref<string | null | undefined>(null)
|
const characterSpriteId = ref<string | null | undefined>(null)
|
||||||
|
|
||||||
const genderOptions: CharacterGender[] = ['MALE' as CharacterGender.MALE, 'FEMALE' as CharacterGender.FEMALE]
|
const genderOptions: CharacterGender[] = ['MALE' as CharacterGender.MALE, 'FEMALE' as CharacterGender.FEMALE]
|
||||||
|
|
||||||
if (!selectedCharacterHair.value) {
|
|
||||||
console.error('No character hair selected')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedCharacterHair.value) {
|
if (selectedCharacterHair.value) {
|
||||||
characterName.value = selectedCharacterHair.value.name
|
characterName.value = selectedCharacterHair.value.name
|
||||||
characterGender.value = selectedCharacterHair.value.gender
|
characterGender.value = selectedCharacterHair.value.gender
|
||||||
|
characterColor.value = selectedCharacterHair.value.color
|
||||||
characterIsSelectable.value = selectedCharacterHair.value.isSelectable
|
characterIsSelectable.value = selectedCharacterHair.value.isSelectable
|
||||||
characterSpriteId.value = selectedCharacterHair.value.spriteId
|
characterSpriteId.value = selectedCharacterHair.value.sprite?.id
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeCharacterHair() {
|
async function removeCharacterHair() {
|
||||||
if (!selectedCharacterHair.value) return
|
if (!selectedCharacterHair.value) return
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:characterHair:remove', { id: selectedCharacterHair.value.id }, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_REMOVE, { id: selectedCharacterHair.value.id }, async (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to remove character hair')
|
console.error('Failed to remove character hair')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
refreshCharacterHairList()
|
|
||||||
|
await downloadCache('character_hair', new CharacterHairStorage())
|
||||||
|
await refreshCharacterHairList()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshCharacterHairList(unsetSelectedCharacterHair = true) {
|
async function refreshCharacterHairList(unsetSelectedCharacterHair = true) {
|
||||||
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
|
||||||
assetManagerStore.setCharacterHairList(response)
|
assetManagerStore.setCharacterHairList(response)
|
||||||
|
|
||||||
if (unsetSelectedCharacterHair) {
|
if (unsetSelectedCharacterHair) {
|
||||||
@ -84,21 +103,24 @@ function refreshCharacterHairList(unsetSelectedCharacterHair = true) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveCharacterHair() {
|
async function saveCharacterHair() {
|
||||||
const characterHairData = {
|
const characterHairData = {
|
||||||
id: selectedCharacterHair.value!.id,
|
id: selectedCharacterHair.value!.id,
|
||||||
name: characterName.value,
|
name: characterName.value,
|
||||||
gender: characterGender.value,
|
gender: characterGender.value,
|
||||||
|
color: characterColor.value,
|
||||||
isSelectable: characterIsSelectable.value,
|
isSelectable: characterIsSelectable.value,
|
||||||
spriteId: characterSpriteId.value
|
spriteId: characterSpriteId.value
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:characterHair:update', characterHairData, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_UPDATE, characterHairData, async (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to save character type')
|
console.error('Failed to save character type')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
refreshCharacterHairList(false)
|
|
||||||
|
await downloadCache('character_hair', new CharacterHairStorage())
|
||||||
|
await refreshCharacterHairList(false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,14 +128,15 @@ watch(selectedCharacterHair, (characterHair: CharacterHair | null) => {
|
|||||||
if (!characterHair) return
|
if (!characterHair) return
|
||||||
characterName.value = characterHair.name
|
characterName.value = characterHair.name
|
||||||
characterGender.value = characterHair.gender
|
characterGender.value = characterHair.gender
|
||||||
|
characterColor.value = characterHair.color
|
||||||
characterIsSelectable.value = characterHair.isSelectable
|
characterIsSelectable.value = characterHair.isSelectable
|
||||||
characterSpriteId.value = characterHair.spriteId
|
characterSpriteId.value = characterHair.sprite?.id
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!selectedCharacterHair.value) return
|
if (!selectedCharacterHair.value) return
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||||
assetManagerStore.setSpriteList(response)
|
assetManagerStore.setSpriteList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -9,8 +9,8 @@
|
|||||||
</button>
|
</button>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
<div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||||
<div v-bind="wrapperProps" ref="elementToScroll">
|
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
|
||||||
<a
|
<a
|
||||||
v-for="{ data: characterHair } in list"
|
v-for="{ data: characterHair } in list"
|
||||||
:key="characterHair.id"
|
:key="characterHair.id"
|
||||||
@ -25,14 +25,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
<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">
|
<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="" />
|
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { SocketEvent } from '@/application/enums'
|
||||||
import type { CharacterHair } from '@/application/types'
|
import type { CharacterHair } from '@/application/types'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useVirtualList } from '@vueuse/core'
|
import { useVirtualList } from '@vueuse/core'
|
||||||
@ -52,13 +54,13 @@ const handleSearch = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createNewCharacterHair = () => {
|
const createNewCharacterHair = () => {
|
||||||
gameStore.connection?.emit('gm:characterHair:create', {}, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_CREATE, {}, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to create new character type')
|
console.error('Failed to create new character type')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
|
||||||
assetManagerStore.setCharacterHairList(response)
|
assetManagerStore.setCharacterHairList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -92,7 +94,7 @@ function toTop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
|
||||||
assetManagerStore.setCharacterHairList(response)
|
assetManagerStore.setCharacterHairList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -40,12 +40,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { SocketEvent } from '@/application/enums'
|
||||||
import type { CharacterGender, CharacterRace, CharacterType, Sprite } from '@/application/types'
|
import type { CharacterGender, CharacterRace, CharacterType, Sprite } from '@/application/types'
|
||||||
|
import { downloadCache } from '@/application/utilities'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
|
import { CharacterTypeStorage } from '@/storage/storages'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const assetManagerStore = useAssetManagerStore()
|
const assetManagerStore = useAssetManagerStore()
|
||||||
|
|
||||||
const selectedCharacterType = computed(() => assetManagerStore.selectedCharacterType)
|
const selectedCharacterType = computed(() => assetManagerStore.selectedCharacterType)
|
||||||
@ -68,23 +70,25 @@ if (selectedCharacterType.value) {
|
|||||||
characterGender.value = selectedCharacterType.value.gender
|
characterGender.value = selectedCharacterType.value.gender
|
||||||
characterRace.value = selectedCharacterType.value.race
|
characterRace.value = selectedCharacterType.value.race
|
||||||
characterIsSelectable.value = selectedCharacterType.value.isSelectable
|
characterIsSelectable.value = selectedCharacterType.value.isSelectable
|
||||||
characterSpriteId.value = selectedCharacterType.value.spriteId
|
characterSpriteId.value = selectedCharacterType.value.sprite?.id
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeCharacterType() {
|
async function removeCharacterType() {
|
||||||
if (!selectedCharacterType.value) return
|
if (!selectedCharacterType.value) return
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:characterType:remove', { id: selectedCharacterType.value.id }, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_REMOVE, { id: selectedCharacterType.value.id }, async (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to remove character type')
|
console.error('Failed to remove character type')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
refreshCharacterTypeList()
|
|
||||||
|
await downloadCache('character_types', new CharacterTypeStorage())
|
||||||
|
await refreshCharacterTypeList()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshCharacterTypeList(unsetSelectedCharacterType = true) {
|
async function refreshCharacterTypeList(unsetSelectedCharacterType = true) {
|
||||||
gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
|
||||||
assetManagerStore.setCharacterTypeList(response)
|
assetManagerStore.setCharacterTypeList(response)
|
||||||
|
|
||||||
if (unsetSelectedCharacterType) {
|
if (unsetSelectedCharacterType) {
|
||||||
@ -93,7 +97,7 @@ function refreshCharacterTypeList(unsetSelectedCharacterType = true) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveCharacterType() {
|
async function saveCharacterType() {
|
||||||
const characterTypeData = {
|
const characterTypeData = {
|
||||||
id: selectedCharacterType.value!.id,
|
id: selectedCharacterType.value!.id,
|
||||||
name: characterName.value,
|
name: characterName.value,
|
||||||
@ -103,12 +107,14 @@ function saveCharacterType() {
|
|||||||
spriteId: characterSpriteId.value
|
spriteId: characterSpriteId.value
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:characterType:update', characterTypeData, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_UPDATE, characterTypeData, async (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to save character type')
|
console.error('Failed to save character type')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
refreshCharacterTypeList(false)
|
|
||||||
|
await downloadCache('character_types', new CharacterTypeStorage())
|
||||||
|
await refreshCharacterTypeList(false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,13 +124,13 @@ watch(selectedCharacterType, (characterType: CharacterType | null) => {
|
|||||||
characterGender.value = characterType.gender
|
characterGender.value = characterType.gender
|
||||||
characterRace.value = characterType.race
|
characterRace.value = characterType.race
|
||||||
characterIsSelectable.value = characterType.isSelectable
|
characterIsSelectable.value = characterType.isSelectable
|
||||||
characterSpriteId.value = characterType.spriteId
|
characterSpriteId.value = characterType.sprite?.id
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!selectedCharacterType.value) return
|
if (!selectedCharacterType.value) return
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||||
assetManagerStore.setSpriteList(response)
|
assetManagerStore.setSpriteList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -9,8 +9,8 @@
|
|||||||
</button>
|
</button>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
<div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||||
<div v-bind="wrapperProps" ref="elementToScroll">
|
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
|
||||||
<a
|
<a
|
||||||
v-for="{ data: characterType } in list"
|
v-for="{ data: characterType } in list"
|
||||||
:key="characterType.id"
|
:key="characterType.id"
|
||||||
@ -25,14 +25,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
<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">
|
<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="" />
|
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { SocketEvent } from '@/application/enums'
|
||||||
import type { CharacterType } from '@/application/types'
|
import type { CharacterType } from '@/application/types'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useVirtualList } from '@vueuse/core'
|
import { useVirtualList } from '@vueuse/core'
|
||||||
@ -52,13 +54,13 @@ const handleSearch = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createNewCharacterType = () => {
|
const createNewCharacterType = () => {
|
||||||
gameStore.connection?.emit('gm:characterType:create', {}, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_CREATE, {}, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to create new character type')
|
console.error('Failed to create new character type')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
|
||||||
assetManagerStore.setCharacterTypeList(response)
|
assetManagerStore.setCharacterTypeList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -92,7 +94,7 @@ function toTop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
|
||||||
assetManagerStore.setCharacterTypeList(response)
|
assetManagerStore.setCharacterTypeList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -44,7 +44,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Item, ItemRarity, ItemType } from '@/application/types'
|
import { SocketEvent } from '@/application/enums'
|
||||||
|
import type { Item, ItemRarity, ItemType, Sprite } from '@/application/types'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
@ -80,7 +82,7 @@ if (selectedItem.value) {
|
|||||||
function removeItem() {
|
function removeItem() {
|
||||||
if (!selectedItem.value) return
|
if (!selectedItem.value) return
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:item:remove', { id: selectedItem.value.id }, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_ITEM_REMOVE, { id: selectedItem.value.id }, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to remove item')
|
console.error('Failed to remove item')
|
||||||
return
|
return
|
||||||
@ -90,7 +92,7 @@ function removeItem() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function refreshItemList(unsetSelectedItem = true) {
|
function refreshItemList(unsetSelectedItem = true) {
|
||||||
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => {
|
socketManager.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
|
||||||
assetManagerStore.setItemList(response)
|
assetManagerStore.setItemList(response)
|
||||||
|
|
||||||
if (unsetSelectedItem) {
|
if (unsetSelectedItem) {
|
||||||
@ -110,7 +112,7 @@ function saveItem() {
|
|||||||
spriteId: itemSpriteId.value
|
spriteId: itemSpriteId.value
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:item:update', itemData, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_ITEM_UPDATE, itemData, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to save item')
|
console.error('Failed to save item')
|
||||||
return
|
return
|
||||||
@ -132,7 +134,7 @@ watch(selectedItem, (item: Item | null) => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!selectedItem.value) return
|
if (!selectedItem.value) return
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||||
assetManagerStore.setSpriteList(response)
|
assetManagerStore.setSpriteList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -9,8 +9,8 @@
|
|||||||
</button>
|
</button>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
<div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||||
<div v-bind="wrapperProps" ref="elementToScroll">
|
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
|
||||||
<a v-for="{ data: 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)">
|
<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">
|
<div class="flex items-center gap-2.5">
|
||||||
<span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedItem?.id === item.id }">
|
<span class="group-hover:text-white" :class="{ 'text-white': assetManagerStore.selectedItem?.id === item.id }">
|
||||||
@ -22,14 +22,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
<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">
|
<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="" />
|
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { SocketEvent } from '@/application/enums'
|
||||||
import type { Item } from '@/application/types'
|
import type { Item } from '@/application/types'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useVirtualList } from '@vueuse/core'
|
import { useVirtualList } from '@vueuse/core'
|
||||||
@ -48,13 +50,13 @@ const handleSearch = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createNewItem = () => {
|
const createNewItem = () => {
|
||||||
gameStore.connection?.emit('gm:item:create', {}, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_ITEM_CREATE, {}, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to create new item')
|
console.error('Failed to create new item')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => {
|
socketManager.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
|
||||||
assetManagerStore.setItemList(response)
|
assetManagerStore.setItemList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -88,7 +90,7 @@ function toTop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => {
|
socketManager.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
|
||||||
assetManagerStore.setItemList(response)
|
assetManagerStore.setItemList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -0,0 +1,217 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-full overflow-auto">
|
||||||
|
<div class="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray">
|
||||||
|
<div class="grid grid-cols-[160px_auto_max-content] gap-12">
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" checked v-model="showOrigin" /><label>Show Origin</label>
|
||||||
|
<br />
|
||||||
|
<input type="checkbox" checked v-model="showPartitionOverlay" /><label>Show Partitions</label>
|
||||||
|
</div>
|
||||||
|
<div class="relative w-fit h-fit">
|
||||||
|
<img class="max-h-56" :src="`${config.server_endpoint}/textures/map_objects/${selectedMapObject?.id}.png`" :alt="'Object ' + selectedMapObject?.id" ref="imageRef" />
|
||||||
|
<svg ref="svg" class="absolute top-0 left-0 w-full h-full inline-block pointer-events-none">
|
||||||
|
<circle v-if="showOrigin && svg" r="4" :cx="mapObjectOriginX * width" :cy="mapObjectOriginY * height" stroke="white" stroke-width="2" />
|
||||||
|
<rect v-if="showPartitionOverlay && svg" v-for="(offset, index) in mapObjectDepthOffsets" style="opacity: 0.5" stroke="red" :x="index * (width / mapObjectDepthOffsets.length)" :width="width / mapObjectDepthOffsets.length" :y="0" :height="height" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="btn-cyan px-4 py-1.5 min-w-24" @click="mapObjectDepthOffsets.push(0)">Add Partition</button>
|
||||||
|
<p>Depth Offset</p>
|
||||||
|
<div class="text-white grid grid-cols-[120px_80px_auto] items-baseline gap-2" v-for="(offset, index) in mapObjectDepthOffsets">
|
||||||
|
<input class="input-field max-h-4 mt-2" type="number" :value="offset" @change="setPartitionDepth($event, index)" />
|
||||||
|
<button @click="mapObjectDepthOffsets.splice(index, 1)">Remove</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-5 block">
|
||||||
|
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject">
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input v-model="mapObjectName" class="input-field" type="text" name="name" placeholder="Wall #1" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field-half">
|
||||||
|
<label for="origin-x">Origin X</label>
|
||||||
|
<input v-model="mapObjectOriginX" class="input-field" type="number" step="any" name="origin-x" placeholder="Origin X" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field-half">
|
||||||
|
<label for="origin-y">Origin Y</label>
|
||||||
|
<input v-model="mapObjectOriginY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="tags">Tags</label>
|
||||||
|
<ChipsInput v-model="mapObjectTags" @update:modelValue="mapObjectTags = $event" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="frame-speed">Frame rate</label>
|
||||||
|
<input v-model="mapObjectFrameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field-half">
|
||||||
|
<label for="frame-width">Frame width</label>
|
||||||
|
<input v-model="mapObjectFrameWidth" class="input-field" type="number" step="any" name="frame-width" placeholder="Frame width" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field-half">
|
||||||
|
<label for="frame-height">Frame height</label>
|
||||||
|
<input v-model="mapObjectFrameHeight" class="input-field" type="number" step="any" name="frame-height" placeholder="Frame height" />
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
||||||
|
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeObject">Delete</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import config from '@/application/config'
|
||||||
|
import { SocketEvent } from '@/application/enums'
|
||||||
|
import type { MapObject } from '@/application/types'
|
||||||
|
import { downloadCache } from '@/application/utilities'
|
||||||
|
import ChipsInput from '@/components/forms/ChipsInput.vue'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
|
import { MapObjectStorage } from '@/storage/storages'
|
||||||
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
|
import { useElementSize } from '@vueuse/core'
|
||||||
|
import { Rectangle } from 'phavuer'
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue'
|
||||||
|
|
||||||
|
const assetManagerStore = useAssetManagerStore()
|
||||||
|
|
||||||
|
const selectedMapObject = computed(() => assetManagerStore.selectedMapObject)
|
||||||
|
const svg = useTemplateRef('svg')
|
||||||
|
const { width, height } = useElementSize(svg)
|
||||||
|
|
||||||
|
const mapObjectName = ref('')
|
||||||
|
const mapObjectTags = ref<string[]>([])
|
||||||
|
const mapObjectDepthOffsets = ref<number[]>([])
|
||||||
|
const mapObjectOriginX = ref(0)
|
||||||
|
const mapObjectOriginY = ref(0)
|
||||||
|
const mapObjectFrameRate = ref(0)
|
||||||
|
const mapObjectFrameWidth = ref(0)
|
||||||
|
const mapObjectFrameHeight = ref(0)
|
||||||
|
const imageRef = ref<HTMLImageElement | null>(null)
|
||||||
|
const showOrigin = ref(true)
|
||||||
|
const showPartitionOverlay = ref(true)
|
||||||
|
|
||||||
|
if (!selectedMapObject.value) {
|
||||||
|
console.error('No map mapObject selected')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedMapObject.value) {
|
||||||
|
mapObjectName.value = selectedMapObject.value.name
|
||||||
|
mapObjectTags.value = selectedMapObject.value.tags
|
||||||
|
mapObjectDepthOffsets.value = selectedMapObject.value.depthOffsets
|
||||||
|
mapObjectOriginX.value = selectedMapObject.value.originX
|
||||||
|
mapObjectOriginY.value = selectedMapObject.value.originY
|
||||||
|
mapObjectFrameRate.value = selectedMapObject.value.frameRate
|
||||||
|
mapObjectFrameWidth.value = selectedMapObject.value.frameWidth
|
||||||
|
mapObjectFrameHeight.value = selectedMapObject.value.frameHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
const setPartitionDepth = (event: any, idx: number) => (mapObjectDepthOffsets.value[idx] = Number.parseInt(event.target.value))
|
||||||
|
|
||||||
|
async function removeObject() {
|
||||||
|
if (!selectedMapObject.value) return
|
||||||
|
socketManager.emit(SocketEvent.GM_MAPOBJECT_REMOVE, { mapObjectId: selectedMapObject.value.id }, async (response: boolean) => {
|
||||||
|
if (!response) {
|
||||||
|
console.error('Failed to remove mapObject')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await downloadCache('map_objects', new MapObjectStorage())
|
||||||
|
await refreshObjectList()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshObjectList(unsetSelectedMapObject = true) {
|
||||||
|
socketManager.emit(SocketEvent.GM_MAPOBJECT_LIST, {}, (response: MapObject[]) => {
|
||||||
|
assetManagerStore.setMapObjectList(response)
|
||||||
|
|
||||||
|
if (unsetSelectedMapObject) {
|
||||||
|
assetManagerStore.setSelectedMapObject(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveObject() {
|
||||||
|
if (!selectedMapObject.value) {
|
||||||
|
console.error('No mapObject selected')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
socketManager.emit(
|
||||||
|
SocketEvent.GM_MAPOBJECT_UPDATE,
|
||||||
|
{
|
||||||
|
id: selectedMapObject.value.id,
|
||||||
|
name: mapObjectName.value,
|
||||||
|
tags: mapObjectTags.value,
|
||||||
|
depthOffsets: mapObjectDepthOffsets.value,
|
||||||
|
originX: mapObjectOriginX.value,
|
||||||
|
originY: mapObjectOriginY.value,
|
||||||
|
frameRate: mapObjectFrameRate.value,
|
||||||
|
frameWidth: mapObjectFrameWidth.value,
|
||||||
|
frameHeight: mapObjectFrameHeight.value
|
||||||
|
},
|
||||||
|
async (response: boolean) => {
|
||||||
|
if (!response) {
|
||||||
|
console.error('Failed to save mapObject')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await downloadCache('map_objects', new MapObjectStorage())
|
||||||
|
await refreshObjectList(false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(selectedMapObject, (mapObject: MapObject | null) => {
|
||||||
|
if (!mapObject) return
|
||||||
|
mapObjectName.value = mapObject.name
|
||||||
|
mapObjectTags.value = mapObject.tags
|
||||||
|
mapObjectDepthOffsets.value = mapObject.depthOffsets
|
||||||
|
mapObjectOriginX.value = mapObject.originX
|
||||||
|
mapObjectOriginY.value = mapObject.originY
|
||||||
|
mapObjectFrameRate.value = mapObject.frameRate
|
||||||
|
mapObjectFrameWidth.value = mapObject.frameWidth
|
||||||
|
mapObjectFrameHeight.value = mapObject.frameHeight
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!selectedMapObject.value) return
|
||||||
|
})
|
||||||
|
|
||||||
|
// function startDragging(index: number, event: MouseEvent) {
|
||||||
|
// isDragging.value = true
|
||||||
|
// draggedPointIndex.value = index
|
||||||
|
//
|
||||||
|
// const moveHandler = (e: MouseEvent) => {
|
||||||
|
// if (!isDragging.value || !imageRef.value) return
|
||||||
|
// const rect = imageRef.value.getBoundingClientRect()
|
||||||
|
// mapObjectPivotPoints.value[draggedPointIndex.value] = {
|
||||||
|
// x: e.clientX - rect.left,
|
||||||
|
// y: e.clientY - rect.top
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// const upHandler = () => {
|
||||||
|
// isDragging.value = false
|
||||||
|
// draggedPointIndex.value = -1
|
||||||
|
// window.removeEventListener('mousemove', moveHandler)
|
||||||
|
// window.removeEventListener('mouseup', upHandler)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// window.addEventListener('mousemove', moveHandler)
|
||||||
|
// window.addEventListener('mouseup', upHandler)
|
||||||
|
// }
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
assetManagerStore.setSelectedMapObject(null)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pointer-events-none {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
@ -8,20 +8,20 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div v-bind="containerProps" class="overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
<div v-bind="containerProps" class="flex-1 overflow-y-auto relative p-2.5 rounded-md default-border bg-gray" @scroll="onScroll">
|
||||||
<div v-bind="wrapperProps" ref="elementToScroll">
|
<div v-bind="wrapperProps" ref="elementToScroll" class="flex flex-col gap-2.5">
|
||||||
<a v-for="{ data: object } in list" :key="object.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedObject?.id === object.id }" @click="assetManagerStore.setSelectedObject(object as Object)">
|
<a v-for="{ data: mapObject } in list" :key="mapObject.id" class="relative p-2.5 cursor-pointer block rounded hover:bg-cyan group" :class="{ 'bg-cyan': assetManagerStore.selectedMapObject?.id === mapObject.id }" @click="assetManagerStore.setSelectedMapObject(mapObject as MapObject)">
|
||||||
<div class="flex items-center gap-2.5">
|
<div class="flex items-center gap-2.5">
|
||||||
<div class="h-7 w-16 max-w-16 flex justify-center">
|
<div class="h-7 w-16 max-w-16 flex justify-center">
|
||||||
<img class="h-7" :src="`${config.server_endpoint}/assets/objects/${object.id}.png`" alt="Object" />
|
<img class="h-7" :src="`${config.server_endpoint}/textures/map_objects/${mapObject.id}.png`" alt="Object" />
|
||||||
</div>
|
</div>
|
||||||
<span :class="{ 'text-white': assetManagerStore.selectedObject?.id === object.id }">{{ object.name }}</span>
|
<span :class="{ 'text-white': assetManagerStore.selectedMapObject?.id === mapObject.id }">{{ mapObject.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute w-12 h-12 bottom-2.5 right-2.5">
|
<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">
|
<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="" />
|
<img class="invert w-8 h-8 center-element rotate-180" src="/assets/icons/mapEditor/chevron.svg" alt="" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -29,7 +29,9 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
import type { Object } from '@/application/types'
|
import { SocketEvent } from '@/application/enums'
|
||||||
|
import type { MapObject } from '@/application/types'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useVirtualList } from '@vueuse/core'
|
import { useVirtualList } from '@vueuse/core'
|
||||||
@ -47,14 +49,14 @@ const elementToScroll = ref()
|
|||||||
const handleFileUpload = (e: Event) => {
|
const handleFileUpload = (e: Event) => {
|
||||||
const files = (e.target as HTMLInputElement).files
|
const files = (e.target as HTMLInputElement).files
|
||||||
if (!files) return
|
if (!files) return
|
||||||
gameStore.connection?.emit('gm:object:upload', files, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_MAPOBJECT_UPLOAD, files, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
if (config.development) console.error('Failed to upload object')
|
if (config.environment === 'development') console.error('Failed to upload map object')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => {
|
socketManager.emit(SocketEvent.GM_MAPOBJECT_LIST, {}, (response: MapObject[]) => {
|
||||||
assetManagerStore.setObjectList(response)
|
assetManagerStore.setMapObjectList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -66,9 +68,9 @@ const handleSearch = () => {
|
|||||||
|
|
||||||
const filteredObjects = computed(() => {
|
const filteredObjects = computed(() => {
|
||||||
if (!searchQuery.value) {
|
if (!searchQuery.value) {
|
||||||
return assetManagerStore.objectList
|
return assetManagerStore.mapObjectList
|
||||||
}
|
}
|
||||||
return assetManagerStore.objectList.filter((object) => object.name.toLowerCase().includes(searchQuery.value.toLowerCase()))
|
return assetManagerStore.mapObjectList.filter((object) => object.name.toLowerCase().includes(searchQuery.value.toLowerCase()))
|
||||||
})
|
})
|
||||||
|
|
||||||
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(filteredObjects, {
|
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(filteredObjects, {
|
||||||
@ -92,8 +94,8 @@ function toTop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => {
|
socketManager.emit(SocketEvent.GM_MAPOBJECT_LIST, {}, (response: MapObject[]) => {
|
||||||
assetManagerStore.setObjectList(response)
|
assetManagerStore.setMapObjectList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
@ -1,163 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="h-full overflow-auto">
|
|
||||||
<div class="relative p-2.5 flex flex-col items-center justify-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" />
|
|
||||||
</div>
|
|
||||||
<div class="mt-5 block">
|
|
||||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject">
|
|
||||||
<div class="form-field-full">
|
|
||||||
<label for="name">Name</label>
|
|
||||||
<input v-model="objectName" class="input-field" type="text" name="name" placeholder="Wall #1" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field-half">
|
|
||||||
<label for="origin-x">Origin X</label>
|
|
||||||
<input v-model="objectOriginX" class="input-field" type="number" step="any" name="origin-x" placeholder="Origin X" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field-half">
|
|
||||||
<label for="origin-y">Origin Y</label>
|
|
||||||
<input v-model="objectOriginY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field-full">
|
|
||||||
<label for="tags">Tags</label>
|
|
||||||
<ChipsInput v-model="objectTags" @update:modelValue="objectTags = $event" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field-full">
|
|
||||||
<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 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>
|
|
||||||
<input v-model="objectFrameWidth" class="input-field" type="number" step="any" name="frame-width" placeholder="Frame width" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field-half">
|
|
||||||
<label for="frame-height">Frame height</label>
|
|
||||||
<input v-model="objectFrameHeight" class="input-field" type="number" step="any" name="frame-height" placeholder="Frame height" />
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-4">
|
|
||||||
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
|
||||||
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeObject">Delete</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import config from '@/application/config'
|
|
||||||
import type { 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()
|
|
||||||
const zoneEditorStore = useZoneEditorStore()
|
|
||||||
|
|
||||||
const selectedObject = computed(() => assetManagerStore.selectedObject)
|
|
||||||
|
|
||||||
const objectName = ref('')
|
|
||||||
const objectTags = ref<string[]>([])
|
|
||||||
const objectOriginX = ref(0)
|
|
||||||
const objectOriginY = ref(0)
|
|
||||||
const objectIsAnimated = ref(false)
|
|
||||||
const objectFrameRate = ref(0)
|
|
||||||
const objectFrameWidth = ref(0)
|
|
||||||
const objectFrameHeight = ref(0)
|
|
||||||
|
|
||||||
if (!selectedObject.value) {
|
|
||||||
console.error('No object selected')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedObject.value) {
|
|
||||||
objectName.value = selectedObject.value.name
|
|
||||||
objectTags.value = selectedObject.value.tags
|
|
||||||
objectOriginX.value = selectedObject.value.originX
|
|
||||||
objectOriginY.value = selectedObject.value.originY
|
|
||||||
objectIsAnimated.value = selectedObject.value.isAnimated
|
|
||||||
objectFrameRate.value = selectedObject.value.frameRate
|
|
||||||
objectFrameWidth.value = selectedObject.value.frameWidth
|
|
||||||
objectFrameHeight.value = selectedObject.value.frameHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeObject() {
|
|
||||||
gameStore.connection?.emit('gm:object:remove', { object: selectedObject.value?.id }, (response: boolean) => {
|
|
||||||
if (!response) {
|
|
||||||
console.error('Failed to remove object')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
refreshObjectList()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshObjectList(unsetSelectedObject = true) {
|
|
||||||
gameStore.connection?.emit('gm:object:list', {}, (response: Object[]) => {
|
|
||||||
assetManagerStore.setObjectList(response)
|
|
||||||
|
|
||||||
if (unsetSelectedObject) {
|
|
||||||
assetManagerStore.setSelectedObject(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (zoneEditorStore.active) {
|
|
||||||
zoneEditorStore.setObjectList(response)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveObject() {
|
|
||||||
if (!selectedObject.value) {
|
|
||||||
console.error('No object selected')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
gameStore.connection?.emit(
|
|
||||||
'gm:object:update',
|
|
||||||
{
|
|
||||||
id: selectedObject.value.id,
|
|
||||||
name: objectName.value,
|
|
||||||
tags: objectTags.value,
|
|
||||||
originX: objectOriginX.value,
|
|
||||||
originY: objectOriginY.value,
|
|
||||||
isAnimated: objectIsAnimated.value,
|
|
||||||
frameRate: objectFrameRate.value,
|
|
||||||
frameWidth: objectFrameWidth.value,
|
|
||||||
frameHeight: objectFrameHeight.value
|
|
||||||
},
|
|
||||||
(response: boolean) => {
|
|
||||||
if (!response) {
|
|
||||||
console.error('Failed to save object')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
refreshObjectList(false)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(selectedObject, (object: Object | null) => {
|
|
||||||
if (!object) return
|
|
||||||
objectName.value = object.name
|
|
||||||
objectTags.value = object.tags
|
|
||||||
objectOriginX.value = object.originX
|
|
||||||
objectOriginY.value = object.originY
|
|
||||||
objectIsAnimated.value = object.isAnimated
|
|
||||||
objectFrameRate.value = object.frameRate
|
|
||||||
objectFrameWidth.value = object.frameWidth
|
|
||||||
objectFrameHeight.value = object.frameHeight
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (!selectedObject.value) return
|
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
assetManagerStore.setSelectedObject(null)
|
|
||||||
})
|
|
||||||
</script>
|
|
@ -1,90 +1,76 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full overflow-auto">
|
<div class="h-full overflow-auto">
|
||||||
<div class="relative flex flex-col">
|
<div class="relative flex flex-col">
|
||||||
<div class="flex flex-wrap gap-2 p-2.5 rounded-md default-border bg-gray">
|
<div class="flex flex-wrap gap-2 p-2.5 rounded-md default-border bg-gray mb-4">
|
||||||
<div class="w-full flex flex-col">
|
<div class="w-full flex flex-col">
|
||||||
<label class="mb-1.5 font-titles" for="name">Name</label>
|
<label class="mb-1.5 font-titles" for="name">Name</label>
|
||||||
<input v-model="spriteName" class="input-field" type="text" name="name" placeholder="New sprite" />
|
<input v-model="spriteName" class="input-field" type="text" name="name" placeholder="New sprite" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full flex gap-2 mt-2 pb-4 relative">
|
<div class="w-full flex gap-2 mt-2 pb-4 relative">
|
||||||
<button class="btn-cyan px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="saveSprite">Save</button>
|
<button class="btn-cyan px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="saveSprite">Save</button>
|
||||||
<button class="btn-red px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="deleteSprite">Delete</button>
|
<button class="btn-red px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="deleteSprite">Delete</button>
|
||||||
<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">
|
<button class="btn-indigo 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">
|
<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" />
|
<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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn-cyan px-4" type="button" @click.prevent="addNewImage">New action</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-for="action in spriteActions" :key="action.id">
|
||||||
<button class="btn-cyan py-2 my-4" type="button" @click.prevent="addNewImage">New action</button>
|
<div class="flex flex-wrap gap-3 mb-3">
|
||||||
<Accordion v-for="action in spriteActions" :key="action.id">
|
<div v-for="(image, index) in action.sprites" :key="index" class="h-20 w-20 p-4 bg-gray-300 bg-opacity-50 rounded text-center relative group">
|
||||||
<template #header>
|
<img :src="image.url" class="max-w-full max-h-full object-contain pointer-events-none" alt="Uploaded image" @load="updateImageDimensions($event, index)" />
|
||||||
<div class="flex justify-between items-center">
|
<div v-if="imageDimensions[index]" class="absolute bottom-1 right-1 bg-black/50 text-white text-xs px-1 py-0.5 rounded transition-opacity font-default">{{ imageDimensions[index].width }}x{{ imageDimensions[index].height }}</div>
|
||||||
{{ action.action }}
|
|
||||||
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="() => spriteActions.splice(spriteActions.indexOf(action), 1)">Delete</button>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
<template #content>
|
|
||||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveSprite">
|
|
||||||
<div class="form-field-full">
|
|
||||||
<label for="action">Action</label>
|
|
||||||
<input v-model="action.action" class="input-field" type="text" name="action" placeholder="Action" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-half">
|
<div class="flex items-center mb-3">
|
||||||
<label for="origin-x">Origin X</label>
|
<div class="mr-3 space-x-2">
|
||||||
<input v-model.number="action.originX" class="input-field" type="number" step="any" name="origin-x" placeholder="Origin X" />
|
<button class="btn-cyan px-4 py-1.5 min-w-24 text-left" type="button" @click.stop.prevent="openEditorModal(action)">
|
||||||
|
Editor
|
||||||
|
<div class="flex">
|
||||||
|
<small class="text-xs font-default">{{ action.action }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-half">
|
</button>
|
||||||
<label for="origin-y">Origin Y</label>
|
|
||||||
<input v-model.number="action.originY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-half">
|
|
||||||
<label for="is-animated">Is animated</label>
|
|
||||||
<select v-model="action.isAnimated" class="input-field" name="is-animated">
|
|
||||||
<option :value="false">No</option>
|
|
||||||
<option :value="true">Yes</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-half" v-if="action.isAnimated">
|
|
||||||
<label for="is-looping">Is looping</label>
|
|
||||||
<select v-model="action.isLooping" class="input-field" name="is-looping">
|
|
||||||
<option :value="false">No</option>
|
|
||||||
<option :value="true">Yes</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-full" v-if="action.isAnimated">
|
<SpriteEditor
|
||||||
<label for="frame-speed">Frame rate</label>
|
v-for="[actionId, editorData] in Array.from(openEditors.entries())"
|
||||||
<input v-model.number="action.frameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
|
:key="actionId"
|
||||||
</div>
|
:sprite="selectedSprite!"
|
||||||
<div class="form-field-full">
|
:sprites="editorData.action.sprites"
|
||||||
<SpriteActionsInput v-model="action.sprites" />
|
:frame-rate="editorData.action.frameRate"
|
||||||
</div>
|
:is-modal-open="editorData.isOpen"
|
||||||
</form>
|
:temp-offset-index="getTempOffsetIndex(editorData.action)"
|
||||||
</template>
|
:temp-offset="getTempOffset(editorData.action)"
|
||||||
</Accordion>
|
@update:frame-rate="(value) => updateFrameRate(editorData.action, value)"
|
||||||
|
@update:is-modal-open="(value) => handleEditorModalClose(editorData.action, value)"
|
||||||
|
@update:temp-offset="(index, offset) => handleTempOffsetChange(editorData.action, index, offset)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { SocketEvent } from '@/application/enums'
|
||||||
import type { Sprite, SpriteAction } from '@/application/types'
|
import type { Sprite, SpriteAction } from '@/application/types'
|
||||||
import { uuidv4 } from '@/application/utilities'
|
import { downloadCache, uuidv4 } from '@/application/utilities'
|
||||||
import SpriteActionsInput from '@/components/gameMaster/assetManager/partials/sprite/partials/SpriteImagesInput.vue'
|
import SpriteEditor from '@/components/gameMaster/assetManager/partials/sprite/partials/SpriteEditor.vue'
|
||||||
import Accordion from '@/components/utilities/Accordion.vue'
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
|
import { SpriteStorage } from '@/storage/storages'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const assetManagerStore = useAssetManagerStore()
|
const assetManagerStore = useAssetManagerStore()
|
||||||
|
|
||||||
const selectedSprite = computed(() => assetManagerStore.selectedSprite)
|
const selectedSprite = computed(() => assetManagerStore.selectedSprite)
|
||||||
|
const tempOffsetData = ref<Map<string, { index: number | undefined; offset: { x: number; y: number } | undefined }>>(new Map())
|
||||||
const spriteName = ref('')
|
const spriteName = ref('')
|
||||||
const spriteActions = ref<SpriteAction[]>([])
|
const spriteActions = ref<SpriteAction[]>([])
|
||||||
|
|
||||||
|
const openEditors = ref(new Map<string, { action: SpriteAction; isOpen: boolean }>())
|
||||||
|
|
||||||
if (!selectedSprite.value) {
|
if (!selectedSprite.value) {
|
||||||
console.error('No sprite selected')
|
console.error('No sprite selected')
|
||||||
}
|
}
|
||||||
@ -94,28 +80,32 @@ if (selectedSprite.value) {
|
|||||||
spriteActions.value = sortSpriteActions(selectedSprite.value.spriteActions)
|
spriteActions.value = sortSpriteActions(selectedSprite.value.spriteActions)
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteSprite() {
|
async function deleteSprite() {
|
||||||
gameStore.connection?.emit('gm:sprite:delete', { id: selectedSprite.value?.id }, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_SPRITE_DELETE, { id: selectedSprite.value?.id }, async (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to delete sprite')
|
console.error('Failed to delete sprite')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
refreshSpriteList()
|
|
||||||
|
await downloadCache('sprites', new SpriteStorage())
|
||||||
|
await refreshSpriteList()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function copySprite() {
|
async function copySprite() {
|
||||||
gameStore.connection?.emit('gm:sprite:copy', { id: selectedSprite.value?.id }, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_SPRITE_COPY, { id: selectedSprite.value?.id }, async (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to copy sprite')
|
console.error('Failed to copy sprite')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
refreshSpriteList(false)
|
|
||||||
|
await downloadCache('sprites', new SpriteStorage())
|
||||||
|
await refreshSpriteList(false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshSpriteList(unsetSelectedSprite = true) {
|
async function refreshSpriteList(unsetSelectedSprite = true) {
|
||||||
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||||
assetManagerStore.setSpriteList(response)
|
assetManagerStore.setSpriteList(response)
|
||||||
|
|
||||||
if (unsetSelectedSprite) {
|
if (unsetSelectedSprite) {
|
||||||
@ -124,7 +114,7 @@ function refreshSpriteList(unsetSelectedSprite = true) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveSprite() {
|
async function saveSprite() {
|
||||||
if (!selectedSprite.value) {
|
if (!selectedSprite.value) {
|
||||||
console.error('No sprite selected')
|
console.error('No sprite selected')
|
||||||
return
|
return
|
||||||
@ -140,8 +130,6 @@ function saveSprite() {
|
|||||||
sprites: action.sprites,
|
sprites: action.sprites,
|
||||||
originX: action.originX,
|
originX: action.originX,
|
||||||
originY: action.originY,
|
originY: action.originY,
|
||||||
isAnimated: action.isAnimated,
|
|
||||||
isLooping: action.isLooping,
|
|
||||||
frameRate: action.frameRate,
|
frameRate: action.frameRate,
|
||||||
frameWidth: action.frameWidth,
|
frameWidth: action.frameWidth,
|
||||||
frameHeight: action.frameHeight
|
frameHeight: action.frameHeight
|
||||||
@ -149,12 +137,14 @@ function saveSprite() {
|
|||||||
}) ?? []
|
}) ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:sprite:update', updatedSprite, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_SPRITE_UPDATE, updatedSprite, async (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to save sprite')
|
console.error('Failed to save sprite')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
refreshSpriteList(false)
|
|
||||||
|
await downloadCache('sprites', new SpriteStorage())
|
||||||
|
await refreshSpriteList(false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,14 +153,11 @@ function addNewImage() {
|
|||||||
|
|
||||||
const newImage: SpriteAction = {
|
const newImage: SpriteAction = {
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
spriteId: selectedSprite.value.id,
|
sprite: selectedSprite.value.id,
|
||||||
sprite: selectedSprite.value,
|
|
||||||
action: 'new_action',
|
action: 'new_action',
|
||||||
sprites: [],
|
sprites: [],
|
||||||
originX: 0,
|
originX: 0,
|
||||||
originY: 0,
|
originY: 0,
|
||||||
isAnimated: false,
|
|
||||||
isLooping: false,
|
|
||||||
frameRate: 0,
|
frameRate: 0,
|
||||||
frameWidth: 0,
|
frameWidth: 0,
|
||||||
frameHeight: 0
|
frameHeight: 0
|
||||||
@ -184,15 +171,74 @@ function addNewImage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sortSpriteActions(actions: SpriteAction[]): SpriteAction[] {
|
function sortSpriteActions(actions: SpriteAction[]): SpriteAction[] {
|
||||||
|
if (!actions) return []
|
||||||
return [...actions].sort((a, b) => a.action.localeCompare(b.action))
|
return [...actions].sort((a, b) => a.action.localeCompare(b.action))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openEditorModal(action: SpriteAction) {
|
||||||
|
const newOpenEditors = new Map(openEditors.value)
|
||||||
|
newOpenEditors.set(action.id, { action, isOpen: true })
|
||||||
|
openEditors.value = newOpenEditors
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFrameRate(action: SpriteAction, value: number) {
|
||||||
|
console.log('update frame rate', action)
|
||||||
|
action.frameRate = value
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEditorModalClose(action: SpriteAction, isOpen: boolean) {
|
||||||
|
if (isOpen) return
|
||||||
|
const newOpenEditors = new Map(openEditors.value)
|
||||||
|
newOpenEditors.delete(action.id)
|
||||||
|
openEditors.value = newOpenEditors
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTempOffsetChange(action: SpriteAction, index: number, offset: { x: number; y: number }) {
|
||||||
|
// Update the temporary offset data for this action
|
||||||
|
const newTempOffsetData = new Map(tempOffsetData.value)
|
||||||
|
newTempOffsetData.set(action.id, { index, offset })
|
||||||
|
tempOffsetData.value = newTempOffsetData
|
||||||
|
|
||||||
|
// Also update the actual sprite data so changes persist
|
||||||
|
if (action.sprites && action.sprites[index]) {
|
||||||
|
action.sprites[index].offset = { ...offset };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTempOffsetIndex(action: SpriteAction): number | undefined {
|
||||||
|
return tempOffsetData.value.get(action.id)?.index
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTempOffset(action: SpriteAction): { x: number; y: number } | undefined {
|
||||||
|
return tempOffsetData.value.get(action.id)?.offset
|
||||||
|
}
|
||||||
|
|
||||||
watch(selectedSprite, (sprite: Sprite | null) => {
|
watch(selectedSprite, (sprite: Sprite | null) => {
|
||||||
if (!sprite) return
|
if (!sprite) return
|
||||||
spriteName.value = sprite.name
|
spriteName.value = sprite.name
|
||||||
spriteActions.value = sortSpriteActions(sprite.spriteActions)
|
spriteActions.value = sortSpriteActions(sprite.spriteActions)
|
||||||
|
openEditors.value = new Map()
|
||||||
|
tempOffsetData.value = new Map() // Reset temp offset data when sprite changes
|
||||||
})
|
})
|
||||||
|
|
||||||
|
interface SpriteImage {
|
||||||
|
url: string
|
||||||
|
offset: {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageDimensions = ref<{ [key: number]: { width: number; height: number } }>({})
|
||||||
|
|
||||||
|
const updateImageDimensions = (event: Event, index: number) => {
|
||||||
|
const img = event.target as HTMLImageElement
|
||||||
|
imageDimensions.value[index] = {
|
||||||
|
width: img.naturalWidth,
|
||||||
|
height: img.naturalHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!selectedSprite.value) return
|
if (!selectedSprite.value) return
|
||||||
})
|
})
|
||||||
|