Compare commits
120 Commits
task/#324
...
feature/#3
Author | SHA1 | Date | |
---|---|---|---|
82a854e647 | |||
3bcb16fa9c | |||
f79ebedc62 | |||
44b0368276 | |||
b8b985470f | |||
39e00c6feb | |||
2a00e206eb | |||
8f9b19ba8b | |||
d997a33b86 | |||
9749b02ccf | |||
f83d5eabee | |||
a9cedba4e0 | |||
49dcd92a9e | |||
d010159989 | |||
275dd95c69 | |||
e3c3d4d420 | |||
87e7f14469 | |||
723aa59142 | |||
c369719564 | |||
2d8c421ac6 | |||
1137c95ff3 | |||
4b56da0fa0 | |||
c21e78c2ec | |||
fcf96a25ae | |||
cf9deebc94 | |||
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 | |||
fb6e2aa742 | |||
e530f69311 | |||
144a513cb6 | |||
2a6321b06b | |||
ba90982e35 |
@ -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_WIDTH=64
|
VITE_TILE_SIZE_WIDTH=64
|
||||||
VITE_TILE_SIZE_HEIGHT=32
|
VITE_TILE_SIZE_HEIGHT=32
|
70
Caddyfile
Normal file
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
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"
|
|
||||||
}
|
|
491
package-lock.json
generated
491
package-lock.json
generated
@ -86,14 +86,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/generator": {
|
"node_modules/@babel/generator": {
|
||||||
"version": "7.26.5",
|
"version": "7.26.8",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.8.tgz",
|
||||||
"integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==",
|
"integrity": "sha512-ef383X5++iZHWAXX0SXQR6ZyQhw/0KtTkrTz61WXRhFM6dhpHulO/RJz79L8S6ugZHJkOOkUrUdxgdF2YiPFnA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.26.5",
|
"@babel/parser": "^7.26.8",
|
||||||
"@babel/types": "^7.26.5",
|
"@babel/types": "^7.26.8",
|
||||||
"@jridgewell/gen-mapping": "^0.3.5",
|
"@jridgewell/gen-mapping": "^0.3.5",
|
||||||
"@jridgewell/trace-mapping": "^0.3.25",
|
"@jridgewell/trace-mapping": "^0.3.25",
|
||||||
"jsesc": "^3.0.2"
|
"jsesc": "^3.0.2"
|
||||||
@ -121,12 +121,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/parser": {
|
"node_modules/@babel/parser": {
|
||||||
"version": "7.26.7",
|
"version": "7.26.8",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.8.tgz",
|
||||||
"integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==",
|
"integrity": "sha512-TZIQ25pkSoaKEYYaHbbxkfL36GNsQ6iFiBbeuzAkLnXayKR1yP1zFe+NxuZWWsUyvt8icPU9CCq0sgWGXR1GEw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.26.7"
|
"@babel/types": "^7.26.8"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"parser": "bin/babel-parser.js"
|
"parser": "bin/babel-parser.js"
|
||||||
@ -149,32 +149,32 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.25.9",
|
"version": "7.26.8",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.8.tgz",
|
||||||
"integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
|
"integrity": "sha512-iNKaX3ZebKIsCvJ+0jd6embf+Aulaa3vNBqZ41kM7iTWjx5qzWKXGHiJUW3+nTpQ18SG11hdF8OAzKrpXkb96Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.25.9",
|
"@babel/code-frame": "^7.26.2",
|
||||||
"@babel/parser": "^7.25.9",
|
"@babel/parser": "^7.26.8",
|
||||||
"@babel/types": "^7.25.9"
|
"@babel/types": "^7.26.8"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/traverse": {
|
"node_modules/@babel/traverse": {
|
||||||
"version": "7.26.7",
|
"version": "7.26.8",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.8.tgz",
|
||||||
"integrity": "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==",
|
"integrity": "sha512-nic9tRkjYH0oB2dzr/JoGIm+4Q6SuYeLEiIiZDwBscRMYFJ+tMAz98fuel9ZnbXViA2I0HVSSRRK8DW5fjXStA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.26.2",
|
"@babel/code-frame": "^7.26.2",
|
||||||
"@babel/generator": "^7.26.5",
|
"@babel/generator": "^7.26.8",
|
||||||
"@babel/parser": "^7.26.7",
|
"@babel/parser": "^7.26.8",
|
||||||
"@babel/template": "^7.25.9",
|
"@babel/template": "^7.26.8",
|
||||||
"@babel/types": "^7.26.7",
|
"@babel/types": "^7.26.8",
|
||||||
"debug": "^4.3.1",
|
"debug": "^4.3.1",
|
||||||
"globals": "^11.1.0"
|
"globals": "^11.1.0"
|
||||||
},
|
},
|
||||||
@ -183,9 +183,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/types": {
|
"node_modules/@babel/types": {
|
||||||
"version": "7.26.7",
|
"version": "7.26.8",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.8.tgz",
|
||||||
"integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==",
|
"integrity": "sha512-eUuWapzEGWFEpHFxgEaBG8e3n6S8L3MSu0oda755rOfabWPnh0Our1AozNFVUxGFIhbKgd1ksprsoDGMinTOTA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-string-parser": "^7.25.9",
|
"@babel/helper-string-parser": "^7.25.9",
|
||||||
@ -1161,9 +1161,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.32.1",
|
"version": "4.34.6",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.6.tgz",
|
||||||
"integrity": "sha512-/pqA4DmqyCm8u5YIDzIdlLcEmuvxb0v8fZdFhVMszSpDTgbQKdw3/mB3eMUHIbubtJ6F9j+LtmyCnHTEqIHyzA==",
|
"integrity": "sha512-+GcCXtOQoWuC7hhX1P00LqjjIiS/iOouHXhMdiDSnq/1DGTox4SpUvO52Xm+div6+106r+TcvOeo/cxvyEyTgg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@ -1175,9 +1175,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm64": {
|
"node_modules/@rollup/rollup-android-arm64": {
|
||||||
"version": "4.32.1",
|
"version": "4.34.6",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.6.tgz",
|
||||||
"integrity": "sha512-If3PDskT77q7zgqVqYuj7WG3WC08G1kwXGVFi9Jr8nY6eHucREHkfpX79c0ACAjLj3QIWKPJR7w4i+f5EdLH5Q==",
|
"integrity": "sha512-E8+2qCIjciYUnCa1AiVF1BkRgqIGW9KzJeesQqVfyRITGQN+dFuoivO0hnro1DjT74wXLRZ7QF8MIbz+luGaJA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -1189,9 +1189,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||||
"version": "4.32.1",
|
"version": "4.34.6",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.6.tgz",
|
||||||
"integrity": "sha512-zCpKHioQ9KgZToFp5Wvz6zaWbMzYQ2LJHQ+QixDKq52KKrF65ueu6Af4hLlLWHjX1Wf/0G5kSJM9PySW9IrvHA==",
|
"integrity": "sha512-z9Ib+OzqN3DZEjX7PDQMHEhtF+t6Mi2z/ueChQPLS/qUMKY7Ybn5A2ggFoKRNRh1q1T03YTQfBTQCJZiepESAg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -1203,9 +1203,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-x64": {
|
"node_modules/@rollup/rollup-darwin-x64": {
|
||||||
"version": "4.32.1",
|
"version": "4.34.6",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.6.tgz",
|
||||||
"integrity": "sha512-sFvF+t2+TyUo/ZQqUcifrJIgznx58oFZbdHS9TvHq3xhPVL9nOp+yZ6LKrO9GWTP+6DbFtoyLDbjTpR62Mbr3Q==",
|
"integrity": "sha512-PShKVY4u0FDAR7jskyFIYVyHEPCPnIQY8s5OcXkdU8mz3Y7eXDJPdyM/ZWjkYdR2m0izD9HHWA8sGcXn+Qrsyg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -1217,9 +1217,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||||
"version": "4.32.1",
|
"version": "4.34.6",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.6.tgz",
|
||||||
"integrity": "sha512-NbOa+7InvMWRcY9RG+B6kKIMD/FsnQPH0MWUvDlQB1iXnF/UcKSudCXZtv4lW+C276g3w5AxPbfry5rSYvyeYA==",
|
"integrity": "sha512-YSwyOqlDAdKqs0iKuqvRHLN4SrD2TiswfoLfvYXseKbL47ht1grQpq46MSiQAx6rQEN8o8URtpXARCpqabqxGQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -1231,9 +1231,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||||
"version": "4.32.1",
|
"version": "4.34.6",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.6.tgz",
|
||||||
"integrity": "sha512-JRBRmwvHPXR881j2xjry8HZ86wIPK2CcDw0EXchE1UgU0ubWp9nvlT7cZYKc6bkypBt745b4bglf3+xJ7hXWWw==",
|
"integrity": "sha512-HEP4CgPAY1RxXwwL5sPFv6BBM3tVeLnshF03HMhJYCNc6kvSqBgTMmsEjb72RkZBAWIqiPUyF1JpEBv5XT9wKQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -1245,9 +1245,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||||
"version": "4.32.1",
|
"version": "4.34.6",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.6.tgz",
|
||||||
"integrity": "sha512-PKvszb+9o/vVdUzCCjL0sKHukEQV39tD3fepXxYrHE3sTKrRdCydI7uldRLbjLmDA3TFDmh418XH19NOsDRH8g==",
|
"integrity": "sha512-88fSzjC5xeH9S2Vg3rPgXJULkHcLYMkh8faix8DX4h4TIAL65ekwuQMA/g2CXq8W+NJC43V6fUpYZNjaX3+IIg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@ -1259,9 +1259,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||||
"version": "4.32.1",
|
"version": "4.34.6",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.6.tgz",
|
||||||
"integrity": "sha512-9WHEMV6Y89eL606ReYowXuGF1Yb2vwfKWKdD1A5h+OYnPZSJvxbEjxTRKPgi7tkP2DSnW0YLab1ooy+i/FQp/Q==",
|
"integrity": "sha512-wM4ztnutBqYFyvNeR7Av+reWI/enK9tDOTKNF+6Kk2Q96k9bwhDDOlnCUNRPvromlVXo04riSliMBs/Z7RteEg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@ -1273,9 +1273,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||||
"version": "4.32.1",
|
"version": "4.34.6",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.6.tgz",
|
||||||
"integrity": "sha512-tZWc9iEt5fGJ1CL2LRPw8OttkCBDs+D8D3oEM8mH8S1ICZCtFJhD7DZ3XMGM8kpqHvhGUTvNUYVDnmkj4BDXnw==",
|
"integrity": "sha512-9RyprECbRa9zEjXLtvvshhw4CMrRa3K+0wcp3KME0zmBe1ILmvcVHnypZ/aIDXpRyfhSYSuN4EPdCCj5Du8FIA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -1287,9 +1287,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||||
"version": "4.32.1",
|
"version": "4.34.6",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.6.tgz",
|
||||||
"integrity": "sha512-FTYc2YoTWUsBz5GTTgGkRYYJ5NGJIi/rCY4oK/I8aKowx1ToXeoVVbIE4LGAjsauvlhjfl0MYacxClLld1VrOw==",
|
"integrity": "sha512-qTmklhCTyaJSB05S+iSovfo++EwnIEZxHkzv5dep4qoszUMX5Ca4WM4zAVUMbfdviLgCSQOu5oU8YoGk1s6M9Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -1301,9 +1301,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
|
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
|
||||||
"version": "4.32.1",
|
"version": "4.34.6",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.6.tgz",
|
||||||
"integrity": "sha512-F51qLdOtpS6P1zJVRzYM0v6MrBNypyPEN1GfMiz0gPu9jN8ScGaEFIZQwteSsGKg799oR5EaP7+B2jHgL+d+Kw==",
|
"integrity": "sha512-4Qmkaps9yqmpjY5pvpkfOerYgKNUGzQpFxV6rnS7c/JfYbDSU0y6WpbbredB5cCpLFGJEqYX40WUmxMkwhWCjw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
@ -1315,9 +1315,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||||
"version": "4.32.1",
|
"version": "4.34.6",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.6.tgz",
|
||||||
"integrity": "sha512-wO0WkfSppfX4YFm5KhdCCpnpGbtgQNj/tgvYzrVYFKDpven8w2N6Gg5nB6w+wAMO3AIfSTWeTjfVe+uZ23zAlg==",
|
"integrity": "sha512-Zsrtux3PuaxuBTX/zHdLaFmcofWGzaWW1scwLU3ZbW/X+hSsFbz9wDIp6XvnT7pzYRl9MezWqEqKy7ssmDEnuQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@ -1329,9 +1329,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||||
"version": "4.32.1",
|
"version": "4.34.6",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.6.tgz",
|
||||||
"integrity": "sha512-iWswS9cIXfJO1MFYtI/4jjlrGb/V58oMu4dYJIKnR5UIwbkzR0PJ09O0PDZT0oJ3LYWXBSWahNf/Mjo6i1E5/g==",
|
"integrity": "sha512-aK+Zp+CRM55iPrlyKiU3/zyhgzWBxLVrw2mwiQSYJRobCURb781+XstzvA8Gkjg/hbdQFuDw44aUOxVQFycrAg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
@ -1343,9 +1343,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||||
"version": "4.32.1",
|
"version": "4.34.6",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.6.tgz",
|
||||||
"integrity": "sha512-RKt8NI9tebzmEthMnfVgG3i/XeECkMPS+ibVZjZ6mNekpbbUmkNWuIN2yHsb/mBPyZke4nlI4YqIdFPgKuoyQQ==",
|
"integrity": "sha512-WoKLVrY9ogmaYPXwTH326+ErlCIgMmsoRSx6bO+l68YgJnlOXhygDYSZe/qbUJCSiCiZAQ+tKm88NcWuUXqOzw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@ -1357,9 +1357,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||||
"version": "4.32.1",
|
"version": "4.34.6",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.6.tgz",
|
||||||
"integrity": "sha512-WQFLZ9c42ECqEjwg/GHHsouij3pzLXkFdz0UxHa/0OM12LzvX7DzedlY0SIEly2v18YZLRhCRoHZDxbBSWoGYg==",
|
"integrity": "sha512-Sht4aFvmA4ToHd2vFzwMFaQCiYm2lDFho5rPcvPBT5pCdC+GwHG6CMch4GQfmWTQ1SwRKS0dhDYb54khSrjDWw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -1371,9 +1371,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||||
"version": "4.32.1",
|
"version": "4.34.6",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.6.tgz",
|
||||||
"integrity": "sha512-BLoiyHDOWoS3uccNSADMza6V6vCNiphi94tQlVIL5de+r6r/CCQuNnerf+1g2mnk2b6edp5dk0nhdZ7aEjOBsA==",
|
"integrity": "sha512-zmmpOQh8vXc2QITsnCiODCDGXFC8LMi64+/oPpPx5qz3pqv0s6x46ps4xoycfUiVZps5PFn1gksZzo4RGTKT+A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -1385,9 +1385,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||||
"version": "4.32.1",
|
"version": "4.34.6",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.6.tgz",
|
||||||
"integrity": "sha512-w2l3UnlgYTNNU+Z6wOR8YdaioqfEnwPjIsJ66KxKAf0p+AuL2FHeTX6qvM+p/Ue3XPBVNyVSfCrfZiQh7vZHLQ==",
|
"integrity": "sha512-3/q1qUsO/tLqGBaD4uXsB6coVGB3usxw3qyeVb59aArCgedSF66MPdgRStUd7vbZOsko/CgVaY5fo2vkvPLWiA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -1399,9 +1399,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||||
"version": "4.32.1",
|
"version": "4.34.6",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.6.tgz",
|
||||||
"integrity": "sha512-Am9H+TGLomPGkBnaPWie4F3x+yQ2rr4Bk2jpwy+iV+Gel9jLAu/KqT8k3X4jxFPW6Zf8OMnehyutsd+eHoq1WQ==",
|
"integrity": "sha512-oLHxuyywc6efdKVTxvc0135zPrRdtYVjtVD5GUm55I3ODxhU/PwkQFD97z16Xzxa1Fz0AEe4W/2hzRtd+IfpOA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@ -1413,9 +1413,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||||
"version": "4.32.1",
|
"version": "4.34.6",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.6.tgz",
|
||||||
"integrity": "sha512-ar80GhdZb4DgmW3myIS9nRFYcpJRSME8iqWgzH2i44u+IdrzmiXVxeFnExQ5v4JYUSpg94bWjevMG8JHf1Da5Q==",
|
"integrity": "sha512-0PVwmgzZ8+TZ9oGBmdZoQVXflbvuwzN/HRclujpl4N/q3i+y0lqLw8n1bXA8ru3sApDjlmONaNAuYr38y1Kr9w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -1465,9 +1465,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.17.16",
|
"version": "20.17.17",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.16.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.17.tgz",
|
||||||
"integrity": "sha512-vOTpLduLkZXePLxHiHsBLp98mHGnl8RptV4YAO3HfKO5UHjDvySGbxKtpYfy8Sx5+WKcgc45qNreJJRVM3L6mw==",
|
"integrity": "sha512-/WndGO4kIfMicEQLTi/mDANUu/iVUhT7KboZPdEqqHQ4aTS+3qT3U5gIqWDFV+XouorjfgGqvKILJeHhuQgFYg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -1502,14 +1502,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/expect": {
|
"node_modules/@vitest/expect": {
|
||||||
"version": "2.1.8",
|
"version": "2.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz",
|
||||||
"integrity": "sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw==",
|
"integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/spy": "2.1.8",
|
"@vitest/spy": "2.1.9",
|
||||||
"@vitest/utils": "2.1.8",
|
"@vitest/utils": "2.1.9",
|
||||||
"chai": "^5.1.2",
|
"chai": "^5.1.2",
|
||||||
"tinyrainbow": "^1.2.0"
|
"tinyrainbow": "^1.2.0"
|
||||||
},
|
},
|
||||||
@ -1518,13 +1518,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/mocker": {
|
"node_modules/@vitest/mocker": {
|
||||||
"version": "2.1.8",
|
"version": "2.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz",
|
||||||
"integrity": "sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==",
|
"integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/spy": "2.1.8",
|
"@vitest/spy": "2.1.9",
|
||||||
"estree-walker": "^3.0.3",
|
"estree-walker": "^3.0.3",
|
||||||
"magic-string": "^0.30.12"
|
"magic-string": "^0.30.12"
|
||||||
},
|
},
|
||||||
@ -1545,9 +1545,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/pretty-format": {
|
"node_modules/@vitest/pretty-format": {
|
||||||
"version": "2.1.8",
|
"version": "2.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz",
|
||||||
"integrity": "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==",
|
"integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -1558,13 +1558,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/runner": {
|
"node_modules/@vitest/runner": {
|
||||||
"version": "2.1.8",
|
"version": "2.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz",
|
||||||
"integrity": "sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg==",
|
"integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/utils": "2.1.8",
|
"@vitest/utils": "2.1.9",
|
||||||
"pathe": "^1.1.2"
|
"pathe": "^1.1.2"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
@ -1572,13 +1572,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/snapshot": {
|
"node_modules/@vitest/snapshot": {
|
||||||
"version": "2.1.8",
|
"version": "2.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz",
|
||||||
"integrity": "sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==",
|
"integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/pretty-format": "2.1.8",
|
"@vitest/pretty-format": "2.1.9",
|
||||||
"magic-string": "^0.30.12",
|
"magic-string": "^0.30.12",
|
||||||
"pathe": "^1.1.2"
|
"pathe": "^1.1.2"
|
||||||
},
|
},
|
||||||
@ -1587,9 +1587,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/spy": {
|
"node_modules/@vitest/spy": {
|
||||||
"version": "2.1.8",
|
"version": "2.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz",
|
||||||
"integrity": "sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg==",
|
"integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -1600,13 +1600,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/utils": {
|
"node_modules/@vitest/utils": {
|
||||||
"version": "2.1.8",
|
"version": "2.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz",
|
||||||
"integrity": "sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==",
|
"integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/pretty-format": "2.1.8",
|
"@vitest/pretty-format": "2.1.9",
|
||||||
"loupe": "^3.1.2",
|
"loupe": "^3.1.2",
|
||||||
"tinyrainbow": "^1.2.0"
|
"tinyrainbow": "^1.2.0"
|
||||||
},
|
},
|
||||||
@ -1926,13 +1926,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/abbrev": {
|
"node_modules/abbrev": {
|
||||||
"version": "2.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.0.tgz",
|
||||||
"integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==",
|
"integrity": "sha512-+/kfrslGQ7TNV2ecmQwMJj/B65g5KVq1/L3SGVZ3tCYGqlzFuFCGBZJtMP99wH3NpEUyAjn0zPdPUg0D+DwrOA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
"node": "^18.17.0 || >=20.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/agent-base": {
|
"node_modules/agent-base": {
|
||||||
@ -2168,9 +2168,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001696",
|
"version": "1.0.30001699",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001696.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001699.tgz",
|
||||||
"integrity": "sha512-pDCPkvzfa39ehJtJ+OwGT/2yvT2SbjfHhiIW2LWOAcMQ7BzwxT/XuyUp4OTOd0XFWA6BKw0JalnBHgSi5DGJBQ==",
|
"integrity": "sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@ -2535,9 +2535,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.90",
|
"version": "1.5.96",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.90.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.96.tgz",
|
||||||
"integrity": "sha512-C3PN4aydfW91Natdyd449Kw+BzhLmof6tzy5W1pFC5SpQxVXT+oyiyOG9AgYYSN9OdA/ik3YkCrpwqI8ug5Tug==",
|
"integrity": "sha512-8AJUW6dh75Fm/ny8+kZKJzI1pgoE8bKLZlzDU2W1ENd+DXKJrx7I7l9hb8UWR4ojlnb5OlixMt00QWiYJoVw1w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
@ -2881,22 +2881,25 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/glob": {
|
"node_modules/glob": {
|
||||||
"version": "10.4.5",
|
"version": "11.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz",
|
||||||
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
"integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"foreground-child": "^3.1.0",
|
"foreground-child": "^3.1.0",
|
||||||
"jackspeak": "^3.1.2",
|
"jackspeak": "^4.0.1",
|
||||||
"minimatch": "^9.0.4",
|
"minimatch": "^10.0.0",
|
||||||
"minipass": "^7.1.2",
|
"minipass": "^7.1.2",
|
||||||
"package-json-from-dist": "^1.0.0",
|
"package-json-from-dist": "^1.0.0",
|
||||||
"path-scurry": "^1.11.1"
|
"path-scurry": "^2.0.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"glob": "dist/esm/bin.mjs"
|
"glob": "dist/esm/bin.mjs"
|
||||||
},
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
@ -2915,16 +2918,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/glob/node_modules/minimatch": {
|
"node_modules/glob/node_modules/minimatch": {
|
||||||
"version": "9.0.5",
|
"version": "10.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz",
|
||||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
"integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"brace-expansion": "^2.0.1"
|
"brace-expansion": "^2.0.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16 || 14 >=14.17"
|
"node": "20 || >=22"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
@ -3201,19 +3204,19 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/jackspeak": {
|
"node_modules/jackspeak": {
|
||||||
"version": "3.4.3",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz",
|
||||||
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
|
"integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@isaacs/cliui": "^8.0.2"
|
"@isaacs/cliui": "^8.0.2"
|
||||||
},
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@pkgjs/parseargs": "^0.11.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jiti": {
|
"node_modules/jiti": {
|
||||||
@ -3227,17 +3230,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/js-beautify": {
|
"node_modules/js-beautify": {
|
||||||
"version": "1.15.1",
|
"version": "1.15.2",
|
||||||
"resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz",
|
"resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.2.tgz",
|
||||||
"integrity": "sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==",
|
"integrity": "sha512-mcG6CHJxxih+EFAbd5NEBwrosIs6MoJmiNLFYN6kj5SeJMf7n29Ii/H4lt6zGTvmdB9AApuj5cs4zydjuLeqjw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"config-chain": "^1.1.13",
|
"config-chain": "^1.1.13",
|
||||||
"editorconfig": "^1.0.4",
|
"editorconfig": "^1.0.4",
|
||||||
"glob": "^10.3.3",
|
"glob": "^11.0.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"nopt": "^7.2.0"
|
"nopt": "^8.0.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"css-beautify": "js/bin/css-beautify.js",
|
"css-beautify": "js/bin/css-beautify.js",
|
||||||
@ -3610,19 +3613,19 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/nopt": {
|
"node_modules/nopt": {
|
||||||
"version": "7.2.1",
|
"version": "8.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz",
|
||||||
"integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==",
|
"integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"abbrev": "^2.0.0"
|
"abbrev": "^3.0.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"nopt": "bin/nopt.js"
|
"nopt": "bin/nopt.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
"node": "^18.17.0 || >=20.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/normalize-path": {
|
"node_modules/normalize-path": {
|
||||||
@ -3784,22 +3787,32 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/path-scurry": {
|
"node_modules/path-scurry": {
|
||||||
"version": "1.11.1",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
|
||||||
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
|
"integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lru-cache": "^10.2.0",
|
"lru-cache": "^11.0.0",
|
||||||
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
|
"minipass": "^7.1.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16 || 14 >=14.18"
|
"node": "20 || >=22"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/path-scurry/node_modules/lru-cache": {
|
||||||
|
"version": "11.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz",
|
||||||
|
"integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pathe": {
|
"node_modules/pathe": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
|
||||||
@ -3852,9 +3865,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/phavuer": {
|
"node_modules/phavuer": {
|
||||||
"version": "0.16.4",
|
"version": "0.16.5",
|
||||||
"resolved": "https://registry.npmjs.org/phavuer/-/phavuer-0.16.4.tgz",
|
"resolved": "https://registry.npmjs.org/phavuer/-/phavuer-0.16.5.tgz",
|
||||||
"integrity": "sha512-mlsGGDVoFUl67J6B8NtLHghmzsd316/VGn6c13/4eb7Upwl+ur382xtb711oBGmsK1NbptI1zkUWqVSWPykXNA==",
|
"integrity": "sha512-NuUghQU1cZ8ePGJ9rt9czahtzPiJb7H5wALuP2ZDn8W4ELYuBk5Fz/nMzyxBFFme78Px7q38YTY3nW7xQMu1Ww==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@ -4086,9 +4099,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/prettier": {
|
"node_modules/prettier": {
|
||||||
"version": "3.4.2",
|
"version": "3.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.0.tgz",
|
||||||
"integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
|
"integrity": "sha512-quyMrVt6svPS7CjQ9gKb3GLEX/rl3BCL2oa/QkNcXv4YNVBC9olt3s+H7ukto06q7B1Qz46PbrKLO34PR6vXcA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
@ -4250,9 +4263,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.32.1",
|
"version": "4.34.6",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.6.tgz",
|
||||||
"integrity": "sha512-z+aeEsOeEa3mEbS1Tjl6sAZ8NE3+AalQz1RJGj81M+fizusbdDMoEJwdJNHfaB40Scr4qNu+welOfes7maKonA==",
|
"integrity": "sha512-wc2cBWqJgkU3Iz5oztRkQbfVkbxoz5EhnCGOrnJvnLnQ7O0WhQUYyv18qQI79O8L7DdHrrlJNeCHd4VGpnaXKQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -4266,25 +4279,25 @@
|
|||||||
"npm": ">=8.0.0"
|
"npm": ">=8.0.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rollup/rollup-android-arm-eabi": "4.32.1",
|
"@rollup/rollup-android-arm-eabi": "4.34.6",
|
||||||
"@rollup/rollup-android-arm64": "4.32.1",
|
"@rollup/rollup-android-arm64": "4.34.6",
|
||||||
"@rollup/rollup-darwin-arm64": "4.32.1",
|
"@rollup/rollup-darwin-arm64": "4.34.6",
|
||||||
"@rollup/rollup-darwin-x64": "4.32.1",
|
"@rollup/rollup-darwin-x64": "4.34.6",
|
||||||
"@rollup/rollup-freebsd-arm64": "4.32.1",
|
"@rollup/rollup-freebsd-arm64": "4.34.6",
|
||||||
"@rollup/rollup-freebsd-x64": "4.32.1",
|
"@rollup/rollup-freebsd-x64": "4.34.6",
|
||||||
"@rollup/rollup-linux-arm-gnueabihf": "4.32.1",
|
"@rollup/rollup-linux-arm-gnueabihf": "4.34.6",
|
||||||
"@rollup/rollup-linux-arm-musleabihf": "4.32.1",
|
"@rollup/rollup-linux-arm-musleabihf": "4.34.6",
|
||||||
"@rollup/rollup-linux-arm64-gnu": "4.32.1",
|
"@rollup/rollup-linux-arm64-gnu": "4.34.6",
|
||||||
"@rollup/rollup-linux-arm64-musl": "4.32.1",
|
"@rollup/rollup-linux-arm64-musl": "4.34.6",
|
||||||
"@rollup/rollup-linux-loongarch64-gnu": "4.32.1",
|
"@rollup/rollup-linux-loongarch64-gnu": "4.34.6",
|
||||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.32.1",
|
"@rollup/rollup-linux-powerpc64le-gnu": "4.34.6",
|
||||||
"@rollup/rollup-linux-riscv64-gnu": "4.32.1",
|
"@rollup/rollup-linux-riscv64-gnu": "4.34.6",
|
||||||
"@rollup/rollup-linux-s390x-gnu": "4.32.1",
|
"@rollup/rollup-linux-s390x-gnu": "4.34.6",
|
||||||
"@rollup/rollup-linux-x64-gnu": "4.32.1",
|
"@rollup/rollup-linux-x64-gnu": "4.34.6",
|
||||||
"@rollup/rollup-linux-x64-musl": "4.32.1",
|
"@rollup/rollup-linux-x64-musl": "4.34.6",
|
||||||
"@rollup/rollup-win32-arm64-msvc": "4.32.1",
|
"@rollup/rollup-win32-arm64-msvc": "4.34.6",
|
||||||
"@rollup/rollup-win32-ia32-msvc": "4.32.1",
|
"@rollup/rollup-win32-ia32-msvc": "4.34.6",
|
||||||
"@rollup/rollup-win32-x64-msvc": "4.32.1",
|
"@rollup/rollup-win32-x64-msvc": "4.34.6",
|
||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -4327,9 +4340,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/sass": {
|
"node_modules/sass": {
|
||||||
"version": "1.83.4",
|
"version": "1.84.0",
|
||||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.83.4.tgz",
|
"resolved": "https://registry.npmjs.org/sass/-/sass-1.84.0.tgz",
|
||||||
"integrity": "sha512-B1bozCeNQiOgDcLd33e2Cs2U60wZwjUUXzh900ZyQF5qUasvMdDZYbQ566LJu7cqR+sAHlAfO6RMkaID5s6qpA==",
|
"integrity": "sha512-XDAbhEPJRxi7H0SxrnOpiXFQoUJHwkR2u3Zc4el+fK/Tt5Hpzw5kkQ59qVDfvdaUq6gCrEZIbySFBM2T9DNKHg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -4361,9 +4374,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.7.0",
|
"version": "7.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
||||||
"integrity": "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ==",
|
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
@ -4651,6 +4664,76 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sucrase/node_modules/glob": {
|
||||||
|
"version": "10.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||||
|
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"foreground-child": "^3.1.0",
|
||||||
|
"jackspeak": "^3.1.2",
|
||||||
|
"minimatch": "^9.0.4",
|
||||||
|
"minipass": "^7.1.2",
|
||||||
|
"package-json-from-dist": "^1.0.0",
|
||||||
|
"path-scurry": "^1.11.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"glob": "dist/esm/bin.mjs"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sucrase/node_modules/jackspeak": {
|
||||||
|
"version": "3.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
||||||
|
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@isaacs/cliui": "^8.0.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@pkgjs/parseargs": "^0.11.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sucrase/node_modules/minimatch": {
|
||||||
|
"version": "9.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||||
|
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16 || 14 >=14.17"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sucrase/node_modules/path-scurry": {
|
||||||
|
"version": "1.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
|
||||||
|
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"lru-cache": "^10.2.0",
|
||||||
|
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16 || 14 >=14.18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/supports-color": {
|
"node_modules/supports-color": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
@ -5040,9 +5123,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite-node": {
|
"node_modules/vite-node": {
|
||||||
"version": "2.1.8",
|
"version": "2.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz",
|
||||||
"integrity": "sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==",
|
"integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -5078,19 +5161,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vitest": {
|
"node_modules/vitest": {
|
||||||
"version": "2.1.8",
|
"version": "2.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz",
|
||||||
"integrity": "sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==",
|
"integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/expect": "2.1.8",
|
"@vitest/expect": "2.1.9",
|
||||||
"@vitest/mocker": "2.1.8",
|
"@vitest/mocker": "2.1.9",
|
||||||
"@vitest/pretty-format": "^2.1.8",
|
"@vitest/pretty-format": "^2.1.9",
|
||||||
"@vitest/runner": "2.1.8",
|
"@vitest/runner": "2.1.9",
|
||||||
"@vitest/snapshot": "2.1.8",
|
"@vitest/snapshot": "2.1.9",
|
||||||
"@vitest/spy": "2.1.8",
|
"@vitest/spy": "2.1.9",
|
||||||
"@vitest/utils": "2.1.8",
|
"@vitest/utils": "2.1.9",
|
||||||
"chai": "^5.1.2",
|
"chai": "^5.1.2",
|
||||||
"debug": "^4.3.7",
|
"debug": "^4.3.7",
|
||||||
"expect-type": "^1.1.0",
|
"expect-type": "^1.1.0",
|
||||||
@ -5102,7 +5185,7 @@
|
|||||||
"tinypool": "^1.0.1",
|
"tinypool": "^1.0.1",
|
||||||
"tinyrainbow": "^1.2.0",
|
"tinyrainbow": "^1.2.0",
|
||||||
"vite": "^5.0.0",
|
"vite": "^5.0.0",
|
||||||
"vite-node": "2.1.8",
|
"vite-node": "2.1.9",
|
||||||
"why-is-node-running": "^2.3.0"
|
"why-is-node-running": "^2.3.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@ -5117,8 +5200,8 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@edge-runtime/vm": "*",
|
"@edge-runtime/vm": "*",
|
||||||
"@types/node": "^18.0.0 || >=20.0.0",
|
"@types/node": "^18.0.0 || >=20.0.0",
|
||||||
"@vitest/browser": "2.1.8",
|
"@vitest/browser": "2.1.9",
|
||||||
"@vitest/ui": "2.1.8",
|
"@vitest/ui": "2.1.9",
|
||||||
"happy-dom": "*",
|
"happy-dom": "*",
|
||||||
"jsdom": "*"
|
"jsdom": "*"
|
||||||
},
|
},
|
||||||
|
4
public/assets/icons/mapEditor/dropdown-chevron.svg
Normal file
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 |
3
public/assets/icons/mapEditor/search.svg
Normal file
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
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 |
Binary file not shown.
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 946 KiB |
Binary file not shown.
BIN
public/assets/music/intro.mp3
Normal file
BIN
public/assets/music/intro.mp3
Normal file
Binary file not shown.
BIN
public/assets/sounds/attack.wav
Normal file
BIN
public/assets/sounds/attack.wav
Normal file
Binary file not shown.
BIN
public/assets/sounds/button-click.wav
Normal file
BIN
public/assets/sounds/button-click.wav
Normal file
Binary file not shown.
BIN
public/assets/sounds/connect.wav
Normal file
BIN
public/assets/sounds/connect.wav
Normal file
Binary file not shown.
BIN
public/assets/sounds/walk.wav
Normal file
BIN
public/assets/sounds/walk.wav
Normal file
Binary file not shown.
18
src/App.vue
18
src/App.vue
@ -2,7 +2,7 @@
|
|||||||
<Debug />
|
<Debug />
|
||||||
<Notifications />
|
<Notifications />
|
||||||
<BackgroundImageLoader />
|
<BackgroundImageLoader />
|
||||||
<GmPanel v-if="gameStore.character?.role === 'gm'" @open-map-editor="mapEditor.toggleActive" />
|
<GmPanel v-if="gameStore.character?.role === 'gm'" />
|
||||||
<component :is="currentScreen" />
|
<component :is="currentScreen" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -17,12 +17,14 @@ import BackgroundImageLoader from '@/components/utilities/BackgroundImageLoader.
|
|||||||
import Debug from '@/components/utilities/Debug.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 { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
|
import { useSoundComposable } from '@/composables/useSoundComposable'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useMapEditorStore } from '@/stores/mapEditorStore'
|
import { computed, watch } from 'vue'
|
||||||
import { computed, ref, useTemplateRef, watch } from 'vue'
|
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
|
||||||
const mapEditor = useMapEditorComposable()
|
const mapEditor = useMapEditorComposable()
|
||||||
|
const { playSound } = useSoundComposable()
|
||||||
|
|
||||||
const currentScreen = computed(() => {
|
const currentScreen = computed(() => {
|
||||||
if (!gameStore.game.isLoaded) return Loading
|
if (!gameStore.game.isLoaded) return Loading
|
||||||
@ -42,13 +44,13 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// #209: Play sound when a button is pressed
|
// #209: Play sound when a button is pressed
|
||||||
// @TODO: Not all button-like elements will actually be a button, so we need to find a better way to do this
|
|
||||||
addEventListener('click', (event) => {
|
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
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
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: {
|
||||||
width: Number(import.meta.env.VITE_TILE_SIZE_WIDTH),
|
width: Number(import.meta.env.VITE_TILE_SIZE_WIDTH),
|
||||||
|
5
src/application/enums.ts
Normal file
5
src/application/enums.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export enum Direction {
|
||||||
|
POSITIVE,
|
||||||
|
NEGATIVE,
|
||||||
|
UNCHANGED
|
||||||
|
}
|
@ -26,7 +26,7 @@ export type TextureData = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Tile = {
|
export type Tile = {
|
||||||
id: UUID
|
id: string
|
||||||
name: string
|
name: string
|
||||||
tags: any | null
|
tags: any | null
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
@ -34,7 +34,7 @@ export type Tile = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type MapObject = {
|
export type MapObject = {
|
||||||
id: UUID
|
id: string
|
||||||
name: string
|
name: string
|
||||||
tags: any | null
|
tags: any | null
|
||||||
originX: number
|
originX: number
|
||||||
@ -47,7 +47,7 @@ export type MapObject = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Item = {
|
export type Item = {
|
||||||
id: UUID
|
id: string
|
||||||
name: string
|
name: string
|
||||||
description: string | null
|
description: string | null
|
||||||
itemType: ItemType
|
itemType: ItemType
|
||||||
@ -62,7 +62,7 @@ export type ItemType = 'WEAPON' | 'HELMET' | 'CHEST' | 'LEGS' | 'BOOTS' | 'GLOVE
|
|||||||
export type ItemRarity = 'COMMON' | 'UNCOMMON' | 'RARE' | 'EPIC' | 'LEGENDARY'
|
export type ItemRarity = 'COMMON' | 'UNCOMMON' | 'RARE' | 'EPIC' | 'LEGENDARY'
|
||||||
|
|
||||||
export type Map = {
|
export type Map = {
|
||||||
id: UUID
|
id: string
|
||||||
name: string
|
name: string
|
||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
@ -78,17 +78,14 @@ export type Map = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type MapEffect = {
|
export type MapEffect = {
|
||||||
id: UUID
|
id: string
|
||||||
map: Map
|
|
||||||
effect: string
|
effect: string
|
||||||
strength: number
|
strength: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PlacedMapObject = {
|
export type PlacedMapObject = {
|
||||||
id: UUID
|
id: string
|
||||||
map: Map
|
mapObject: MapObject | string
|
||||||
mapObject: MapObject
|
|
||||||
depth: number
|
|
||||||
isRotated: boolean
|
isRotated: boolean
|
||||||
positionX: number
|
positionX: number
|
||||||
positionY: number
|
positionY: number
|
||||||
@ -102,8 +99,8 @@ export enum MapEventTileType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type MapEventTile = {
|
export type MapEventTile = {
|
||||||
id: UUID
|
id: string
|
||||||
mapId: UUID
|
mapid: string
|
||||||
type: MapEventTileType
|
type: MapEventTileType
|
||||||
positionX: number
|
positionX: number
|
||||||
positionY: number
|
positionY: number
|
||||||
@ -111,7 +108,7 @@ export type MapEventTile = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type MapEventTileTeleport = {
|
export type MapEventTileTeleport = {
|
||||||
id: UUID
|
id: string
|
||||||
mapEventTile: MapEventTile
|
mapEventTile: MapEventTile
|
||||||
toMap: Map
|
toMap: Map
|
||||||
toPositionX: number
|
toPositionX: number
|
||||||
@ -120,7 +117,7 @@ export type MapEventTileTeleport = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
id: UUID
|
id: string
|
||||||
username: string
|
username: string
|
||||||
password: string
|
password: string
|
||||||
characters: Character[]
|
characters: Character[]
|
||||||
@ -140,7 +137,7 @@ 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
|
||||||
@ -151,7 +148,7 @@ export type CharacterType = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type CharacterHair = {
|
export type CharacterHair = {
|
||||||
id: UUID
|
id: string
|
||||||
name: string
|
name: string
|
||||||
sprite?: Sprite
|
sprite?: Sprite
|
||||||
gender: CharacterGender
|
gender: CharacterGender
|
||||||
@ -159,8 +156,8 @@ export type CharacterHair = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
@ -183,17 +180,18 @@ export type Character = {
|
|||||||
export type MapCharacter = {
|
export type MapCharacter = {
|
||||||
character: Character
|
character: Character
|
||||||
isMoving: boolean
|
isMoving: boolean
|
||||||
|
isAttacking?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CharacterItem = {
|
export type CharacterItem = {
|
||||||
id: UUID
|
id: string
|
||||||
character: Character
|
character: Character
|
||||||
item: Item
|
item: Item
|
||||||
quantity: number
|
quantity: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CharacterEquipment = {
|
export type CharacterEquipment = {
|
||||||
id: UUID
|
id: string
|
||||||
slot: CharacterEquipmentSlotType
|
slot: CharacterEquipmentSlotType
|
||||||
characterItem: CharacterItem
|
characterItem: CharacterItem
|
||||||
}
|
}
|
||||||
@ -208,7 +206,7 @@ export enum CharacterEquipmentSlotType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Sprite = {
|
export type Sprite = {
|
||||||
id: UUID
|
id: string
|
||||||
name: string
|
name: string
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
@ -255,6 +253,6 @@ export type WeatherState = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type mapLoadData = {
|
export type mapLoadData = {
|
||||||
mapId: UUID
|
mapId: string
|
||||||
characters: MapCharacter[]
|
characters: MapCharacter[]
|
||||||
}
|
}
|
||||||
|
@ -7,25 +7,8 @@ export function uuidv4() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
|
||||||
// Check if not localhost
|
|
||||||
if (window.location.hostname !== 'localhost') {
|
|
||||||
return window.location.hostname
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if not IP address
|
|
||||||
if (window.location.hostname.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) {
|
|
||||||
return window.location.hostname
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.location.hostname.split('.').length < 3) {
|
|
||||||
return window.location.hostname
|
|
||||||
}
|
|
||||||
|
|
||||||
return window.location.hostname.split('.').slice(-2).join('.')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function downloadCache<T extends { id: string; updatedAt: Date }>(endpoint: string, storage: BaseStorage<T>) {
|
export async function downloadCache<T extends { id: string; updatedAt: Date }>(endpoint: string, storage: BaseStorage<T>) {
|
||||||
@ -41,7 +24,7 @@ export async function downloadCache<T extends { id: string; updatedAt: Date }>(e
|
|||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
let overwrite = false
|
let overwrite = false
|
||||||
const existingItem = await storage.get(item.id)
|
const existingItem = await storage.getById(item.id)
|
||||||
|
|
||||||
if (!existingItem || item.updatedAt > existingItem.updatedAt) {
|
if (!existingItem || item.updatedAt > existingItem.updatedAt) {
|
||||||
overwrite = true
|
overwrite = true
|
||||||
|
@ -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 {
|
||||||
@ -122,6 +128,15 @@ button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.btn-indigo {
|
||||||
|
@apply bg-indigo-500 text-gray-50 text-base leading-5 rounded py-2.5;
|
||||||
|
|
||||||
|
&.active,
|
||||||
|
&:hover {
|
||||||
|
@apply bg-indigo-600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.btn-empty {
|
&.btn-empty {
|
||||||
@apply text-gray-50 border-2 border-solid border-gray-500 text-base leading-5 rounded py-2.5;
|
@apply text-gray-50 border-2 border-solid border-gray-500 text-base leading-5 rounded py-2.5;
|
||||||
|
|
||||||
@ -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,141 +1,41 @@
|
|||||||
<template>
|
<template>
|
||||||
<ChatBubble :mapCharacter="props.mapCharacter" :currentX="currentPositionX" :currentY="currentPositionY" />
|
<Container ref="characterContainer" :x="currentPositionX" :y="currentPositionY" :depth="isometricDepth">
|
||||||
<Healthbar :mapCharacter="props.mapCharacter" :currentX="currentPositionX" :currentY="currentPositionY" />
|
<ChatBubble :mapCharacter="props.mapCharacter" />
|
||||||
<CharacterHair :mapCharacter="props.mapCharacter" :currentX="currentPositionX" :currentY="currentPositionY" />
|
<HealthBar :mapCharacter="props.mapCharacter" />
|
||||||
<Sprite ref="charSprite" :depth="isometricDepth" :x="currentPositionX" :y="currentPositionY" :origin-y="1" :flipX="isFlippedX" />
|
<CharacterHair :mapCharacter="props.mapCharacter" />
|
||||||
|
<Sprite ref="characterSprite" :origin-y="1" :flipX="isFlippedX" />
|
||||||
|
</Container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import config from '@/application/config'
|
|
||||||
import { type MapCharacter } from '@/application/types'
|
import { type MapCharacter } 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/mapComposable'
|
import { useSoundComposable } from '@/composables/useSoundComposable'
|
||||||
import { CharacterTypeStorage } from '@/storage/storages'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useMapStore } from '@/stores/mapStore'
|
import { useMapStore } from '@/stores/mapStore'
|
||||||
import { 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'
|
||||||
|
|
||||||
enum Direction {
|
|
||||||
POSITIVE,
|
|
||||||
NEGATIVE,
|
|
||||||
UNCHANGED
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
tilemap: Phaser.Tilemaps.Tilemap
|
tileMap: Phaser.Tilemaps.Tilemap
|
||||||
mapCharacter: MapCharacter
|
mapCharacter: MapCharacter
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const charSprite = refObj<Phaser.GameObjects.Sprite>()
|
|
||||||
const charSpriteId = ref('')
|
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const mapStore = useMapStore()
|
const mapStore = useMapStore()
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
|
|
||||||
const currentPositionX = ref(0)
|
const { characterContainer, characterSprite, currentPositionX, currentPositionY, isometricDepth, isFlippedX, updatePosition, playAnimation, calcDirection, updateSprite, initializeSprite, cleanup } = useCharacterSpriteComposable(scene, props.tileMap, props.mapCharacter)
|
||||||
const currentPositionY = ref(0)
|
const { playSound, stopSound } = useSoundComposable()
|
||||||
const isometricDepth = ref(1)
|
|
||||||
const isInitialPosition = ref(true)
|
|
||||||
const tween = ref<Phaser.Tweens.Tween | null>(null)
|
|
||||||
|
|
||||||
const updateIsometricDepth = (positionX: number, positionY: number) => {
|
|
||||||
isometricDepth.value = calculateIsometricDepth(positionX, positionY, 28, 94, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatePosition = (positionX: number, positionY: number, direction: Direction) => {
|
|
||||||
const newPositionX = tileToWorldX(props.tilemap, positionX, positionY)
|
|
||||||
const newPositionY = tileToWorldY(props.tilemap, positionX, positionY)
|
|
||||||
|
|
||||||
if (isInitialPosition.value) {
|
|
||||||
currentPositionX.value = newPositionX
|
|
||||||
currentPositionY.value = newPositionY
|
|
||||||
isInitialPosition.value = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tween.value?.isPlaying()) {
|
|
||||||
tween.value.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
const distance = Math.sqrt(Math.pow(newPositionX - currentPositionX.value, 2) + Math.pow(newPositionY - currentPositionY.value, 2))
|
|
||||||
|
|
||||||
if (distance >= config.tile_size.width / 1.1) {
|
|
||||||
currentPositionX.value = newPositionX
|
|
||||||
currentPositionY.value = newPositionY
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const duration = distance * 5.7
|
|
||||||
|
|
||||||
tween.value = props.tilemap.scene.tweens.add({
|
|
||||||
targets: { x: currentPositionX.value, y: currentPositionY.value },
|
|
||||||
x: newPositionX,
|
|
||||||
y: newPositionY,
|
|
||||||
duration,
|
|
||||||
ease: 'Linear',
|
|
||||||
onStart: () => {
|
|
||||||
if (direction === Direction.POSITIVE) {
|
|
||||||
updateIsometricDepth(positionX, positionY)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onUpdate: (tween) => {
|
|
||||||
// @ts-ignore
|
|
||||||
currentPositionX.value = tween.targets[0].x
|
|
||||||
// @ts-ignore
|
|
||||||
currentPositionY.value = tween.targets[0].y
|
|
||||||
},
|
|
||||||
onComplete: () => {
|
|
||||||
if (direction === Direction.NEGATIVE) {
|
|
||||||
updateIsometricDepth(positionX, positionY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const calcDirection = (oldPositionX: number, oldPositionY: number, newPositionX: number, newPositionY: number): Direction => {
|
|
||||||
if (newPositionY < oldPositionY || newPositionX < oldPositionX) return Direction.NEGATIVE
|
|
||||||
if (newPositionX > oldPositionX || newPositionY > oldPositionY) return Direction.POSITIVE
|
|
||||||
return Direction.UNCHANGED
|
|
||||||
}
|
|
||||||
|
|
||||||
const isFlippedX = computed(() => [6, 4].includes(props.mapCharacter.character.rotation ?? 0))
|
|
||||||
|
|
||||||
const currentDirection = computed(() => {
|
|
||||||
return [0, 6].includes(props.mapCharacter.character.rotation ?? 0) ? 'left_up' : 'right_down'
|
|
||||||
})
|
|
||||||
|
|
||||||
const currentAction = computed(() => {
|
|
||||||
return props.mapCharacter.isMoving ? 'walk' : 'idle'
|
|
||||||
})
|
|
||||||
|
|
||||||
const charTexture = computed(() => {
|
|
||||||
const spriteId = charSpriteId.value ?? 'idle_right_down'
|
|
||||||
return `${spriteId}-${currentAction.value}_${currentDirection.value}`
|
|
||||||
})
|
|
||||||
|
|
||||||
const updateSprite = () => {
|
|
||||||
if (!charSprite.value) return
|
|
||||||
|
|
||||||
if (props.mapCharacter.isMoving) {
|
|
||||||
charSprite.value.anims.play(charTexture.value, true)
|
|
||||||
} else {
|
|
||||||
charSprite.value.anims.stop()
|
|
||||||
charSprite.value.setFrame(0)
|
|
||||||
charSprite.value.setTexture(charTexture.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePositionUpdate = (newValues: any, oldValues: any) => {
|
const handlePositionUpdate = (newValues: any, oldValues: any) => {
|
||||||
if (!newValues) return
|
if (!newValues) return
|
||||||
|
|
||||||
if (!oldValues || newValues.positionX !== oldValues.positionX || newValues.positionY !== oldValues.positionY) {
|
if (!oldValues || newValues.positionX !== oldValues.positionX || newValues.positionY !== oldValues.positionY) {
|
||||||
const direction = !oldValues ? Direction.POSITIVE : calcDirection(oldValues.positionX, oldValues.positionY, newValues.positionX, newValues.positionY)
|
updatePosition(newValues.positionX, newValues.positionY)
|
||||||
updatePosition(newValues.positionX, newValues.positionY, direction)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newValues.isMoving !== oldValues?.isMoving || newValues.rotation !== oldValues?.rotation) {
|
if (newValues.isMoving !== oldValues?.isMoving || newValues.rotation !== oldValues?.rotation) {
|
||||||
@ -143,45 +43,61 @@ const handlePositionUpdate = (newValues: any, oldValues: any) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(
|
watch(
|
||||||
() => ({
|
() => ({
|
||||||
positionX: props.mapCharacter.character.positionX,
|
positionX: props.mapCharacter.character.positionX,
|
||||||
positionY: props.mapCharacter.character.positionY,
|
positionY: props.mapCharacter.character.positionY,
|
||||||
isMoving: props.mapCharacter.isMoving,
|
isMoving: props.mapCharacter.isMoving,
|
||||||
rotation: props.mapCharacter.character.rotation
|
rotation: props.mapCharacter.character.rotation,
|
||||||
|
isAttacking: props.mapCharacter.isAttacking
|
||||||
}),
|
}),
|
||||||
handlePositionUpdate
|
(oldValues, newValues) => {
|
||||||
|
handlePositionUpdate(oldValues, newValues)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
let character = props.mapCharacter.character
|
await initializeSprite()
|
||||||
|
if (props.mapCharacter.character.id === gameStore.character!.id) {
|
||||||
const characterTypeStorage = new CharacterTypeStorage()
|
|
||||||
|
|
||||||
const spriteId = await characterTypeStorage.getSpriteId(character.characterType!)
|
|
||||||
if (!spriteId) return
|
|
||||||
|
|
||||||
charSpriteId.value = spriteId
|
|
||||||
|
|
||||||
await loadSpriteTextures(scene, spriteId)
|
|
||||||
|
|
||||||
if (charSprite.value) {
|
|
||||||
charSprite.value.setTexture(charTexture.value)
|
|
||||||
charSprite.value.setFlipX(isFlippedX.value)
|
|
||||||
charSprite.value.setName(props.mapCharacter.character.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (character.id === gameStore.character!.id) {
|
|
||||||
mapStore.setCharacterLoaded(true)
|
mapStore.setCharacterLoaded(true)
|
||||||
|
scene.cameras.main.startFollow(characterContainer.value as Phaser.GameObjects.Container)
|
||||||
// #146 : Set camera position to character, need to be improved still
|
|
||||||
scene.cameras.main.startFollow(charSprite.value as Phaser.GameObjects.Sprite)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePosition(character.positionX, character.positionY, character.rotation)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
tween.value?.stop()
|
cleanup()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { MapCharacter, Sprite as SpriteT } 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 { useGameStore } from '@/stores/gameStore'
|
||||||
import { Image, useScene } from 'phavuer'
|
import { Image, useScene } from 'phavuer'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { MapCharacter, Sprite as SpriteT } from '@/application/types'
|
import type { MapCharacter, Sprite as SpriteT } from '@/application/types'
|
||||||
import { loadSpriteTextures } from '@/composables/gameComposable'
|
import { loadSpriteTextures } from '@/services/textureService'
|
||||||
import { CharacterHairStorage, CharacterTypeStorage, SpriteStorage } from '@/storage/storages'
|
import { CharacterHairStorage, CharacterTypeStorage, SpriteStorage } from '@/storage/storages'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { Image, useScene } from 'phavuer'
|
import { Image, useScene } from 'phavuer'
|
||||||
@ -12,8 +12,6 @@ import { computed, onMounted, ref } from 'vue'
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
mapCharacter: MapCharacter
|
mapCharacter: MapCharacter
|
||||||
currentX: number
|
|
||||||
currentY: number
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
@ -39,9 +37,7 @@ const imageProps = computed(() => {
|
|||||||
originX: Number(spriteAction?.originX) ?? 0,
|
originX: Number(spriteAction?.originX) ?? 0,
|
||||||
originY: Number(spriteAction?.originY) ?? 0,
|
originY: Number(spriteAction?.originY) ?? 0,
|
||||||
flipX: isFlippedX.value,
|
flipX: isFlippedX.value,
|
||||||
texture: texture.value,
|
texture: texture.value
|
||||||
y: props.currentY,
|
|
||||||
x: props.currentX
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -52,7 +48,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
hairSpriteId.value = spriteId
|
hairSpriteId.value = spriteId
|
||||||
const spriteStorage = new SpriteStorage()
|
const spriteStorage = new SpriteStorage()
|
||||||
sprite.value = await spriteStorage.get(spriteId)
|
sprite.value = await spriteStorage.getById(spriteId)
|
||||||
await loadSpriteTextures(scene, spriteId)
|
await loadSpriteTextures(scene, spriteId)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Container ref="charChatContainer" :depth="999" :x="currentX" :y="currentY">
|
<Container ref="characterChatContainer" :depth="999">
|
||||||
<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>
|
||||||
@ -12,12 +12,10 @@ import { onMounted } from 'vue'
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
mapCharacter: MapCharacter
|
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.mapCharacter.character.name}_chatBubble`)
|
container.setName(`${props.mapCharacter.character.name}_chatBubble`)
|
||||||
@ -41,7 +39,7 @@ const createChatText = (text: Phaser.GameObjects.Text) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
charChatContainer.value!.setName(`${props.mapCharacter.character!.name}_chatContainer`)
|
characterChatContainer.value!.setName(`${props.mapCharacter.character!.name}_chatContainer`)
|
||||||
charChatContainer.value!.setVisible(false)
|
characterChatContainer.value!.setVisible(false)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Container :depth="999" :x="currentX" :y="currentY">
|
<Container :depth="999">
|
||||||
<Text @create="createNicknameText" :text="props.mapCharacter.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" />
|
||||||
@ -12,8 +12,6 @@ import { Container, RoundRectangle, Text, useGame } from 'phavuer'
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
mapCharacter: MapCharacter
|
mapCharacter: MapCharacter
|
||||||
currentX: number
|
|
||||||
currentY: number
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const game = useGame()
|
const game = useGame()
|
@ -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>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
</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..."
|
||||||
@ -85,11 +85,14 @@ gameStore.connection?.on('chat:message', (data: Chat) => {
|
|||||||
|
|
||||||
if (!mapStore.characterLoaded) return
|
if (!mapStore.characterLoaded) return
|
||||||
|
|
||||||
const charChatContainer = scene.children.getByName(data.character.name + '_chatContainer') as Phaser.GameObjects.Container
|
const characterContainer = scene.children.getByName(data.character.name) as Phaser.GameObjects.Container
|
||||||
if (!charChatContainer) return
|
if (!characterContainer) return
|
||||||
|
|
||||||
const chatBubble = charChatContainer.getByName(data.character.name + '_chatBubble') as Phaser.GameObjects.Container
|
const characterChatContainer = characterContainer.getByName(data.character.name + '_chatContainer') as Phaser.GameObjects.Container
|
||||||
const chatText = charChatContainer.getByName(data.character.name + '_chatText') as Phaser.GameObjects.Text
|
if (!characterChatContainer) return
|
||||||
|
|
||||||
|
const chatBubble = characterChatContainer.getByName(data.character.name + '_chatBubble') as Phaser.GameObjects.Container
|
||||||
|
const chatText = characterChatContainer.getByName(data.character.name + '_chatText') as Phaser.GameObjects.Text
|
||||||
if (!chatText || !chatBubble) return
|
if (!chatText || !chatBubble) return
|
||||||
|
|
||||||
function calculateTextWidth(text: string, font: string, fontSize: number): number {
|
function calculateTextWidth(text: string, font: string, fontSize: number): number {
|
||||||
@ -115,24 +118,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()
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="absolute z-50 w-full h-dvh top-0 left-0 bg-black/60" v-show="false">
|
|
||||||
<div class="center-element max-w-[875px] max-h-[600px] h-full w-[80%] bg-gray border-solid border-2 border-gray-500 rounded-md z-50 flex flex-col backdrop-blur-sm shadow-lg">
|
|
||||||
<div class="p-2.5 flex max-sm:flex-wrap justify-between items-center gap-5 border-solid border-0 border-b border-gray-500">
|
|
||||||
<h3 class="m-0 font-medium shrink-0">Game menu</h3>
|
|
||||||
<div class="hidden sm:flex gap-1.5 flex-wrap">
|
|
||||||
<button @click.stop="userPanelScreen = 'inventory'" :class="{ active: userPanelScreen === 'inventory' }" class="btn-cyan py-1.5 px-4 min-w-24">Inventory</button>
|
|
||||||
<button @click.stop="userPanelScreen = 'equipment'" :class="{ active: userPanelScreen === 'equipment' }" class="btn-cyan py-1.5 px-4 min-w-24">Equipment</button>
|
|
||||||
<button @click.stop="userPanelScreen = 'characterScreen'" :class="{ active: userPanelScreen === 'characterScreen' }" class="btn-cyan py-1.5 px-4 min-w-24">Character</button>
|
|
||||||
<button @click.stop="userPanelScreen = 'settings'" :class="{ active: userPanelScreen === 'settings' }" class="btn-cyan py-1.5 px-4 min-w-24">Settings</button>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-2.5">
|
|
||||||
<button class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
|
|
||||||
<img alt="close" draggable="false" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="flex sm:hidden gap-1.5 flex-wrap">
|
|
||||||
<button @click.stop="userPanelScreen = 'inventory'" :class="{ active: userPanelScreen === 'inventory' }" class="btn-cyan py-1.5 px-4 min-w-24">Inventory</button>
|
|
||||||
<button @click.stop="userPanelScreen = 'equipment'" :class="{ active: userPanelScreen === 'equipment' }" class="btn-cyan py-1.5 px-4 min-w-24">Equipment</button>
|
|
||||||
<button @click.stop="userPanelScreen = 'characterScreen'" :class="{ active: userPanelScreen === 'characterScreen' }" class="btn-cyan py-1.5 px-4 min-w-24">Character</button>
|
|
||||||
<button @click.stop="userPanelScreen = 'settings'" :class="{ active: userPanelScreen === 'settings' }" class="btn-cyan py-1.5 px-4 min-w-24">Settings</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Inventory v-show="userPanelScreen === 'inventory'" />
|
|
||||||
<Equipment v-show="userPanelScreen === 'equipment'" />
|
|
||||||
<CharacterScreen v-show="userPanelScreen === 'characterScreen'" />
|
|
||||||
<Settings v-show="userPanelScreen === 'settings'" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import CharacterScreen from '@/components/game/gui/partials/CharacterScreen.vue'
|
|
||||||
import Equipment from '@/components/game/gui/partials/Equipment.vue'
|
|
||||||
import Inventory from '@/components/game/gui/partials/Inventory.vue'
|
|
||||||
import Settings from '@/components/game/gui/partials/Settings.vue'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
let userPanelScreen = ref('inventory')
|
|
||||||
</script>
|
|
@ -1,68 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="grow flex flex-col w-full h-full relative overflow-auto">
|
|
||||||
<div class="m-4 relative">
|
|
||||||
<h4 class="font-medium text-lg max-w-[375px]">Character</h4>
|
|
||||||
<div class="flex justify-center flex-wrap gap-20">
|
|
||||||
<div class="flex gap-3 mt-2 flex-wrap max-w-[375px]">
|
|
||||||
<div class="h-full flex flex-col justify-center items-center">
|
|
||||||
<img class="h-72 my-2 mx-auto" src="/assets/placeholders/inventory_player.png" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
<div class="flex flex-col gap-3 mx-5 mt-2">
|
|
||||||
<h3>{{ gameStore.character?.name }}</h3>
|
|
||||||
<div class="flex gap-4 flex-wrap max-w-[375px]">
|
|
||||||
<ul class="p-0 m-0 list-none">
|
|
||||||
<li class="leading-6 text-lg">Class:</li>
|
|
||||||
<li class="leading-6 text-lg">Race:</li>
|
|
||||||
<li class="leading-6 text-lg">Hit Points:</li>
|
|
||||||
<li class="leading-6 text-lg">Mana Points:</li>
|
|
||||||
<li class="leading-6 text-lg">Level:</li>
|
|
||||||
<li class="leading-6 text-lg">Stat Points:</li>
|
|
||||||
</ul>
|
|
||||||
<ul class="p-0 m-0 list-none">
|
|
||||||
<li class="leading-6 text-lg min-h-6">Knight</li>
|
|
||||||
<li class="leading-6 text-lg min-h-6">{{ gameStore.character?.characterType?.race }}</li>
|
|
||||||
<li class="leading-6 text-lg min-h-6">{{ gameStore.character?.hitpoints }} <span class="text-green">(+15)</span></li>
|
|
||||||
<li class="leading-6 text-lg min-h-6">{{ gameStore.character?.mana }}</li>
|
|
||||||
<li class="leading-6 text-lg min-h-6">{{ gameStore.character?.level }}</li>
|
|
||||||
<li class="leading-6 text-lg min-h-6">3</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-3 mx-5 mt-2">
|
|
||||||
<h3>Alignment</h3>
|
|
||||||
<div class="h-8 w-64 rounded border border-solid border-white bg-gradient-to-r from-red to-blue relative">
|
|
||||||
<!-- TODO: Give slider left value based on alignment (0-100), new characters start with 50 -->
|
|
||||||
<div class="rounded border-2 border-solid border-white h-10 w-2 absolute top-1/2 -translate-y-1/2 -translate-x-1/2" :style="{ left: gameStore.character?.alignment + '%' }"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="absolute -left-5 -bottom-5 w-[calc(100%_+_40px)] my-px h-px bg-gray-500"></div>
|
|
||||||
</div>
|
|
||||||
<div class="m-4">
|
|
||||||
<h4 class="font-medium text-lg max-w-[375px]">Character stats</h4>
|
|
||||||
<div class="flex gap-3 mt-4 flex-wrap max-w-[375px]">
|
|
||||||
<ul class="p-0 m-0 list-none">
|
|
||||||
<li class="leading-6">Health:</li>
|
|
||||||
<li class="leading-6">Defense:</li>
|
|
||||||
<li class="leading-6">More stats:</li>
|
|
||||||
<li class="leading-6">Extra stats:</li>
|
|
||||||
</ul>
|
|
||||||
<ul class="p-0 m-0 list-none text-right">
|
|
||||||
<li class="leading-6">100 <span class="text-green">(+15)</span></li>
|
|
||||||
<li class="leading-6">1000 <span class="text-green">(+500)</span></li>
|
|
||||||
<li class="leading-6"></li>
|
|
||||||
<li class="leading-6"></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
</script>
|
|
@ -1,89 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="grow flex flex-col w-full h-full relative overflow-auto">
|
|
||||||
<div class="m-4 relative">
|
|
||||||
<h4 class="font-medium text-lg max-w-[375px]">Equipment</h4>
|
|
||||||
<div class="flex justify-center items-center flex-wrap gap-20">
|
|
||||||
<div class="flex gap-3 mt-2 flex-wrap max-w-[375px]">
|
|
||||||
<div class="h-full flex flex-col justify-center items-center">
|
|
||||||
<h3>{{ gameStore.character?.name }}</h3>
|
|
||||||
<img class="h-60 my-2 mx-auto" src="/assets/placeholders/inventory_player.png" />
|
|
||||||
<span class="block text-sm">Level {{ gameStore.character?.level }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-3 mx-5 mt-2">
|
|
||||||
<div class="flex gap-3 justify-center">
|
|
||||||
<!-- Helmet -->
|
|
||||||
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square h-11 w-11 relative hover:bg-gray-200">
|
|
||||||
<img src="/assets/icons/inventory/helmet.svg" class="center-element w-11/12 opacity-20" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Head charm -->
|
|
||||||
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square h-11 w-11 relative hover:bg-gray-200">
|
|
||||||
<img src="/assets/icons/inventory/head_charm.svg" class="center-element w-11/12 opacity-20" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex gap-3 justify-center">
|
|
||||||
<!-- Bracers -->
|
|
||||||
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md w-11 h-[104px] relative hover:bg-gray-200">
|
|
||||||
<img src="/assets/icons/inventory/bracers.svg" class="center-element w-11/12 opacity-20" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Chestplate -->
|
|
||||||
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square w-[104px] h-[104px] relative hover:bg-gray-200">
|
|
||||||
<img src="/assets/icons/inventory/chestplate.svg" class="center-element w-10/12 opacity-20" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Primary Weapon -->
|
|
||||||
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md w-11 h-[104px] self-stretch justify-self-stretch relative hover:bg-gray-200">
|
|
||||||
<img src="/assets/icons/inventory/primary_weapon.svg" class="center-element w-11/12 opacity-20" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex gap-3 justify-center">
|
|
||||||
<!-- Legs -->
|
|
||||||
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md w-11 h-[104px] self-stretch justify-self-stretch relative hover:bg-gray-200">
|
|
||||||
<img src="/assets/icons/inventory/legs.svg" class="center-element w-11/12 opacity-20" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-3">
|
|
||||||
<!-- Belt/pouch -->
|
|
||||||
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square h-11 w-11 self-stretch justify-self-stretch relative hover:bg-gray-200">
|
|
||||||
<img src="/assets/icons/inventory/pouch.svg" class="center-element w-11/12 opacity-20" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Boots -->
|
|
||||||
<div class="bg-gray-300/80 border-solid border-2 border-gray-500 rounded-md aspect-square h-11 w-11 self-stretch justify-self-stretch relative hover:bg-gray-200">
|
|
||||||
<img src="/assets/icons/inventory/boots.svg" class="center-element w-11/12 opacity-20" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="absolute -left-5 -bottom-5 w-[calc(100%_+_40px)] my-px h-px bg-gray-500"></div>
|
|
||||||
</div>
|
|
||||||
<div class="m-4">
|
|
||||||
<h4 class="font-medium text-lg max-w-[375px]">Equipment Bonus</h4>
|
|
||||||
<div class="flex gap-3 mt-4 flex-wrap max-w-[375px]">
|
|
||||||
<ul class="p-0 m-0 list-none">
|
|
||||||
<li class="leading-6">Health:</li>
|
|
||||||
<li class="leading-6">Defense:</li>
|
|
||||||
<li class="leading-6">More stats:</li>
|
|
||||||
<li class="leading-6">Extra stats:</li>
|
|
||||||
</ul>
|
|
||||||
<ul class="p-0 m-0 list-none text-right">
|
|
||||||
<li class="leading-6">+15</li>
|
|
||||||
<li class="leading-6">500</li>
|
|
||||||
<li class="leading-6"></li>
|
|
||||||
<li class="leading-6"></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
</script>
|
|
@ -1,17 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="grow flex flex-col w-full h-full relative overflow-auto">
|
|
||||||
<div class="m-4 relative">
|
|
||||||
<h4 class="m-auto font-medium text-lg max-w-[375px]">Inventory</h4>
|
|
||||||
<div class="flex gap-3 mt-4 mx-auto flex-wrap max-w-[375px]">
|
|
||||||
<div v-for="n in 24" class="bg-gray-300/80 border-solid border-2 border-gray-500 w-12 h-12 rounded-md aspect-square shrink-0 justify-self-stretch hover:bg-gray-200"></div>
|
|
||||||
</div>
|
|
||||||
<div class="absolute -left-5 -bottom-5 w-[calc(100%_+_40px)] my-px h-px bg-gray-500"></div>
|
|
||||||
</div>
|
|
||||||
<div class="m-4">
|
|
||||||
<h4 class="m-auto font-medium text-lg max-w-[375px]">Chest items</h4>
|
|
||||||
<div class="flex gap-3 mt-4 mx-auto flex-wrap max-w-[375px]">
|
|
||||||
<div v-for="n in 12" class="bg-gray-300/80 border-solid border-2 border-gray-500 w-12 h-12 rounded-md aspect-square shrink-0 justify-self-stretch hover:bg-gray-200"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
@ -1,40 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex h-full w-full relative">
|
|
||||||
<div class="w-2/12 flex flex-col relative">
|
|
||||||
<!-- Settings Categories -->
|
|
||||||
<div class="relative p-2.5">
|
|
||||||
<h3>Settings</h3>
|
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</div>
|
|
||||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'character' }" @click.stop="settingCategory = 'character'">
|
|
||||||
<span>Character</span>
|
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
|
||||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'account' }" @click.stop="settingCategory = 'account'">
|
|
||||||
<span>Account</span>
|
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
|
||||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'audio' }" @click.stop="settingCategory = 'audio'">
|
|
||||||
<span>Audio</span>
|
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
|
||||||
<a class="relative p-2.5 hover:cursor-pointer" :class="{ 'bg-cyan/80': settingCategory === 'video' }" @click.stop="settingCategory = 'video'">
|
|
||||||
<span>Video</span>
|
|
||||||
<div class="absolute left-0 bottom-0 w-full h-px bg-gray-500"></div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="absolute w-px bg-gray-500 h-full top-0 left-1/6"></div>
|
|
||||||
|
|
||||||
<!-- Assets list -->
|
|
||||||
<div class="overflow-auto h-full w-10/12 flex flex-col relative">
|
|
||||||
<CharacterSettings v-show="settingCategory == 'character'" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import CharacterSettings from '@/components/game/gui/partials/settings/CharacterSettings.vue'
|
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
let settingCategory = ref('character')
|
|
||||||
</script>
|
|
@ -1,34 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="h-full overflow-auto">
|
|
||||||
<div class="relative p-2.5 flex flex-col gap-5 h-72">
|
|
||||||
<h3>Character details</h3>
|
|
||||||
<button @click="toggle" class="btn-cyan px-4 py-1.5 w-24">Edit</button>
|
|
||||||
<form class="flex gap-2.5 flex-wrap">
|
|
||||||
<div class="form-field-half max-w-[300px]">
|
|
||||||
<label for="name">Name</label>
|
|
||||||
<input class="input-field" :class="{ inactive: !editCharacter }" type="text" name="name" placeholder="Ethereal" :disabled="!editCharacter" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field-half max-w-[300px] relative">
|
|
||||||
<label for="class">Class</label>
|
|
||||||
<select class="input-field" v-model="characterClass" :class="{ inactive: !editCharacter }" name="class" :disabled="!editCharacter">
|
|
||||||
<option value="Knight" :selected="characterClass == 'Knight'" :disabled="characterClass == 'Knight'">Knight</option>
|
|
||||||
<option value="Paladin" :selected="characterClass == 'Paladin'" :disabled="characterClass == 'Paladin'">Paladin</option>
|
|
||||||
</select>
|
|
||||||
<span v-if="!editCharacter" class="absolute bottom-[9px] left-[14px] text-sm text-gray-300/50">{{ characterClass }}</span>
|
|
||||||
</div>
|
|
||||||
<button v-if="editCharacter" @click="toggle" class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
const editCharacter = ref(false)
|
|
||||||
const characterClass = ref('')
|
|
||||||
|
|
||||||
const toggle = () => {
|
|
||||||
editCharacter.value = !editCharacter.value
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -1,14 +1,46 @@
|
|||||||
<template>
|
<template>
|
||||||
<Character v-for="item in mapStore.characters" :key="item.character.id" :tilemap="tilemap" :mapCharacter="item" />
|
<Character v-for="item in mapStore.characters" :key="item.character.id" :tileMap :mapCharacter="item" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { MapCharacter, UUID } from '@/application/types'
|
||||||
import Character from '@/components/game/character/Character.vue'
|
import Character from '@/components/game/character/Character.vue'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useMapStore } from '@/stores/mapStore'
|
import { useMapStore } from '@/stores/mapStore'
|
||||||
|
import { onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
const mapStore = useMapStore()
|
const mapStore = useMapStore()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
tilemap: Phaser.Tilemaps.Tilemap
|
tileMap: Phaser.Tilemaps.Tilemap
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
gameStore.connection?.on('map:character:join', async (data: MapCharacter) => {
|
||||||
|
mapStore.addCharacter(data)
|
||||||
|
})
|
||||||
|
|
||||||
|
gameStore.connection?.on('map:character:leave', (characterId: UUID) => {
|
||||||
|
mapStore.removeCharacter(characterId)
|
||||||
|
})
|
||||||
|
|
||||||
|
gameStore.connection?.on('map:character:move', (data: { characterId: UUID; positionX: number; positionY: number; rotation: number; isMoving: boolean }) => {
|
||||||
|
mapStore.updateCharacterPosition(data)
|
||||||
|
// @TODO: Replace with universal class, composable or store
|
||||||
|
if (data.characterId === gameStore.character?.id) {
|
||||||
|
gameStore.character!.positionX = data.positionX
|
||||||
|
gameStore.character!.positionY = data.positionY
|
||||||
|
gameStore.character!.rotation = data.rotation
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
gameStore.connection?.on('map:character:attack', (characterId: UUID) => {
|
||||||
|
mapStore.updateCharacterProperty(characterId, 'isAttacking', true)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
gameStore.connection?.off('map:character:join')
|
||||||
|
gameStore.connection?.off('map:character:leave')
|
||||||
|
gameStore.connection?.off('map:character:move')
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,22 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<MapTiles :key="mapStore.mapId" @tileMap:create="tileMap = $event" />
|
<MapTiles v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer />
|
||||||
<PlacedMapObjects v-if="tileMap" :key="mapStore.mapId" :tilemap="tileMap" />
|
<PlacedMapObjects v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer />
|
||||||
<Characters v-if="tileMap && mapStore.characters" :tilemap="tileMap" />
|
<Characters v-if="tileMap && mapStore.characters" :tileMap />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { MapCharacter, mapLoadData, UUID } from '@/application/types'
|
import type { mapLoadData } from '@/application/types'
|
||||||
|
import { unduplicateArray } from '@/application/utilities'
|
||||||
import Characters from '@/components/game/map/Characters.vue'
|
import Characters from '@/components/game/map/Characters.vue'
|
||||||
import MapTiles from '@/components/game/map/MapTiles.vue'
|
import MapTiles from '@/components/game/map/MapTiles.vue'
|
||||||
import PlacedMapObjects from '@/components/game/map/PlacedMapObjects.vue'
|
import PlacedMapObjects from '@/components/game/map/PlacedMapObjects.vue'
|
||||||
|
import { createTileLayer, createTileMap, loadTileTexturesFromMapTileArray } from '@/services/mapService'
|
||||||
|
import { MapStorage } from '@/storage/storages'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useMapStore } from '@/stores/mapStore'
|
import { useMapStore } from '@/stores/mapStore'
|
||||||
import { onUnmounted, shallowRef } from 'vue'
|
import { useScene } from 'phavuer'
|
||||||
|
import { onMounted, onUnmounted, shallowRef, watch } from 'vue'
|
||||||
|
|
||||||
|
const scene = useScene()
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const mapStore = useMapStore()
|
const mapStore = useMapStore()
|
||||||
|
|
||||||
|
const mapStorage = new MapStorage()
|
||||||
|
|
||||||
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
|
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
|
||||||
|
const tileMapLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
|
||||||
|
|
||||||
// Event listeners
|
// Event listeners
|
||||||
gameStore.connection?.on('map:character:teleport', async (data: mapLoadData) => {
|
gameStore.connection?.on('map:character:teleport', async (data: mapLoadData) => {
|
||||||
@ -24,23 +33,37 @@ gameStore.connection?.on('map:character:teleport', async (data: mapLoadData) =>
|
|||||||
mapStore.setCharacters(data.characters)
|
mapStore.setCharacters(data.characters)
|
||||||
})
|
})
|
||||||
|
|
||||||
gameStore.connection?.on('map:character:join', async (data: MapCharacter) => {
|
async function initialize() {
|
||||||
mapStore.addCharacter(data)
|
if (!mapStore.mapId) return
|
||||||
})
|
|
||||||
|
|
||||||
gameStore.connection?.on('map:character:leave', (characterId: UUID) => {
|
const map = await mapStorage.getById(mapStore.mapId)
|
||||||
mapStore.removeCharacter(characterId)
|
if (!map) return
|
||||||
})
|
|
||||||
|
|
||||||
gameStore.connection?.on('map:character:move', (data: { characterId: UUID; positionX: number; positionY: number; rotation: number; isMoving: boolean }) => {
|
await loadTileTexturesFromMapTileArray(mapStore.mapId, scene)
|
||||||
mapStore.updateCharacterPosition(data)
|
|
||||||
|
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(() => {
|
onUnmounted(() => {
|
||||||
mapStore.reset()
|
if (tileMap.value) {
|
||||||
|
tileMap.value.destroyLayer('tiles')
|
||||||
|
tileMap.value.removeAllLayers()
|
||||||
|
tileMap.value.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
gameStore.connection?.off('map:character:teleport')
|
gameStore.connection?.off('map:character:teleport')
|
||||||
gameStore.connection?.off('map:character:join')
|
|
||||||
gameStore.connection?.off('map:character:leave')
|
|
||||||
gameStore.connection?.off('map:character:move')
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,74 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<Controls v-if="tileLayer" :layer="tileLayer" :depth="0" />
|
<Controls v-if="tileMapLayer" :layer="tileMapLayer" :depth="0" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/application/config'
|
|
||||||
import type { Map as MapT, UUID } from '@/application/types'
|
|
||||||
import { unduplicateArray } from '@/application/utilities'
|
|
||||||
import Controls from '@/components/utilities/Controls.vue'
|
import Controls from '@/components/utilities/Controls.vue'
|
||||||
import { loadMapTilesIntoScene, setLayerTiles } from '@/composables/mapComposable'
|
import { loadTileTexturesFromMapTileArray, placeTiles } from '@/services/mapService'
|
||||||
import { MapStorage } from '@/storage/storages'
|
import { MapStorage } from '@/storage/storages'
|
||||||
import { useMapStore } from '@/stores/mapStore'
|
import { useMapStore } from '@/stores/mapStore'
|
||||||
import { useScene } from 'phavuer'
|
import { useScene } from 'phavuer'
|
||||||
import { onBeforeUnmount, shallowRef } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
|
|
||||||
import Tileset = Phaser.Tilemaps.Tileset
|
|
||||||
|
|
||||||
const emit = defineEmits(['tileMap:create'])
|
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
const mapStore = useMapStore()
|
const mapStore = useMapStore()
|
||||||
const mapStorage = new MapStorage()
|
const mapStorage = new MapStorage()
|
||||||
|
|
||||||
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
|
const props = defineProps<{
|
||||||
const tileLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
|
tileMap: Phaser.Tilemaps.Tilemap
|
||||||
|
tileMapLayer: Phaser.Tilemaps.TilemapLayer
|
||||||
|
}>()
|
||||||
|
|
||||||
function createTileMap(map: MapT) {
|
onMounted(async () => {
|
||||||
const mapConfig = new Phaser.Tilemaps.MapData({
|
if (!mapStore.mapId) return
|
||||||
width: map.width,
|
|
||||||
height: map.height,
|
|
||||||
tileWidth: config.tile_size.width,
|
|
||||||
tileHeight: config.tile_size.height,
|
|
||||||
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
|
|
||||||
format: Phaser.Tilemaps.Formats.ARRAY_2D
|
|
||||||
})
|
|
||||||
|
|
||||||
const newTileMap = new Phaser.Tilemaps.Tilemap(scene, mapConfig)
|
const map = await mapStorage.getById(mapStore.mapId)
|
||||||
emit('tileMap:create', newTileMap)
|
if (!map) return
|
||||||
return newTileMap
|
|
||||||
}
|
|
||||||
|
|
||||||
function createTileLayer(currentTileMap: Phaser.Tilemaps.Tilemap, mapData: any) {
|
await loadTileTexturesFromMapTileArray(mapStore.mapId, scene)
|
||||||
const tilesArray = unduplicateArray(mapData?.tiles.flat())
|
|
||||||
|
|
||||||
const tilesetImages = tilesArray.map((tile: string, index: number) => {
|
placeTiles(props.tileMap, props.tileMapLayer, map.tiles)
|
||||||
return currentTileMap.addTilesetImage(tile, tile, config.tile_size.width, config.tile_size.height, 1, 2, index + 1, { x: 0, y: -config.tile_size.height })
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add blank tile
|
|
||||||
tilesetImages.push(currentTileMap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.width, config.tile_size.height, 1, 2, 0, { x: 0, y: -config.tile_size.height }))
|
|
||||||
|
|
||||||
const layer = currentTileMap.createBlankLayer('tiles', tilesetImages as Tileset[], 0, config.tile_size.height) as Phaser.Tilemaps.TilemapLayer
|
|
||||||
|
|
||||||
layer.setDepth(0)
|
|
||||||
layer.setCullPadding(2, 2)
|
|
||||||
return layer
|
|
||||||
}
|
|
||||||
|
|
||||||
loadMapTilesIntoScene(mapStore.mapId as UUID, scene)
|
|
||||||
.then(() => mapStorage.get(mapStore.mapId))
|
|
||||||
.then((mapData) => {
|
|
||||||
if (!mapData || !mapData?.tiles) return
|
|
||||||
tileMap.value = createTileMap(mapData)
|
|
||||||
tileLayer.value = createTileLayer(tileMap.value, mapData)
|
|
||||||
setLayerTiles(tileMap.value, tileLayer.value, mapData.tiles)
|
|
||||||
})
|
|
||||||
.catch((error) => console.error('Failed to initialize map:', error))
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
if (!tileMap.value) return
|
|
||||||
tileMap.value.destroyLayer('tiles')
|
|
||||||
tileMap.value.removeAllLayers()
|
|
||||||
tileMap.value.destroy()
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<PlacedMapObject v-for="placedMapObject in items" :tilemap="tilemap" :placedMapObject />
|
<PlacedMapObject v-for="placedMapObject in items" :tileMap :tileMapLayer :placedMapObject />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -9,8 +9,11 @@ import { MapStorage } from '@/storage/storages'
|
|||||||
import { useMapStore } from '@/stores/mapStore'
|
import { useMapStore } from '@/stores/mapStore'
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
tilemap: Phaser.Tilemaps.Tilemap
|
tileMap: Phaser.Tilemaps.Tilemap
|
||||||
|
tileMapLayer: TilemapLayer
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const mapStore = useMapStore()
|
const mapStore = useMapStore()
|
||||||
@ -20,7 +23,7 @@ const items = ref<PlacedMapObjectT[]>([])
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!mapStore.mapId) return
|
if (!mapStore.mapId) return
|
||||||
|
|
||||||
const map = await mapStorage.get(mapStore.mapId)
|
const map = await mapStorage.getById(mapStore.mapId)
|
||||||
if (!map) return
|
if (!map) return
|
||||||
|
|
||||||
items.value = map.placedMapObjects
|
items.value = map.placedMapObjects
|
||||||
|
@ -1,43 +1,82 @@
|
|||||||
<template>
|
<template>
|
||||||
<Image v-if="gameStore.isTextureLoaded(props.placedMapObject.mapObject.id)" v-bind="imageProps" />
|
<Image v-if="mapObject && gameStore.isTextureLoaded(props.placedMapObject.mapObject as string)" v-bind="imageProps" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PlacedMapObject, TextureData } from '@/application/types'
|
import config from '@/application/config'
|
||||||
import { loadTexture } from '@/composables/gameComposable'
|
import type { MapObject, PlacedMapObject } from '@/application/types'
|
||||||
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/mapComposable'
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
|
import { calculateIsometricDepth, loadMapObjectTextures, tileToWorldXY } from '@/services/mapService'
|
||||||
|
import { MapObjectStorage } from '@/storage/storages'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { Image, useScene } from 'phavuer'
|
import { Image, useScene } from 'phavuer'
|
||||||
import { computed, onMounted } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import Tilemap = Phaser.Tilemaps.Tilemap
|
||||||
|
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
tilemap: Phaser.Tilemaps.Tilemap
|
|
||||||
placedMapObject: PlacedMapObject
|
placedMapObject: PlacedMapObject
|
||||||
|
tileMap: Tilemap
|
||||||
|
tileMapLayer: TilemapLayer
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const mapEditor = useMapEditorComposable()
|
||||||
|
|
||||||
|
const mapObject = ref<MapObject>()
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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 - mapObject.value!.frameWidth / 2,
|
||||||
|
y: position.worldPositionY - mapObject.value!.frameHeight / 2 + config.tile_size.height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const imageProps = computed(() => ({
|
const imageProps = computed(() => ({
|
||||||
depth: calculateIsometricDepth(props.placedMapObject.positionX, props.placedMapObject.positionY, props.placedMapObject.mapObject.frameWidth, props.placedMapObject.mapObject.frameHeight),
|
alpha: mapEditor.movingPlacedObject.value?.id == props.placedMapObject.id ? 0.5 : 1,
|
||||||
x: tileToWorldX(props.tilemap, props.placedMapObject.positionX, props.placedMapObject.positionY),
|
tint: mapEditor.selectedPlacedObject.value?.id == props.placedMapObject.id ? 0x00ff00 : 0xffffff,
|
||||||
y: tileToWorldY(props.tilemap, props.placedMapObject.positionX, props.placedMapObject.positionY),
|
depth: calculateIsometricDepth(props.placedMapObject.positionX, props.placedMapObject.positionY, mapObject.value!.frameWidth, mapObject.value!.frameHeight),
|
||||||
|
...calculateObjectPlacement(props.placedMapObject),
|
||||||
flipX: props.placedMapObject.isRotated,
|
flipX: props.placedMapObject.isRotated,
|
||||||
texture: props.placedMapObject.mapObject.id,
|
texture: mapObject.value!.id,
|
||||||
originY: Number(props.placedMapObject.mapObject.originX),
|
originX: mapObject.value!.originX,
|
||||||
originX: Number(props.placedMapObject.mapObject.originY)
|
originY: mapObject.value!.originY
|
||||||
}))
|
}))
|
||||||
|
|
||||||
loadTexture(scene, {
|
watch(
|
||||||
key: props.placedMapObject.mapObject.id,
|
() => mapEditor.refreshMapObject.value,
|
||||||
data: '/textures/map_objects/' + props.placedMapObject.mapObject.id + '.png',
|
async () => {
|
||||||
group: 'map_objects',
|
await initialize()
|
||||||
updatedAt: props.placedMapObject.mapObject.updatedAt,
|
}
|
||||||
frameWidth: props.placedMapObject.mapObject.frameWidth,
|
)
|
||||||
frameHeight: props.placedMapObject.mapObject.frameHeight
|
|
||||||
} as TextureData).catch((error) => {
|
|
||||||
console.error('Error loading texture:', error)
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(async () => {})
|
onMounted(async () => {
|
||||||
|
await initialize()
|
||||||
|
})
|
||||||
</script>
|
</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="$emit('open-map-editor')">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,13 +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 { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
|
|
||||||
defineEmits(['open-map-editor'])
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const mapEditor = useMapEditorComposable()
|
const mapEditor = useMapEditorComposable()
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
|
||||||
let toggle = ref('asset-manager')
|
let toggle = ref('asset-manager')
|
||||||
</script>
|
</script>
|
||||||
|
@ -49,7 +49,7 @@ const handleFileUpload = (e: Event) => {
|
|||||||
if (!files) return
|
if (!files) return
|
||||||
gameStore.connection?.emit('gm:mapObject:upload', files, (response: boolean) => {
|
gameStore.connection?.emit('gm:mapObject:upload', files, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
if (config.development) console.error('Failed to upload object')
|
if (config.environment === 'development') console.error('Failed to upload map object')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<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>
|
||||||
@ -48,23 +48,20 @@
|
|||||||
<input v-model.number="action.frameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
|
<input v-model.number="action.frameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<SpriteActionsInput
|
<SpriteActionsInput v-model="action.sprites" @tempOffsetChange="(index, offset) => handleTempOffsetChange(action, index, offset)" />
|
||||||
v-model="action.sprites"
|
|
||||||
@tempOffsetChange="(index, offset) => handleTempOffsetChange(action, index, offset)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
<SpritePreview
|
<SpritePreview
|
||||||
v-if="selectedAction"
|
v-if="selectedAction"
|
||||||
:sprites="selectedAction.sprites"
|
:sprites="selectedAction.sprites"
|
||||||
:frame-rate="selectedAction.frameRate"
|
:frame-rate="selectedAction.frameRate"
|
||||||
:is-modal-open="isModalOpen"
|
:is-modal-open="isModalOpen"
|
||||||
:temp-offset-index="tempOffsetData.index"
|
:temp-offset-index="tempOffsetData.index"
|
||||||
:temp-offset="tempOffsetData.offset"
|
:temp-offset="tempOffsetData.offset"
|
||||||
@update:frame-rate="updateFrameRate"
|
@update:frame-rate="updateFrameRate"
|
||||||
@update:is-modal-open="isModalOpen = $event"
|
@update:is-modal-open="isModalOpen = $event"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -199,9 +196,9 @@ function updateFrameRate(value: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tempOffsetData = ref<{ index: number | undefined; offset: { x: number; y: number } | undefined }>({
|
const tempOffsetData = ref<{ index: number | undefined; offset: { x: number; y: number } | undefined }>({
|
||||||
index: undefined,
|
index: undefined,
|
||||||
offset: undefined
|
offset: undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
function handleTempOffsetChange(action: SpriteAction, index: number, offset: { x: number; y: number }) {
|
function handleTempOffsetChange(action: SpriteAction, index: number, offset: { x: number; y: number }) {
|
||||||
|
@ -42,7 +42,7 @@ const elementToScroll = ref()
|
|||||||
function newButtonClickHandler() {
|
function newButtonClickHandler() {
|
||||||
gameStore.connection?.emit('gm:sprite:create', {}, (response: boolean) => {
|
gameStore.connection?.emit('gm:sprite:create', {}, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
if (config.development) console.error('Failed to create new sprite')
|
if (config.environment === 'development') console.error('Failed to create new sprite')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,9 +2,7 @@
|
|||||||
<div class="flex flex-wrap gap-3">
|
<div class="flex flex-wrap gap-3">
|
||||||
<div v-for="(image, index) in modelValue" :key="index" class="h-20 w-20 p-4 bg-gray-300 bg-opacity-50 rounded text-center relative group cursor-move" draggable="true" @dragstart="dragStart($event, index)" @dragover.prevent @dragenter.prevent @drop="drop($event, index)">
|
<div v-for="(image, index) in modelValue" :key="index" class="h-20 w-20 p-4 bg-gray-300 bg-opacity-50 rounded text-center relative group cursor-move" draggable="true" @dragstart="dragStart($event, index)" @dragover.prevent @dragenter.prevent @drop="drop($event, index)">
|
||||||
<img :src="image.url" class="max-w-full max-h-full object-contain pointer-events-none" alt="Uploaded image" @load="updateImageDimensions($event, index)" />
|
<img :src="image.url" class="max-w-full max-h-full object-contain pointer-events-none" alt="Uploaded image" @load="updateImageDimensions($event, index)" />
|
||||||
<div v-if="image.dimensions" class="absolute bottom-1 right-1 bg-black/50 text-white text-xs px-1 py-0.5 rounded transition-opacity font-default">
|
<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>
|
||||||
{{ image.dimensions.width }}x{{ image.dimensions.height }}
|
|
||||||
</div>
|
|
||||||
<div class="absolute top-1 left-1 flex-row space-y-1">
|
<div class="absolute top-1 left-1 flex-row space-y-1">
|
||||||
<button @click.stop="deleteImage(index)" class="bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" aria-label="Delete image">
|
<button @click.stop="deleteImage(index)" class="bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" aria-label="Delete image">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
@ -19,7 +17,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal :is-modal-open="selectedImageIndex === index" :modal-width="300" :modal-height="210" :is-resizable="false" :bg-style="'none'" @modal:close="closeOffsetModal">
|
<Modal :is-modal-open="selectedImageIndex === index" :modal-width="300" :modal-height="210" :is-resizable="false" bg-style="none" @modal:close="closeOffsetModal">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="m-0 font-medium shrink-0 text-white">Action offset ({{ selectedImageIndex }})</h3>
|
<h3 class="m-0 font-medium shrink-0 text-white">Action offset ({{ selectedImageIndex }})</h3>
|
||||||
</template>
|
</template>
|
||||||
@ -61,10 +59,6 @@ interface SpriteImage {
|
|||||||
x: number
|
x: number
|
||||||
y: number
|
y: number
|
||||||
}
|
}
|
||||||
dimensions?: {
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -78,7 +72,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: SpriteImage[]): void
|
(e: 'update:modelValue', value: SpriteImage[]): void
|
||||||
(e: 'close'): void
|
(e: 'close'): void
|
||||||
(e: 'tempOffsetChange', index: number, offset: { x: number, y: number }): void
|
(e: 'tempOffsetChange', index: number, offset: { x: number; y: number }): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const fileInput = ref<HTMLInputElement | null>(null)
|
const fileInput = ref<HTMLInputElement | null>(null)
|
||||||
@ -179,16 +173,13 @@ const onOffsetChange = () => {
|
|||||||
|
|
||||||
watch(tempOffset, onOffsetChange, { deep: true })
|
watch(tempOffset, onOffsetChange, { deep: true })
|
||||||
|
|
||||||
|
const imageDimensions = ref<{ [key: number]: { width: number; height: number } }>({})
|
||||||
|
|
||||||
const updateImageDimensions = (event: Event, index: number) => {
|
const updateImageDimensions = (event: Event, index: number) => {
|
||||||
const img = event.target as HTMLImageElement
|
const img = event.target as HTMLImageElement
|
||||||
const newImages = [...props.modelValue]
|
imageDimensions.value[index] = {
|
||||||
newImages[index] = {
|
width: img.naturalWidth,
|
||||||
...newImages[index],
|
height: img.naturalHeight
|
||||||
dimensions: {
|
|
||||||
width: img.naturalWidth,
|
|
||||||
height: img.naturalHeight
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
updateImages(newImages)
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal :is-modal-open="isModalOpen" :modal-width="700" :modal-height="330" :bg-style="'none'" @modal:close="closeModal">
|
<Modal :is-modal-open="isModalOpen" :modal-width="700" :modal-height="330" bg-style="none" @modal:close="closeModal">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="m-0 font-medium shrink-0 text-white">View sprite</h3>
|
<h3 class="m-0 font-medium shrink-0 text-white">View sprite</h3>
|
||||||
</template>
|
</template>
|
||||||
@ -39,15 +39,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<label class="block mb-2 text-white">Frame: {{ currentFrame + 1 }} of {{ sprites.length }}</label>
|
<label class="block mb-2 text-white">Frame: {{ currentFrame + 1 }} of {{ sprites.length }}</label>
|
||||||
<input
|
<input type="range" v-model.number="currentFrame" :min="0" :max="sprites.length - 1" step="1" class="w-full accent-cyan-500" @input="stopAnimation" />
|
||||||
type="range"
|
|
||||||
v-model.number="currentFrame"
|
|
||||||
:min="0"
|
|
||||||
:max="sprites.length - 1"
|
|
||||||
step="1"
|
|
||||||
class="w-full accent-cyan-500"
|
|
||||||
@input="stopAnimation"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<label class="block mb-2 text-white">Zoom: {{ zoomLevel }}%</label>
|
<label class="block mb-2 text-white">Zoom: {{ zoomLevel }}%</label>
|
||||||
@ -69,7 +61,7 @@ const props = defineProps<{
|
|||||||
frameRate: number
|
frameRate: number
|
||||||
isModalOpen?: boolean
|
isModalOpen?: boolean
|
||||||
tempOffsetIndex?: number
|
tempOffsetIndex?: number
|
||||||
tempOffset?: { x: number, y: number }
|
tempOffset?: { x: number; y: number }
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
@ -49,7 +49,7 @@ const handleFileUpload = (e: Event) => {
|
|||||||
if (!files) return
|
if (!files) return
|
||||||
gameStore.connection?.emit('gm:tile:upload', files, (response: boolean) => {
|
gameStore.connection?.emit('gm:tile:upload', files, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
if (config.development) console.error('Failed to upload tile')
|
if (config.environment === 'development') console.error('Failed to upload tile')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<MapTiles ref="mapTiles" @tileMap:create="tileMap = $event" />
|
<MapTiles ref="mapTiles" v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer />
|
||||||
<PlacedMapObjects ref="mapObjects" v-if="tileMap" :tileMap="tileMap as Phaser.Tilemaps.Tilemap" />
|
<PlacedMapObjects ref="mapObjects" v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer />
|
||||||
<MapEventTiles ref="eventTiles" v-if="tileMap" :tileMap="tileMap as Phaser.Tilemaps.Tilemap" />
|
<MapEventTiles ref="eventTiles" v-if="tileMap" :tileMap />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -9,10 +9,14 @@ import MapEventTiles from '@/components/gameMaster/mapEditor/mapPartials/MapEven
|
|||||||
import MapTiles from '@/components/gameMaster/mapEditor/mapPartials/MapTiles.vue'
|
import MapTiles from '@/components/gameMaster/mapEditor/mapPartials/MapTiles.vue'
|
||||||
import PlacedMapObjects from '@/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.vue'
|
import PlacedMapObjects from '@/components/gameMaster/mapEditor/mapPartials/PlacedMapObjects.vue'
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
|
import { createTileLayer, createTileMap } from '@/services/mapService'
|
||||||
|
import { TileStorage } from '@/storage/storages'
|
||||||
import { useScene } from 'phavuer'
|
import { useScene } from 'phavuer'
|
||||||
import { onMounted, onUnmounted, shallowRef, useTemplateRef } from 'vue'
|
import { onBeforeUnmount, onMounted, onUnmounted, shallowRef, useTemplateRef } from 'vue'
|
||||||
|
|
||||||
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
|
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
|
||||||
|
const tileMapLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
|
||||||
|
|
||||||
const mapEditor = useMapEditorComposable()
|
const mapEditor = useMapEditorComposable()
|
||||||
|
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
@ -21,7 +25,7 @@ const mapTiles = useTemplateRef('mapTiles')
|
|||||||
const mapObjects = useTemplateRef('mapObjects')
|
const mapObjects = useTemplateRef('mapObjects')
|
||||||
const eventTiles = useTemplateRef('eventTiles')
|
const eventTiles = useTemplateRef('eventTiles')
|
||||||
|
|
||||||
function handlePointer(pointer: Phaser.Input.Pointer) {
|
function handlePointerDown(pointer: Phaser.Input.Pointer) {
|
||||||
if (!mapTiles.value || !mapObjects.value || !eventTiles.value) return
|
if (!mapTiles.value || !mapObjects.value || !eventTiles.value) return
|
||||||
|
|
||||||
// Check if left mouse button is pressed
|
// Check if left mouse button is pressed
|
||||||
@ -34,21 +38,74 @@ function handlePointer(pointer: Phaser.Input.Pointer) {
|
|||||||
switch (mapEditor.drawMode.value) {
|
switch (mapEditor.drawMode.value) {
|
||||||
case 'tile':
|
case 'tile':
|
||||||
mapTiles.value.handlePointer(pointer)
|
mapTiles.value.handlePointer(pointer)
|
||||||
case 'object':
|
break
|
||||||
|
case 'map_object':
|
||||||
mapObjects.value.handlePointer(pointer)
|
mapObjects.value.handlePointer(pointer)
|
||||||
|
break
|
||||||
case 'teleport':
|
case 'teleport':
|
||||||
eventTiles.value.handlePointer(pointer)
|
eventTiles.value.handlePointer(pointer)
|
||||||
|
break
|
||||||
|
case 'blocking tile':
|
||||||
|
eventTiles.value.handlePointer(pointer)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
function handleKeyDown(event: KeyboardEvent) {
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointer)
|
//CTRL+Y
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_DOWN, handlePointer)
|
if (event.key === 'y' && event.ctrlKey) {
|
||||||
|
mapTiles.value!.redo()
|
||||||
|
}
|
||||||
|
|
||||||
|
//CTRL+Z
|
||||||
|
if (event.key === 'z' && event.ctrlKey) {
|
||||||
|
mapTiles.value!.undo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerMove(pointer: Phaser.Input.Pointer) {
|
||||||
|
if (mapEditor.inputMode.value === 'hold' && pointer.isDown) {
|
||||||
|
handlePointerDown(pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerUp(pointer: Phaser.Input.Pointer) {
|
||||||
|
if (mapEditor.drawMode.value === 'tile') {
|
||||||
|
mapTiles.value?.finalizeCommand()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
let mapValue = mapEditor.currentMap.value
|
||||||
|
if (!mapValue) return
|
||||||
|
|
||||||
|
const tileStorage = new TileStorage()
|
||||||
|
const allTiles = await tileStorage.getAll()
|
||||||
|
const allTileIds = allTiles.map((tile) => tile.id)
|
||||||
|
|
||||||
|
tileMap.value = createTileMap(scene, mapValue)
|
||||||
|
tileMapLayer.value = createTileLayer(tileMap.value, allTileIds)
|
||||||
|
|
||||||
|
addEventListener('keydown', handleKeyDown)
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointer)
|
if (tileMap.value) {
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_DOWN, handlePointer)
|
tileMap.value.destroyLayer('tiles')
|
||||||
|
tileMap.value.removeAllLayers()
|
||||||
|
tileMap.value.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
||||||
mapEditor.reset()
|
mapEditor.reset()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
removeEventListener('keydown', handleKeyDown)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -3,11 +3,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { MapEventTileType, type MapEventTile, type Map as MapT } from '@/application/types'
|
import { MapEventTileType, type MapEventTile, type Map as MapT, type UUID } from '@/application/types'
|
||||||
import { uuidv4 } from '@/application/utilities'
|
import { uuidv4 } from '@/application/utilities'
|
||||||
import { getTile, tileToWorldX, tileToWorldY } from '@/composables/mapComposable'
|
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
import { Image, useScene } from 'phavuer'
|
import { getTile, tileToWorldX, tileToWorldY } from '@/services/mapService'
|
||||||
|
import { Image } from 'phavuer'
|
||||||
import { shallowRef } from 'vue'
|
import { shallowRef } from 'vue'
|
||||||
|
|
||||||
const mapEditor = useMapEditorComposable()
|
const mapEditor = useMapEditorComposable()
|
||||||
@ -30,10 +30,8 @@ function getImageProps(tile: MapEventTile) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function pencil(pointer: Phaser.Input.Pointer, map: MapT) {
|
function pencil(pointer: Phaser.Input.Pointer, map: MapT) {
|
||||||
if (!tileLayer.value) return
|
|
||||||
|
|
||||||
// Check if there is a tile
|
// Check if there is a tile
|
||||||
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
|
const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
|
||||||
if (!tile) return
|
if (!tile) return
|
||||||
|
|
||||||
// Check if event tile already exists on position
|
// Check if event tile already exists on position
|
||||||
@ -44,9 +42,9 @@ function pencil(pointer: Phaser.Input.Pointer, map: MapT) {
|
|||||||
if (mapEditor.drawMode.value === 'teleport' && !mapEditor.teleportSettings.value.toMapId) return
|
if (mapEditor.drawMode.value === 'teleport' && !mapEditor.teleportSettings.value.toMapId) return
|
||||||
|
|
||||||
const newEventTile = {
|
const newEventTile = {
|
||||||
id: uuidv4(),
|
id: uuidv4() as UUID,
|
||||||
mapId: map?.id,
|
mapId: map.id,
|
||||||
map: map?.id,
|
map: map.id,
|
||||||
type: mapEditor.drawMode.value === 'blocking tile' ? MapEventTileType.BLOCK : MapEventTileType.TELEPORT,
|
type: mapEditor.drawMode.value === 'blocking tile' ? MapEventTileType.BLOCK : MapEventTileType.TELEPORT,
|
||||||
positionX: tile.x,
|
positionX: tile.x,
|
||||||
positionY: tile.y,
|
positionY: tile.y,
|
||||||
@ -61,19 +59,24 @@ function pencil(pointer: Phaser.Input.Pointer, map: MapT) {
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
map!.mapEventTiles = map!.mapEventTiles.concat(newEventTile as MapEventTile)
|
map.mapEventTiles.push(newEventTile)
|
||||||
}
|
}
|
||||||
|
|
||||||
function erase(pointer: Phaser.Input.Pointer, map: MapT) {
|
function erase(pointer: Phaser.Input.Pointer, map: MapT) {
|
||||||
if (!tileLayer.value) return
|
|
||||||
// Check if there is a tile
|
// Check if there is a tile
|
||||||
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
|
const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
|
||||||
if (!tile) return
|
if (!tile) return
|
||||||
|
|
||||||
// Check if event tile already exists on position
|
// Check if event tile already exists on position
|
||||||
const existingEventTile = map.mapEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y)
|
const existingEventTile = map.mapEventTiles.find((eventTile) => eventTile.positionX === tile.x && eventTile.positionY === tile.y)
|
||||||
if (!existingEventTile) return
|
if (!existingEventTile) return
|
||||||
|
|
||||||
|
if (mapEditor.drawMode.value !== existingEventTile.type.toLowerCase()) {
|
||||||
|
if (mapEditor.drawMode.value === 'blocking tile' && existingEventTile.type === MapEventTileType.BLOCK)
|
||||||
|
null //skip this case
|
||||||
|
else return
|
||||||
|
}
|
||||||
|
|
||||||
// Remove existing event tile
|
// Remove existing event tile
|
||||||
map.mapEventTiles = map.mapEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id)
|
map.mapEventTiles = map.mapEventTiles.filter((eventTile) => eventTile.id !== existingEventTile.id)
|
||||||
}
|
}
|
||||||
@ -82,11 +85,7 @@ function handlePointer(pointer: Phaser.Input.Pointer) {
|
|||||||
const map = mapEditor.currentMap.value
|
const map = mapEditor.currentMap.value
|
||||||
if (!map) return
|
if (!map) return
|
||||||
|
|
||||||
// Check if left mouse button is pressed
|
if (pointer.event.altKey) return
|
||||||
if (!pointer.isDown) return
|
|
||||||
|
|
||||||
// Check if shift is not pressed, this means we are moving the camera
|
|
||||||
if (pointer.event.shiftKey) return
|
|
||||||
|
|
||||||
switch (mapEditor.tool.value) {
|
switch (mapEditor.tool.value) {
|
||||||
case 'pencil':
|
case 'pencil':
|
||||||
|
@ -1,62 +1,40 @@
|
|||||||
<template>
|
<template>
|
||||||
<Controls v-if="tileLayer" :layer="tileLayer" :depth="0" />
|
<Controls v-if="tileMapLayer" :layer="tileMapLayer" :depth="0" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/application/config'
|
|
||||||
import Controls from '@/components/utilities/Controls.vue'
|
import Controls from '@/components/utilities/Controls.vue'
|
||||||
import { createTileArray, getTile, placeTile, setLayerTiles } from '@/composables/mapComposable'
|
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
import { TileStorage } from '@/storage/storages'
|
import { createTileArray, getTile, placeTile, placeTiles } from '@/services/mapService'
|
||||||
import { useScene } from 'phavuer'
|
import { onMounted, ref, watch } from 'vue'
|
||||||
import { onMounted, onUnmounted, shallowRef, watch } from 'vue'
|
|
||||||
|
|
||||||
import Tileset = Phaser.Tilemaps.Tileset
|
|
||||||
|
|
||||||
const emit = defineEmits(['tileMap:create'])
|
|
||||||
|
|
||||||
const scene = useScene()
|
|
||||||
const mapEditor = useMapEditorComposable()
|
const mapEditor = useMapEditorComposable()
|
||||||
const tileStorage = new TileStorage()
|
|
||||||
|
|
||||||
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
|
defineExpose({ handlePointer, finalizeCommand, undo, redo })
|
||||||
const tileLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
|
|
||||||
|
|
||||||
defineExpose({ handlePointer })
|
const props = defineProps<{
|
||||||
|
tileMap: Phaser.Tilemaps.Tilemap
|
||||||
|
tileMapLayer: Phaser.Tilemaps.TilemapLayer
|
||||||
|
}>()
|
||||||
|
|
||||||
function createTileMap() {
|
class EditorCommand {
|
||||||
const mapData = new Phaser.Tilemaps.MapData({
|
public operation: 'draw' | 'erase' = 'draw'
|
||||||
width: mapEditor.currentMap.value?.width,
|
public tileName: string = 'blank_tile'
|
||||||
height: mapEditor.currentMap.value?.height,
|
public affectedTiles: number[][]
|
||||||
tileWidth: config.tile_size.width,
|
|
||||||
tileHeight: config.tile_size.height,
|
|
||||||
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
|
|
||||||
format: Phaser.Tilemaps.Formats.ARRAY_2D
|
|
||||||
})
|
|
||||||
|
|
||||||
const newTileMap = new Phaser.Tilemaps.Tilemap(scene, mapData)
|
constructor(operation: 'draw' | 'erase', tileName: string) {
|
||||||
emit('tileMap:create', newTileMap)
|
this.operation = operation
|
||||||
return newTileMap
|
this.tileName = tileName
|
||||||
}
|
this.affectedTiles = []
|
||||||
|
|
||||||
async function createTileLayer(currentTileMap: Phaser.Tilemaps.Tilemap) {
|
|
||||||
const tiles = await tileStorage.getAll()
|
|
||||||
const tilesetImages = []
|
|
||||||
|
|
||||||
for (const tile of tiles) {
|
|
||||||
tilesetImages.push(currentTileMap.addTilesetImage(tile.id, tile.id, config.tile_size.width, config.tile_size.height, 1, 2, tilesetImages.length + 1, { x: 0, y: -config.tile_size.height }))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add blank tile
|
|
||||||
tilesetImages.push(currentTileMap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.width, config.tile_size.height, 1, 2, 0, { x: 0, y: -config.tile_size.height }))
|
|
||||||
|
|
||||||
const layer = currentTileMap.createBlankLayer('tiles', tilesetImages as Tileset[], 0, config.tile_size.height) as Phaser.Tilemaps.TilemapLayer
|
|
||||||
|
|
||||||
layer.setDepth(0)
|
|
||||||
layer.setCullPadding(2, 2)
|
|
||||||
return layer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Record of commands
|
||||||
|
let commandStack: EditorCommand[] = []
|
||||||
|
let currentCommand: EditorCommand | null = null
|
||||||
|
let commandIndex = ref(0)
|
||||||
|
let originTiles: string[][] = []
|
||||||
|
|
||||||
function pencil(pointer: Phaser.Input.Pointer) {
|
function pencil(pointer: Phaser.Input.Pointer) {
|
||||||
let map = mapEditor.currentMap.value
|
let map = mapEditor.currentMap.value
|
||||||
if (!map) return
|
if (!map) return
|
||||||
@ -64,14 +42,14 @@ function pencil(pointer: Phaser.Input.Pointer) {
|
|||||||
// Check if there is a selected tile
|
// Check if there is a selected tile
|
||||||
if (!mapEditor.selectedTile.value) return
|
if (!mapEditor.selectedTile.value) return
|
||||||
|
|
||||||
if (!tileMap.value || !tileLayer.value) return
|
|
||||||
|
|
||||||
// Check if there is a tile
|
// Check if there is a tile
|
||||||
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
|
const tile = getTile(props.tileMapLayer, pointer.worldX, pointer.worldY)
|
||||||
if (!tile) return
|
if (!tile) return
|
||||||
|
|
||||||
// Place tile
|
// Place tile
|
||||||
placeTile(tileMap.value, tileLayer.value, tile.x, tile.y, mapEditor.selectedTile.value)
|
placeTile(props.tileMap, props.tileMapLayer, tile.x, tile.y, mapEditor.selectedTile.value)
|
||||||
|
|
||||||
|
createCommandUpdate(tile.x, tile.y, mapEditor.selectedTile.value, 'draw')
|
||||||
|
|
||||||
// Adjust mapEditorStore.map.tiles
|
// Adjust mapEditorStore.map.tiles
|
||||||
map.tiles[tile.y][tile.x] = mapEditor.selectedTile.value
|
map.tiles[tile.y][tile.x] = mapEditor.selectedTile.value
|
||||||
@ -81,29 +59,29 @@ function eraser(pointer: Phaser.Input.Pointer) {
|
|||||||
let map = mapEditor.currentMap.value
|
let map = mapEditor.currentMap.value
|
||||||
if (!map) return
|
if (!map) return
|
||||||
|
|
||||||
if (!tileMap.value || !tileLayer.value) return
|
|
||||||
|
|
||||||
// Check if there is a tile
|
// Check if there is a tile
|
||||||
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
|
const tile = getTile(props.tileMapLayer, pointer.worldX, pointer.worldY)
|
||||||
if (!tile) return
|
if (!tile) return
|
||||||
|
|
||||||
// Place tile
|
// Place tile
|
||||||
placeTile(tileMap.value, tileLayer.value, tile.x, tile.y, 'blank_tile')
|
placeTile(props.tileMap, props.tileMapLayer, tile.x, tile.y, 'blank_tile')
|
||||||
|
|
||||||
|
createCommandUpdate(tile.x, tile.y, 'blank_tile', 'erase')
|
||||||
|
|
||||||
// Adjust mapEditorStore.map.tiles
|
// Adjust mapEditorStore.map.tiles
|
||||||
map.tiles[tile.y][tile.x] = 'blank_tile'
|
map.tiles[tile.y][tile.x] = 'blank_tile'
|
||||||
}
|
}
|
||||||
|
|
||||||
function paint(pointer: Phaser.Input.Pointer) {
|
function paint(pointer: Phaser.Input.Pointer) {
|
||||||
if (!tileMap.value || !tileLayer.value) return
|
let map = mapEditor.currentMap.value
|
||||||
|
if (!map) return
|
||||||
|
|
||||||
// Set new tileArray with selected tile
|
// Set new tileArray with selected tile
|
||||||
const tileArray = createTileArray(tileMap.value.width, tileMap.value.height, mapEditor.selectedTile.value)
|
const tileArray = createTileArray(props.tileMap.width, props.tileMap.height, mapEditor.selectedTile.value)
|
||||||
setLayerTiles(tileMap.value, tileLayer.value, tileArray)
|
placeTiles(props.tileMap, props.tileMapLayer, tileArray)
|
||||||
|
|
||||||
// Adjust mapEditorStore.map.tiles
|
// Adjust mapEditorStore.map.tiles
|
||||||
if (mapEditor.currentMap.value) {
|
map.tiles = tileArray
|
||||||
mapEditor.currentMap.value.tiles = tileArray
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// When alt is pressed, and the pointer is down, select the tile that the pointer is over
|
// When alt is pressed, and the pointer is down, select the tile that the pointer is over
|
||||||
@ -111,33 +89,17 @@ function tilePicker(pointer: Phaser.Input.Pointer) {
|
|||||||
let map = mapEditor.currentMap.value
|
let map = mapEditor.currentMap.value
|
||||||
if (!map) return
|
if (!map) return
|
||||||
|
|
||||||
if (!tileMap.value || !tileLayer.value) return
|
|
||||||
|
|
||||||
// Check if there is a tile
|
// Check if there is a tile
|
||||||
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
|
const tile = getTile(props.tileMapLayer, pointer.worldX, pointer.worldY)
|
||||||
if (!tile) return
|
if (!tile) return
|
||||||
|
|
||||||
// Select the tile
|
// Select the tile
|
||||||
mapEditor.setSelectedTile(map.tiles[tile.y][tile.x])
|
mapEditor.setSelectedTile(map.tiles[tile.y][tile.x])
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
|
||||||
() => mapEditor.shouldClearTiles,
|
|
||||||
(shouldClear) => {
|
|
||||||
if (shouldClear && mapEditor.currentMap.value && tileMap.value && tileLayer.value) {
|
|
||||||
const blankTiles = createTileArray(tileLayer.value.width, tileLayer.value.height, 'blank_tile')
|
|
||||||
setLayerTiles(tileMap.value, tileLayer.value, blankTiles)
|
|
||||||
mapEditor.currentMap.value.tiles = blankTiles
|
|
||||||
mapEditor.resetClearTilesFlag()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function handlePointer(pointer: Phaser.Input.Pointer) {
|
function handlePointer(pointer: Phaser.Input.Pointer) {
|
||||||
if (!tileMap.value || !tileLayer.value) return
|
|
||||||
|
|
||||||
// Check if left mouse button is pressed
|
// Check if left mouse button is pressed
|
||||||
if (!pointer.isDown) return
|
if (!pointer.isDown && pointer.button === 0) return
|
||||||
|
|
||||||
// Check if shift is not pressed, this means we are moving the camera
|
// Check if shift is not pressed, this means we are moving the camera
|
||||||
if (pointer.event.shiftKey) return
|
if (pointer.event.shiftKey) return
|
||||||
@ -162,33 +124,90 @@ function handlePointer(pointer: Phaser.Input.Pointer) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
function createCommandUpdate(x: number, y: number, tileName: string, operation: 'draw' | 'erase') {
|
||||||
if (!mapEditor.currentMap.value) return
|
if (!currentCommand) {
|
||||||
|
currentCommand = new EditorCommand(operation, tileName)
|
||||||
|
}
|
||||||
|
|
||||||
tileMap.value = createTileMap()
|
//If position is already in, do not proceed
|
||||||
tileLayer.value = await createTileLayer(tileMap.value)
|
for (const vec of currentCommand.affectedTiles) {
|
||||||
|
if (vec[0] === x && vec[1] === y) return
|
||||||
|
}
|
||||||
|
|
||||||
// First fill the entire map with blank tiles using current map dimensions
|
currentCommand.affectedTiles.push([x, y])
|
||||||
const blankTiles = createTileArray(mapEditor.currentMap.value.width, mapEditor.currentMap.value.height, 'blank_tile')
|
}
|
||||||
|
|
||||||
// Then overlay the map tiles, but only within the current map dimensions
|
function finalizeCommand() {
|
||||||
const mapTiles = mapEditor.currentMap.value.tiles
|
if (!currentCommand) return
|
||||||
for (let y = 0; y < mapEditor.currentMap.value.height; y++) {
|
//Cut the stack so the current edit is the last
|
||||||
for (let x = 0; x < mapEditor.currentMap.value.width; x++) {
|
commandStack = commandStack.slice(0, commandIndex.value)
|
||||||
if (mapTiles[y] && mapTiles[y][x] !== undefined) {
|
commandStack.push(currentCommand)
|
||||||
blankTiles[y][x] = mapTiles[y][x]
|
if (commandStack.length >= 9) {
|
||||||
}
|
originTiles = applyCommands(originTiles, commandStack.shift()!)
|
||||||
|
}
|
||||||
|
|
||||||
|
commandIndex.value = commandStack.length
|
||||||
|
currentCommand = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function undo() {
|
||||||
|
if (commandIndex.value > 0) {
|
||||||
|
commandIndex.value--
|
||||||
|
updateMapTiles()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function redo() {
|
||||||
|
if (commandIndex.value <= 9 && commandIndex.value <= commandStack.length) {
|
||||||
|
commandIndex.value++
|
||||||
|
updateMapTiles()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyCommands(tiles: string[][], ...commands: EditorCommand[]): string[][] {
|
||||||
|
let tileVersion = cloneArray(tiles)
|
||||||
|
for (let command of commands) {
|
||||||
|
for (const position of command.affectedTiles) {
|
||||||
|
tileVersion[position[1]][position[0]] = command.tileName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return tileVersion
|
||||||
|
}
|
||||||
|
|
||||||
setLayerTiles(tileMap.value, tileLayer.value, blankTiles)
|
function updateMapTiles() {
|
||||||
})
|
if (!mapEditor.currentMap.value) return
|
||||||
|
|
||||||
onUnmounted(() => {
|
let indexedCommands = commandStack.slice(0, commandIndex.value)
|
||||||
if (tileMap.value) {
|
let modifiedTiles = applyCommands(originTiles, ...indexedCommands)
|
||||||
tileMap.value.destroyLayer('tiles')
|
|
||||||
tileMap.value.removeAllLayers()
|
placeTiles(props.tileMap, props.tileMapLayer, modifiedTiles)
|
||||||
tileMap.value.destroy()
|
mapEditor.currentMap.value.tiles = modifiedTiles
|
||||||
|
}
|
||||||
|
|
||||||
|
//Recursive Array Clone
|
||||||
|
function cloneArray(arr: any[]): any[] {
|
||||||
|
return arr.map((item) => (item instanceof Array ? cloneArray(item) : item))
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => mapEditor.shouldClearTiles.value,
|
||||||
|
(shouldClear) => {
|
||||||
|
if (shouldClear && mapEditor.currentMap.value) {
|
||||||
|
const blankTiles = createTileArray(props.tileMapLayer.width, props.tileMapLayer.height, 'blank_tile')
|
||||||
|
placeTiles(props.tileMap, props.tileMapLayer, blankTiles)
|
||||||
|
mapEditor.currentMap.value.tiles = blankTiles
|
||||||
|
mapEditor.resetClearTilesFlag()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!mapEditor.currentMap.value) return
|
||||||
|
const mapState = mapEditor.currentMap.value
|
||||||
|
|
||||||
|
//Clone
|
||||||
|
originTiles = cloneArray(mapState.tiles)
|
||||||
|
|
||||||
|
placeTiles(props.tileMap, props.tileMapLayer, mapState.tiles)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Image v-if="gameStore.isTextureLoaded(props.placedMapObject.mapObject.id)" v-bind="imageProps" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { PlacedMapObject, TextureData } from '@/application/types'
|
|
||||||
import { loadTexture } from '@/composables/gameComposable'
|
|
||||||
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/mapComposable'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import { Image, useScene } from 'phavuer'
|
|
||||||
import { computed } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
tileMap: Phaser.Tilemaps.Tilemap
|
|
||||||
placedMapObject: PlacedMapObject
|
|
||||||
selectedPlacedMapObject: PlacedMapObject | null
|
|
||||||
movingPlacedMapObject: PlacedMapObject | null
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const scene = useScene()
|
|
||||||
|
|
||||||
const imageProps = computed(() => ({
|
|
||||||
alpha: props.movingPlacedMapObject?.id === props.placedMapObject.id ? 0.5 : 1,
|
|
||||||
tint: props.selectedPlacedMapObject?.id === props.placedMapObject.id ? 0x00ff00 : 0xffffff,
|
|
||||||
depth: calculateIsometricDepth(props.placedMapObject.positionX, props.placedMapObject.positionY, props.placedMapObject.mapObject.frameWidth, props.placedMapObject.mapObject.frameHeight),
|
|
||||||
x: tileToWorldX(props.tileMap, props.placedMapObject.positionX, props.placedMapObject.positionY),
|
|
||||||
y: tileToWorldY(props.tileMap, props.placedMapObject.positionX, props.placedMapObject.positionY),
|
|
||||||
flipX: props.placedMapObject.isRotated,
|
|
||||||
texture: props.placedMapObject.mapObject.id,
|
|
||||||
originY: Number(props.placedMapObject.mapObject.originX),
|
|
||||||
originX: Number(props.placedMapObject.mapObject.originY)
|
|
||||||
}))
|
|
||||||
|
|
||||||
loadTexture(scene, {
|
|
||||||
key: props.placedMapObject.mapObject.id,
|
|
||||||
data: '/textures/map_objects/' + props.placedMapObject.mapObject.id + '.png',
|
|
||||||
group: 'map_objects',
|
|
||||||
updatedAt: props.placedMapObject.mapObject.updatedAt,
|
|
||||||
frameWidth: props.placedMapObject.mapObject.frameWidth,
|
|
||||||
frameHeight: props.placedMapObject.mapObject.frameHeight
|
|
||||||
} as TextureData).catch((error) => {
|
|
||||||
console.error('Error loading texture:', error)
|
|
||||||
})
|
|
||||||
</script>
|
|
@ -1,112 +1,119 @@
|
|||||||
<template>
|
<template>
|
||||||
<SelectedPlacedMapObjectComponent v-if="selectedPlacedMapObject" :placedMapObject="selectedPlacedMapObject" @move="moveMapObject" @rotate="rotatePlacedMapObject" @delete="deletePlacedMapObject" />
|
<SelectedPlacedMapObjectComponent v-if="mapEditor.selectedPlacedObject.value" :key="mapEditor.selectedPlacedObject.value.id" :map :placedMapObject="mapEditor.selectedPlacedObject.value" @move="moveMapObject" @rotate="rotatePlacedMapObject" @delete="deletePlacedMapObject" />
|
||||||
<PlacedMapObject v-for="placedMapObject in mapEditor.currentMap.value?.placedMapObjects" :tileMap="tileMap" :placedMapObject :selectedPlacedMapObject :movingPlacedMapObject @pointerup="clickPlacedMapObject(placedMapObject)" />
|
<PlacedMapObject v-for="placedMapObject in mapEditor.currentMap.value?.placedMapObjects" :tileMap :tileMapLayer :placedMapObject @pointerdown="clickPlacedMapObject(placedMapObject)" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Map as MapT, PlacedMapObject as PlacedMapObjectT } from '@/application/types'
|
import type { MapObject, Map as MapT, PlacedMapObject as PlacedMapObjectT } from '@/application/types'
|
||||||
import { uuidv4 } from '@/application/utilities'
|
import { uuidv4 } from '@/application/utilities'
|
||||||
import PlacedMapObject from '@/components/gameMaster/mapEditor/mapPartials/PlacedMapObject.vue'
|
import PlacedMapObject from '@/components/game/map/partials/PlacedMapObject.vue'
|
||||||
import SelectedPlacedMapObjectComponent from '@/components/gameMaster/mapEditor/partials/SelectedPlacedMapObject.vue'
|
import SelectedPlacedMapObjectComponent from '@/components/gameMaster/mapEditor/partials/SelectedPlacedMapObject.vue'
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
|
import { getTile } from '@/services/mapService'
|
||||||
import { useScene } from 'phavuer'
|
import { useScene } from 'phavuer'
|
||||||
import { ref, watch } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import Tilemap = Phaser.Tilemaps.Tilemap
|
||||||
|
import TilemapLayer = Phaser.Tilemaps.TilemapLayer
|
||||||
|
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
const mapEditor = useMapEditorComposable()
|
const mapEditor = useMapEditorComposable()
|
||||||
const selectedPlacedMapObject = ref<PlacedMapObjectT | null>(null)
|
const map = computed(() => mapEditor.currentMap.value!)
|
||||||
const movingPlacedMapObject = ref<PlacedMapObjectT | null>(null)
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
tileMap: Phaser.Tilemaps.Tilemap
|
|
||||||
}>()
|
|
||||||
|
|
||||||
defineExpose({ handlePointer })
|
defineExpose({ handlePointer })
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
tileMap: Tilemap
|
||||||
|
tileMapLayer: TilemapLayer
|
||||||
|
}>()
|
||||||
|
|
||||||
function pencil(pointer: Phaser.Input.Pointer, map: MapT) {
|
function pencil(pointer: Phaser.Input.Pointer, map: MapT) {
|
||||||
|
const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
|
||||||
|
if (!tile) return
|
||||||
|
|
||||||
// Check if object already exists on position
|
// Check if object already exists on position
|
||||||
const existingPlacedMapObject = findInMap(pointer, map)
|
const existingPlacedMapObject = findObjectByPointer(pointer, map)
|
||||||
if (existingPlacedMapObject) return
|
if (existingPlacedMapObject) return
|
||||||
|
|
||||||
|
if (!mapEditor.selectedMapObject.value) return
|
||||||
|
|
||||||
const newPlacedMapObject: PlacedMapObjectT = {
|
const newPlacedMapObject: PlacedMapObjectT = {
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
depth: 0,
|
mapObject: mapEditor.selectedMapObject.value,
|
||||||
map: map,
|
|
||||||
mapObject: mapEditor.selectedMapObject.value!,
|
|
||||||
isRotated: false,
|
isRotated: false,
|
||||||
positionX: pointer.x,
|
positionX: tile.x,
|
||||||
positionY: pointer.y
|
positionY: tile.y
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add new object to mapObjects
|
// Add new object to mapObjects
|
||||||
map.placedMapObjects.concat(newPlacedMapObject)
|
map.placedMapObjects.push(newPlacedMapObject)
|
||||||
|
mapEditor.selectedPlacedObject.value = newPlacedMapObject
|
||||||
}
|
}
|
||||||
|
|
||||||
function eraser(pointer: Phaser.Input.Pointer, map: MapT) {
|
function eraser(pointer: Phaser.Input.Pointer, map: MapT) {
|
||||||
// Check if object already exists on position
|
// Check if object already exists on position
|
||||||
const existingPlacedMapObject = findInMap(pointer, map)
|
const existingPlacedMapObject = findObjectByPointer(pointer, map)
|
||||||
if (!existingPlacedMapObject) return
|
if (!existingPlacedMapObject) return
|
||||||
|
|
||||||
// Remove existing object
|
// Remove existing object
|
||||||
map.placedMapObjects = map.placedMapObjects.filter((placedMapObject) => placedMapObject.id !== existingPlacedMapObject.id)
|
map.placedMapObjects = map.placedMapObjects.filter((placedMapObject) => placedMapObject.id !== existingPlacedMapObject.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
function findInMap(pointer: Phaser.Input.Pointer, map: MapT) {
|
function findObjectByPointer(pointer: Phaser.Input.Pointer, map: MapT): PlacedMapObjectT | undefined {
|
||||||
return map.placedMapObjects.find((placedMapObject) => placedMapObject.positionX === pointer.worldX && placedMapObject.positionY === pointer.worldY)
|
const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
|
||||||
|
if (!tile) return undefined
|
||||||
|
|
||||||
|
return map.placedMapObjects.find((placedMapObject) => placedMapObject.positionX === tile.x && placedMapObject.positionY === tile.y)
|
||||||
}
|
}
|
||||||
|
|
||||||
function objectPicker(pointer: Phaser.Input.Pointer, map: MapT) {
|
function objectPicker(pointer: Phaser.Input.Pointer, map: MapT) {
|
||||||
// Check if object already exists on position
|
// Check if object already exists on position
|
||||||
const existingPlacedMapObject = findInMap(pointer, map)
|
const existingPlacedMapObject = findObjectByPointer(pointer, map)
|
||||||
if (!existingPlacedMapObject) return
|
if (!existingPlacedMapObject) return
|
||||||
|
|
||||||
// Select the object
|
// Select the object
|
||||||
mapEditor.setSelectedMapObject(existingPlacedMapObject.mapObject)
|
mapEditor.setSelectedMapObject(existingPlacedMapObject.mapObject as MapObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveMapObject(id: string, map: MapT) {
|
function moveMapObject(id: string, map: MapT) {
|
||||||
movingPlacedMapObject.value = map.placedMapObjects.find((object) => object.id === id) as PlacedMapObjectT
|
mapEditor.movingPlacedObject.value = map.placedMapObjects.find((object) => object.id === id) as PlacedMapObjectT
|
||||||
|
|
||||||
function handlePointerMove(pointer: Phaser.Input.Pointer) {
|
function handlePointerMove(pointer: Phaser.Input.Pointer) {
|
||||||
if (!movingPlacedMapObject.value) return
|
if (!mapEditor.movingPlacedObject.value) return
|
||||||
|
const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
|
||||||
|
if (!tile) return
|
||||||
|
|
||||||
movingPlacedMapObject.value.positionX = pointer.worldX
|
mapEditor.movingPlacedObject.value.positionX = tile.x
|
||||||
movingPlacedMapObject.value.positionY = pointer.worldY
|
mapEditor.movingPlacedObject.value.positionY = tile.y
|
||||||
}
|
}
|
||||||
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
|
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
|
||||||
|
|
||||||
function handlePointerUp() {
|
function handlePointerUp() {
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
|
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
|
||||||
movingPlacedMapObject.value = null
|
mapEditor.movingPlacedObject.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
||||||
}
|
}
|
||||||
|
|
||||||
function rotatePlacedMapObject(id: string, map: MapT) {
|
function rotatePlacedMapObject(id: string, map: MapT) {
|
||||||
map.placedMapObjects = map.placedMapObjects.map((placedMapObject) => {
|
const matchingObject = map.placedMapObjects.find((placedMapObject) => placedMapObject.id === id)
|
||||||
if (placedMapObject.id === id) {
|
matchingObject!.isRotated = !matchingObject!.isRotated
|
||||||
return {
|
|
||||||
...placedMapObject,
|
|
||||||
isRotated: !placedMapObject.isRotated
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return placedMapObject
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function deletePlacedMapObject(id: string, map: MapT) {
|
function deletePlacedMapObject(id: string, map: MapT) {
|
||||||
map.placedMapObjects = map.placedMapObjects.filter((object) => object.id !== id)
|
let mapE = mapEditor.currentMap.value!
|
||||||
selectedPlacedMapObject.value = null
|
mapE.placedMapObjects = map.placedMapObjects.filter((object) => object.id !== id)
|
||||||
|
mapEditor.selectedPlacedObject.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function clickPlacedMapObject(placedMapObject: PlacedMapObjectT) {
|
function clickPlacedMapObject(placedMapObject: PlacedMapObjectT) {
|
||||||
selectedPlacedMapObject.value = placedMapObject
|
mapEditor.selectedPlacedObject.value = placedMapObject
|
||||||
|
|
||||||
// If alt is pressed, select the object
|
// If alt is pressed, select the object
|
||||||
if (scene.input.activePointer.event.altKey) {
|
if (scene.input.activePointer.event.altKey) {
|
||||||
mapEditor.setSelectedMapObject(placedMapObject.mapObject)
|
mapEditor.setSelectedMapObject(placedMapObject.mapObject as MapObject)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,21 +121,13 @@ function handlePointer(pointer: Phaser.Input.Pointer) {
|
|||||||
const map = mapEditor.currentMap.value
|
const map = mapEditor.currentMap.value
|
||||||
if (!map) return
|
if (!map) return
|
||||||
|
|
||||||
if (mapEditor.drawMode.value !== 'map_object') return
|
|
||||||
|
|
||||||
// Check if left mouse button is pressed
|
|
||||||
if (!pointer.isDown) return
|
|
||||||
|
|
||||||
// Check if shift is not pressed, this means we are moving the camera
|
|
||||||
if (pointer.event.shiftKey) return
|
|
||||||
|
|
||||||
// Check if alt is pressed, this means we are selecting the object
|
// Check if alt is pressed, this means we are selecting the object
|
||||||
if (pointer.event.altKey) return
|
if (pointer.event.altKey) return
|
||||||
|
|
||||||
// Check if tool is pencil
|
// Check if tool is pencil
|
||||||
switch (mapEditor.tool.value) {
|
switch (mapEditor.tool.value) {
|
||||||
case 'pencil':
|
case 'pencil':
|
||||||
if (mapEditor.selectedMapObject.value) pencil(pointer, map)
|
pencil(pointer, map)
|
||||||
break
|
break
|
||||||
case 'eraser':
|
case 'eraser':
|
||||||
eraser(pointer, map)
|
eraser(pointer, map)
|
||||||
@ -138,44 +137,4 @@ function handlePointer(pointer: Phaser.Input.Pointer) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// watch mapEditorStore.mapObjectList and update originX and originY of objects in mapObjects
|
|
||||||
watch(
|
|
||||||
() => mapEditor.currentMap.value,
|
|
||||||
() => {
|
|
||||||
const map = mapEditor.currentMap.value
|
|
||||||
if (!map) return
|
|
||||||
|
|
||||||
const updatedMapObjects = map.placedMapObjects.map((mapObject) => {
|
|
||||||
const updatedMapObject = map.placedMapObjects.find((obj) => obj.id === mapObject.mapObject.id)
|
|
||||||
if (updatedMapObject) {
|
|
||||||
return {
|
|
||||||
...mapObject,
|
|
||||||
mapObject: {
|
|
||||||
...mapObject.mapObject,
|
|
||||||
originX: updatedMapObject.positionX,
|
|
||||||
originY: updatedMapObject.positionY
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mapObject
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update the map with the new mapObjects
|
|
||||||
map.placedMapObjects = [...map.placedMapObjects, ...updatedMapObjects]
|
|
||||||
|
|
||||||
// Update mapObject if it's set
|
|
||||||
if (mapEditor.selectedMapObject.value) {
|
|
||||||
const updatedMapObject = map.placedMapObjects.find((obj) => obj.id === mapEditor.selectedMapObject.value?.id)
|
|
||||||
if (updatedMapObject) {
|
|
||||||
mapEditor.setSelectedMapObject({
|
|
||||||
...mapEditor.selectedMapObject.value,
|
|
||||||
originX: updatedMapObject.positionX,
|
|
||||||
originY: updatedMapObject.positionY
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// { deep: true }
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal ref="modalRef" :modal-width="300" :modal-height="420" :is-resizable="false" :bg-style="'none'">
|
<Modal ref="modalRef" :modal-width="300" :modal-height="420" :is-resizable="false" bg-style="none">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="m-0 font-medium shrink-0 text-white">Create new map</h3>
|
<h3 class="m-0 font-medium shrink-0 text-white">Create new map</h3>
|
||||||
</template>
|
</template>
|
||||||
@ -58,10 +58,6 @@ defineExpose({ open: () => modalRef.value?.open() })
|
|||||||
async function submit() {
|
async function submit() {
|
||||||
gameStore.connection?.emit('gm:map:create', { name: name.value, width: width.value, height: height.value }, async (response: Map | false) => {
|
gameStore.connection?.emit('gm:map:create', { name: name.value, width: width.value, height: height.value }, async (response: Map | false) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
gameStore.addNotification({
|
|
||||||
title: 'Error',
|
|
||||||
message: 'Failed to create map.'
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,8 +68,6 @@ async function submit() {
|
|||||||
pvp.value = false
|
pvp.value = false
|
||||||
|
|
||||||
// Add map to storage
|
// Add map to storage
|
||||||
|
|
||||||
console.log(response)
|
|
||||||
await mapStorage.add(response)
|
await mapStorage.add(response)
|
||||||
|
|
||||||
// Let list know to fetch new maps
|
// Let list know to fetch new maps
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal ref="modalRef" :is-resizable="false" :modal-width="300" :modal-height="360" :bg-style="'none'">
|
<Modal ref="modalRef" :is-resizable="false" :modal-width="300" :modal-height="360" bg-style="none">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="text-lg text-white">Maps</h3>
|
<h3 class="text-lg text-white">Maps</h3>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,80 +1,65 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal ref="modalRef" :modal-width="645" :modal-height="260" :bg-style="'none'">
|
<div class="absolute border-0 border-l-2 border-solid border-gray-500 w-1/4 min-w-80 flex flex-col top-0 right-0 z-10 h-dvh bg-gray-800">
|
||||||
<template #modalHeader>
|
<div class="flex flex-col gap-2.5 p-2.5">
|
||||||
<h3 class="text-lg text-white">Map objects</h3>
|
<div class="relative flex">
|
||||||
</template>
|
<img src="/assets/icons/mapEditor/search.svg" class="w-4 h-4 py-0.5 absolute top-1/2 -translate-y-1/2 left-2.5" alt="Search icon" />
|
||||||
<template #modalBody>
|
<label class="mb-1.5 font-titles hidden" for="search">Search</label>
|
||||||
<div class="flex pt-4 pl-4">
|
<input @mousedown.stop class="!pl-7 input-field w-full" type="text" name="search" placeholder="Search" v-model="searchQuery" />
|
||||||
<div class="w-full flex gap-1.5 flex-row">
|
</div>
|
||||||
<div>
|
<div class="flex">
|
||||||
<label class="mb-1.5 font-titles hidden" for="search">Search...</label>
|
<select class="input-field w-full" name="lists">
|
||||||
<input @mousedown.stop class="input-field" type="text" name="search" placeholder="Search" v-model="searchQuery" />
|
<option value="tile">Tiles</option>
|
||||||
|
<option value="map_object">Objects</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="h-full overflow-auto relative border-0 border-t border-solid border-gray-500 p-2.5">
|
||||||
|
<div class="h-full overflow-auto">
|
||||||
|
<div class="flex justify-between flex-wrap gap-2.5 items-center">
|
||||||
|
<div v-for="(mapObject, index) in filteredMapObjects" :key="index" class="max-w-1/4 inline-block">
|
||||||
|
<img
|
||||||
|
class="border-2 border-solid rounded max-w-full"
|
||||||
|
:src="`${config.server_endpoint}/textures/map_objects/${mapObject.id}.png`"
|
||||||
|
alt="Object"
|
||||||
|
@click="mapEditor.setSelectedMapObject(mapObject)"
|
||||||
|
:class="{
|
||||||
|
'cursor-pointer transition-all duration-300': true,
|
||||||
|
'border-cyan shadow-lg': mapEditor.selectedMapObject.value?.id === mapObject.id,
|
||||||
|
'border-transparent hover:border-gray-300': mapEditor.selectedMapObject.value?.id !== mapObject.id
|
||||||
|
}"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col h-full p-4">
|
</div>
|
||||||
<div class="mb-4 flex flex-wrap gap-2">
|
<div class="flex flex-col h-40 gap-2.5 p-3.5 border-t border-0 border-solid border-gray-500">
|
||||||
<button v-for="tag in uniqueTags" :key="tag" @click="toggleTag(tag)" class="btn-cyan" :class="{ 'opacity-50': !selectedTags.includes(tag) }">
|
<span>Tags:</span>
|
||||||
{{ tag }}
|
<div class="flex grow items-center flex-wrap gap-1.5 overflow-auto">
|
||||||
</button>
|
<span class="m-auto">No tags selected</span>
|
||||||
</div>
|
|
||||||
<div class="h-full overflow-auto">
|
|
||||||
<div class="flex justify-between flex-wrap gap-2.5 items-center">
|
|
||||||
<div v-for="(mapObject, index) in filteredMapObjects" :key="index" class="max-w-1/4 inline-block">
|
|
||||||
<img
|
|
||||||
class="border-2 border-solid max-w-full"
|
|
||||||
:src="`${config.server_endpoint}/textures/map_objects/${mapObject.id}.png`"
|
|
||||||
alt="Object"
|
|
||||||
@click="mapEditor.setSelectedMapObject(mapObject)"
|
|
||||||
:class="{
|
|
||||||
'cursor-pointer transition-all duration-300': true,
|
|
||||||
'border-cyan shadow-lg scale-105': mapEditor.selectedMapObject.value?.id === mapObject.id,
|
|
||||||
'border-transparent hover:border-gray-300': mapEditor.selectedMapObject.value?.id !== mapObject.id
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</Modal>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
import type { MapObject } from '@/application/types'
|
import type { MapObject } from '@/application/types'
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
import { MapObjectStorage } from '@/storage/storages'
|
import { MapObjectStorage } from '@/storage/storages'
|
||||||
import { liveQuery } from 'dexie'
|
import { liveQuery } from 'dexie'
|
||||||
import { computed, onMounted, onUnmounted, ref, useTemplateRef } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
|
||||||
const mapObjectStorage = new MapObjectStorage()
|
const mapObjectStorage = new MapObjectStorage()
|
||||||
const isModalOpen = ref(false)
|
|
||||||
const mapEditor = useMapEditorComposable()
|
const mapEditor = useMapEditorComposable()
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const selectedTags = ref<string[]>([])
|
const selectedTags = ref<string[]>([])
|
||||||
const mapObjectList = ref<MapObject[]>([])
|
const mapObjectList = ref<MapObject[]>([])
|
||||||
const modalRef = useTemplateRef('modalRef')
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
open: () => modalRef.value?.open(),
|
|
||||||
close: () => modalRef.value?.close()
|
|
||||||
})
|
|
||||||
|
|
||||||
const uniqueTags = computed(() => {
|
const uniqueTags = computed(() => {
|
||||||
const allTags = mapObjectList.value.flatMap((obj) => obj.tags || [])
|
const allTags = mapObjectList.value.flatMap((obj) => obj.tags || [])
|
||||||
return Array.from(new Set(allTags))
|
return Array.from(new Set(allTags))
|
||||||
})
|
})
|
||||||
|
|
||||||
const filteredMapObjects = computed(() => {
|
|
||||||
return mapObjectList.value.filter((object) => {
|
|
||||||
const matchesSearch = !searchQuery.value || object.name.toLowerCase().includes(searchQuery.value.toLowerCase())
|
|
||||||
const matchesTags = selectedTags.value.length === 0 || (object.tags && selectedTags.value.some((tag) => object.tags.includes(tag)))
|
|
||||||
return matchesSearch && matchesTags
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const toggleTag = (tag: string) => {
|
const toggleTag = (tag: string) => {
|
||||||
if (selectedTags.value.includes(tag)) {
|
if (selectedTags.value.includes(tag)) {
|
||||||
selectedTags.value = selectedTags.value.filter((t) => t !== tag)
|
selectedTags.value = selectedTags.value.filter((t) => t !== tag)
|
||||||
@ -83,10 +68,17 @@ const toggleTag = (tag: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filteredMapObjects = computed(() => {
|
||||||
|
return mapObjectList.value.filter((object) => {
|
||||||
|
const matchesSearch = !searchQuery.value || object.name.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||||
|
const matchesTags = selectedTags.value.length === 0 || (object.tags && selectedTags.value.some((tag) => object.tags.includes(tag)))
|
||||||
|
return matchesSearch && matchesTags
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
let subscription: any = null
|
let subscription: any = null
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
isModalOpen.value = true
|
|
||||||
subscription = liveQuery(() => mapObjectStorage.liveQuery()).subscribe({
|
subscription = liveQuery(() => mapObjectStorage.liveQuery()).subscribe({
|
||||||
next: (result) => {
|
next: (result) => {
|
||||||
mapObjectList.value = result
|
mapObjectList.value = result
|
||||||
@ -98,8 +90,7 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (subscription) {
|
if (!subscription) return
|
||||||
subscription.unsubscribe()
|
subscription.unsubscribe()
|
||||||
}
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal ref="modalRef" :modal-width="600" :modal-height="430" :bg-style="'none'">
|
<Modal ref="modalRef" :modal-width="600" :modal-height="430" bg-style="none">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="m-0 font-medium shrink-0 text-white">Map settings</h3>
|
<h3 class="m-0 font-medium shrink-0 text-white">Map settings</h3>
|
||||||
</template>
|
</template>
|
||||||
@ -14,22 +14,19 @@
|
|||||||
<div class="gap-2.5 flex flex-wrap mt-4">
|
<div class="gap-2.5 flex flex-wrap mt-4">
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
<label for="name">Name</label>
|
<label for="name">Name</label>
|
||||||
<input class="input-field" v-model="name" name="name" id="name" />
|
<input class="input-field" v-model="name" @input="updateValue" name="name" id="name" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-half">
|
<div class="form-field-half">
|
||||||
<label for="width">Width</label>
|
<label for="width">Width</label>
|
||||||
<input class="input-field" v-model="width" name="width" id="width" type="number" />
|
<input class="input-field" v-model="width" @input="updateValue" name="width" id="width" type="number" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-half">
|
<div class="form-field-half">
|
||||||
<label for="height">Height</label>
|
<label for="height">Height</label>
|
||||||
<input class="input-field" v-model="height" name="height" id="height" type="number" />
|
<input class="input-field" v-model="height" @input="updateValue" name="height" id="height" type="number" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-full">
|
<div>
|
||||||
<label for="pvp">PVP enabled</label>
|
<label class="mr-4" for="pvp">PVP enabled</label>
|
||||||
<select v-model="pvp" class="input-field" name="pvp" id="pvp">
|
<input type="checkbox" v-model="pvp" @input="updateValue" class="input-field scale-125" name="pvp" id="pvp" />
|
||||||
<option :value="false">No</option>
|
|
||||||
<option :value="true">Yes</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@ -51,15 +48,15 @@ import type { UUID } from '@/application/types'
|
|||||||
import { uuidv4 } from '@/application/utilities'
|
import { uuidv4 } from '@/application/utilities'
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
import { ref, useTemplateRef, watch } from 'vue'
|
import { onMounted, ref, useTemplateRef, watch } from 'vue'
|
||||||
|
|
||||||
const mapEditor = useMapEditorComposable()
|
const mapEditor = useMapEditorComposable()
|
||||||
const screen = ref('settings')
|
const screen = ref('settings')
|
||||||
|
|
||||||
const name = ref(mapEditor.currentMap.value?.name)
|
const name = ref<string | undefined>('Map')
|
||||||
const width = ref(mapEditor.currentMap.value?.width)
|
const width = ref<number>(0)
|
||||||
const height = ref(mapEditor.currentMap.value?.height)
|
const height = ref<number>(0)
|
||||||
const pvp = ref(mapEditor.currentMap.value?.pvp)
|
const pvp = ref<boolean>(false)
|
||||||
const mapEffects = ref(mapEditor.currentMap.value?.mapEffects || [])
|
const mapEffects = ref(mapEditor.currentMap.value?.mapEffects || [])
|
||||||
const modalRef = useTemplateRef('modalRef')
|
const modalRef = useTemplateRef('modalRef')
|
||||||
|
|
||||||
@ -67,36 +64,35 @@ defineExpose({
|
|||||||
open: () => modalRef.value?.open()
|
open: () => modalRef.value?.open()
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(name, (value) => {
|
function updateValue(event: Event) {
|
||||||
mapEditor.updateProperty('name', value!)
|
let ev = event.target as HTMLInputElement
|
||||||
})
|
const value = ev.name === 'pvp' ? (ev.checked ? 1 : 0) : ev.value
|
||||||
|
mapEditor.updateProperty(ev.name as 'name' | 'width' | 'height' | 'pvp' | 'mapEffects', value)
|
||||||
|
}
|
||||||
|
|
||||||
watch(width, (value) => {
|
watch(
|
||||||
mapEditor.updateProperty('width', value!)
|
() => mapEditor.currentMap.value,
|
||||||
})
|
(map) => {
|
||||||
|
if (!map) return
|
||||||
watch(height, (value) => {
|
name.value = map.name
|
||||||
mapEditor.updateProperty('height', value!)
|
width.value = map.width
|
||||||
})
|
height.value = map.height
|
||||||
|
pvp.value = map.pvp
|
||||||
watch(pvp, (value) => {
|
mapEffects.value = map.mapEffects
|
||||||
mapEditor.updateProperty('pvp', value!)
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
watch(mapEffects, (value) => {
|
|
||||||
mapEditor.updateProperty('mapEffects', value!)
|
|
||||||
})
|
|
||||||
|
|
||||||
const addEffect = () => {
|
const addEffect = () => {
|
||||||
mapEffects.value.push({
|
mapEffects.value.push({
|
||||||
id: uuidv4() as UUID, // Simple unique id generation
|
id: uuidv4(),
|
||||||
map: mapEditor.currentMap.value!,
|
|
||||||
effect: '',
|
effect: '',
|
||||||
strength: 1
|
strength: 1
|
||||||
})
|
})
|
||||||
|
mapEditor.updateProperty('mapEffects', mapEffects.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeEffect = (index: number) => {
|
const removeEffect = (index: number) => {
|
||||||
mapEffects.value.splice(index, 1)
|
mapEffects.value.splice(index, 1)
|
||||||
|
mapEditor.updateProperty('mapEffects', mapEffects.value)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,33 +1,113 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col items-center py-5 px-3 fixed bottom-14 right-0">
|
<div class="flex flex-col items-center px-5 py-1 fixed bottom-20 left-0 z-20">
|
||||||
<div class="self-end mt-2 flex gap-2">
|
<div class="flex h-10 gap-2">
|
||||||
<button @mousedown.stop @click="handleDelete" class="btn-red py-1.5 px-4">
|
<button @mousedown.stop @click="handleDelete" class="btn-red !py-3 px-4">
|
||||||
<img src="/assets/icons/trashcan.svg" class="w-4 h-4" alt="Delete" />
|
<img src="/assets/icons/trashcan.svg" class="w-4 h-4" alt="Delete" />
|
||||||
</button>
|
</button>
|
||||||
|
<button @mousedown.stop @click="showMapObjectSettings = !showMapObjectSettings" class="btn-indigo !py-3 px-4">
|
||||||
|
<img src="/assets/icons/mapEditor/gear.svg" class="w-4 h-4 invert" alt="Delete" />
|
||||||
|
</button>
|
||||||
<button @mousedown.stop @click="handleRotate" class="btn-cyan py-1.5 px-4">Rotate</button>
|
<button @mousedown.stop @click="handleRotate" class="btn-cyan py-1.5 px-4">Rotate</button>
|
||||||
<button @mousedown.stop @click="handleMove" class="btn-cyan py-1.5 px-4 min-w-24">Move</button>
|
<button @mousedown.stop @click="handleMove" class="btn-cyan py-1.5 px-4 min-w-24">Move</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Modal :is-modal-open="showMapObjectSettings" @modal:close="showMapObjectSettings = false" :modal-height="320" bg-style="none">
|
||||||
|
<template #modalHeader>
|
||||||
|
<h3 class="m-0 font-medium shrink-0 text-white">Map object settings</h3>
|
||||||
|
</template>
|
||||||
|
<template #modalBody>
|
||||||
|
<div class="m-4">
|
||||||
|
<form method="post" @submit.prevent="" class="inline">
|
||||||
|
<div class="gap-2.5 flex flex-wrap">
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input class="input-field" v-model="mapObjectName" name="name" id="name" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field-half">
|
||||||
|
<label for="originX">Origin X</label>
|
||||||
|
<input class="input-field" v-model="mapObjectOriginX" name="originX" id="originX" type="number" min="0.0" step="0.01" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field-half">
|
||||||
|
<label for="originY">Origin Y</label>
|
||||||
|
<input class="input-field" v-model="mapObjectOriginY" name="originY" id="originY" type="number" min="0.0" step="0.01" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn-cyan px-4 py-1.5 min-w-24" @click="handleUpdate">Save</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PlacedMapObject } from '@/application/types'
|
import type { MapObject, Map as MapT, PlacedMapObject } from '@/application/types'
|
||||||
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
|
import { MapObjectStorage } from '@/storage/storages'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
placedMapObject: PlacedMapObject
|
placedMapObject: PlacedMapObject
|
||||||
|
map: MapT
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits(['move', 'rotate', 'delete'])
|
const emit = defineEmits(['move', 'rotate', 'delete'])
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const mapEditor = useMapEditorComposable()
|
||||||
|
|
||||||
|
const mapObjectStorage = new MapObjectStorage()
|
||||||
|
const mapObject = ref<MapObject | null>(null)
|
||||||
|
const showMapObjectSettings = ref(false)
|
||||||
|
const mapObjectName = ref('')
|
||||||
|
const mapObjectOriginX = ref(0)
|
||||||
|
const mapObjectOriginY = ref(0)
|
||||||
|
|
||||||
const handleMove = () => {
|
const handleMove = () => {
|
||||||
emit('move', props.placedMapObject.id)
|
emit('move', props.placedMapObject.id, props.map)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRotate = () => {
|
const handleRotate = () => {
|
||||||
emit('rotate', props.placedMapObject.id)
|
emit('rotate', props.placedMapObject.id, props.map)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
emit('delete', props.placedMapObject.id)
|
emit('delete', props.placedMapObject.id, props.map)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleUpdate() {
|
||||||
|
if (!mapObject.value) return
|
||||||
|
|
||||||
|
gameStore.connection?.emit(
|
||||||
|
'gm:mapObject:update',
|
||||||
|
{
|
||||||
|
id: props.placedMapObject.mapObject as string,
|
||||||
|
name: mapObjectName.value,
|
||||||
|
originX: mapObjectOriginX.value,
|
||||||
|
originY: mapObjectOriginY.value
|
||||||
|
},
|
||||||
|
async (response: boolean) => {
|
||||||
|
if (!response) return
|
||||||
|
await mapObjectStorage.update(mapObject.value!.id, {
|
||||||
|
name: mapObjectName.value,
|
||||||
|
originX: mapObjectOriginX.value,
|
||||||
|
originY: mapObjectOriginY.value
|
||||||
|
})
|
||||||
|
mapEditor.triggerMapObjectRefresh()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!props.placedMapObject.mapObject) return
|
||||||
|
|
||||||
|
mapObject.value = await mapObjectStorage.getById(props.placedMapObject.mapObject as string)
|
||||||
|
if (!mapObject.value) return
|
||||||
|
|
||||||
|
mapObjectName.value = mapObject.value.name
|
||||||
|
mapObjectOriginX.value = mapObject.value.originX
|
||||||
|
mapObjectOriginY.value = mapObject.value.originY
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal ref="modalRef" @modal:close="() => mapEditorStore.setTool('move')" :modal-width="300" :modal-height="350" :is-resizable="false" :bg-style="'none'">
|
<Modal ref="modalRef" @modal:close="() => mapEditorStore.setTool('move')" :modal-width="300" :modal-height="350" :is-resizable="false" bg-style="none">
|
||||||
<template #modalHeader>
|
<template #modalHeader>
|
||||||
<h3 class="m-0 font-medium shrink-0 text-white">Teleport settings</h3>
|
<h3 class="m-0 font-medium shrink-0 text-white">Teleport settings</h3>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,44 +1,37 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal ref="modalRef" :modal-width="645" :modal-height="600" :bg-style="'none'">
|
<div class="absolute border-0 border-l-2 border-solid border-gray-500 w-1/4 min-w-80 flex flex-col top-0 right-0 z-10 h-dvh bg-gray-800">
|
||||||
<template #modalHeader>
|
<div class="flex flex-col gap-2.5 p-2.5">
|
||||||
<h3 class="text-lg text-white">Tiles</h3>
|
<div class="relative flex">
|
||||||
</template>
|
<img src="/assets/icons/mapEditor/search.svg" class="w-4 h-4 py-0.5 absolute top-1/2 -translate-y-1/2 left-2.5" alt="Search icon" />
|
||||||
<template #modalBody>
|
<label class="mb-1.5 font-titles hidden" for="search">Search</label>
|
||||||
<div class="h-full overflow-auto" v-if="!selectedGroup">
|
<input @mousedown.stop class="!pl-7 input-field w-full" type="text" name="search" placeholder="Search" v-model="searchQuery" />
|
||||||
<div class="flex pt-4 pl-4">
|
</div>
|
||||||
<div class="w-full flex gap-1.5 flex-row">
|
<div class="flex">
|
||||||
<div>
|
<select class="input-field w-full" name="lists">
|
||||||
<label class="mb-1.5 font-titles hidden" for="search">Search...</label>
|
<option value="tile">Tiles</option>
|
||||||
<input @mousedown.stop class="input-field" type="text" name="search" placeholder="Search" v-model="searchQuery" />
|
<option value="map_object">Objects</option>
|
||||||
</div>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col h-full p-4">
|
<div class="h-full overflow-auto relative border-0 border-t border-solid border-gray-500 p-2.5">
|
||||||
<div class="mb-4 flex flex-wrap gap-2">
|
<div class="h-full" v-if="!selectedGroup">
|
||||||
<button v-for="tag in uniqueTags" :key="tag" @click="toggleTag(tag)" class="btn-cyan" :class="{ 'opacity-50': !selectedTags.includes(tag) }">
|
<div class="grid grid-cols-4 gap-2 justify-items-center">
|
||||||
{{ tag }}
|
<div v-for="group in groupedTiles" :key="group.parent.id" class="flex flex-col items-center justify-center relative">
|
||||||
</button>
|
<img
|
||||||
</div>
|
class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300"
|
||||||
<div class="h-[calc(100%_-_60px)] flex-grow overflow-y-auto">
|
:src="`${config.server_endpoint}/textures/tiles/${group.parent.id}.png`"
|
||||||
<div class="grid grid-cols-8 gap-2 justify-items-center">
|
:alt="group.parent.name"
|
||||||
<div v-for="group in groupedTiles" :key="group.parent.id" class="flex flex-col items-center justify-center relative">
|
@click="openGroup(group)"
|
||||||
<img
|
@load="() => tileProcessor.processTile(group.parent)"
|
||||||
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
|
:class="{
|
||||||
:src="`${config.server_endpoint}/textures/tiles/${group.parent.id}.png`"
|
'border-cyan shadow-lg': isActiveTile(group.parent),
|
||||||
:alt="group.parent.name"
|
'border-transparent hover:border-gray-300': !isActiveTile(group.parent)
|
||||||
@click="openGroup(group)"
|
}"
|
||||||
@load="() => processTile(group.parent)"
|
/>
|
||||||
:class="{
|
<span class="text-xs mt-1">{{ getTileCategory(group.parent) }}</span>
|
||||||
'border-cyan shadow-lg scale-105': isActiveTile(group.parent),
|
<span v-if="group.children.length > 0" class="absolute top-0 right-0 bg-cyan text-white rounded-full w-5 h-5 flex items-center justify-center text-xs">
|
||||||
'border-transparent hover:border-gray-300': !isActiveTile(group.parent)
|
{{ group.children.length + 1 }}
|
||||||
}"
|
</span>
|
||||||
/>
|
|
||||||
<span class="text-xs mt-1">{{ getTileCategory(group.parent) }}</span>
|
|
||||||
<span v-if="group.children.length > 0" class="absolute top-0 right-0 bg-cyan text-white rounded-full w-5 h-5 flex items-center justify-center text-xs">
|
|
||||||
{{ group.children.length + 1 }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -46,15 +39,15 @@
|
|||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<button @click="closeGroup" class="btn-cyan mb-4">Back to All Tiles</button>
|
<button @click="closeGroup" class="btn-cyan mb-4">Back to All Tiles</button>
|
||||||
<h4 class="text-lg mb-4">{{ selectedGroup.parent.name }} Group</h4>
|
<h4 class="text-lg mb-4">{{ selectedGroup.parent.name }} Group</h4>
|
||||||
<div class="grid grid-cols-8 gap-2 justify-items-center">
|
<div class="grid grid-cols-4 gap-2 justify-items-center">
|
||||||
<div class="flex flex-col items-center justify-center">
|
<div class="flex flex-col items-center justify-center">
|
||||||
<img
|
<img
|
||||||
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
|
class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300"
|
||||||
:src="`${config.server_endpoint}/textures/tiles/${selectedGroup.parent.id}.png`"
|
:src="`${config.server_endpoint}/textures/tiles/${selectedGroup.parent.id}.png`"
|
||||||
:alt="selectedGroup.parent.name"
|
:alt="selectedGroup.parent.name"
|
||||||
@click="selectTile(selectedGroup.parent.id)"
|
@click="selectTile(selectedGroup.parent.id)"
|
||||||
:class="{
|
:class="{
|
||||||
'border-cyan shadow-lg scale-105': isActiveTile(selectedGroup.parent),
|
'border-cyan shadow-lg': isActiveTile(selectedGroup.parent),
|
||||||
'border-transparent hover:border-gray-300': !isActiveTile(selectedGroup.parent)
|
'border-transparent hover:border-gray-300': !isActiveTile(selectedGroup.parent)
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
@ -62,12 +55,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-for="childTile in selectedGroup.children" :key="childTile.id" class="flex flex-col items-center justify-center">
|
<div v-for="childTile in selectedGroup.children" :key="childTile.id" class="flex flex-col items-center justify-center">
|
||||||
<img
|
<img
|
||||||
class="max-w-full max-h-full border-2 border-solid cursor-pointer transition-all duration-300"
|
class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300"
|
||||||
:src="`${config.server_endpoint}/textures/tiles/${childTile.id}.png`"
|
:src="`${config.server_endpoint}/textures/tiles/${childTile.id}.png`"
|
||||||
:alt="childTile.name"
|
:alt="childTile.name"
|
||||||
@click="selectTile(childTile.id)"
|
@click="selectTile(childTile.id)"
|
||||||
:class="{
|
:class="{
|
||||||
'border-cyan shadow-lg scale-105': isActiveTile(childTile),
|
'border-cyan shadow-lg': isActiveTile(childTile),
|
||||||
'border-transparent hover:border-gray-300': !isActiveTile(childTile)
|
'border-transparent hover:border-gray-300': !isActiveTile(childTile)
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
@ -76,32 +69,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</Modal>
|
<div class="flex flex-col h-40 gap-2.5 p-3.5 border-t border-0 border-solid border-gray-500">
|
||||||
|
<span>Tags:</span>
|
||||||
|
<div class="flex grow items-center flex-wrap gap-1.5 overflow-auto">
|
||||||
|
<span class="m-auto">No tags selected</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
import type { Tile } from '@/application/types'
|
import type { Tile } from '@/application/types'
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
|
import { useTileProcessingComposable } from '@/composables/useTileProcessingComposable'
|
||||||
import { TileStorage } from '@/storage/storages'
|
import { TileStorage } from '@/storage/storages'
|
||||||
import { liveQuery } from 'dexie'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { computed, onMounted, onUnmounted, ref, useTemplateRef } from 'vue'
|
|
||||||
|
|
||||||
const tileStorage = new TileStorage()
|
const tileStorage = new TileStorage()
|
||||||
const mapEditor = useMapEditorComposable()
|
const mapEditor = useMapEditorComposable()
|
||||||
|
const tileProcessor = useTileProcessingComposable()
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const selectedTags = ref<string[]>([])
|
const selectedTags = ref<string[]>([])
|
||||||
const tileCategories = ref<Map<string, string>>(new Map())
|
const tileCategories = ref<Map<string, string>>(new Map())
|
||||||
const selectedGroup = ref<{ parent: Tile; children: Tile[] } | null>(null)
|
const selectedGroup = ref<{ parent: Tile; children: Tile[] } | null>(null)
|
||||||
const tiles = ref<Tile[]>([])
|
const tiles = ref<Tile[]>([])
|
||||||
const modalRef = useTemplateRef('modalRef')
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
open: () => modalRef.value?.open(),
|
|
||||||
close: () => modalRef.value?.close()
|
|
||||||
})
|
|
||||||
|
|
||||||
const uniqueTags = computed(() => {
|
const uniqueTags = computed(() => {
|
||||||
const allTags = tiles.value.flatMap((tile) => tile.tags || [])
|
const allTags = tiles.value.flatMap((tile) => tile.tags || [])
|
||||||
@ -117,7 +110,7 @@ const groupedTiles = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
filteredTiles.forEach((tile) => {
|
filteredTiles.forEach((tile) => {
|
||||||
const parentGroup = groups.find((group) => areTilesRelated(group.parent, tile))
|
const parentGroup = groups.find((group) => tileProcessor.areTilesRelated(group.parent, tile))
|
||||||
if (parentGroup && parentGroup.parent.id !== tile.id) {
|
if (parentGroup && parentGroup.parent.id !== tile.id) {
|
||||||
parentGroup.children.push(tile)
|
parentGroup.children.push(tile)
|
||||||
} else {
|
} else {
|
||||||
@ -128,32 +121,6 @@ const groupedTiles = computed(() => {
|
|||||||
return groups
|
return groups
|
||||||
})
|
})
|
||||||
|
|
||||||
const tileColorData = ref<Map<string, { r: number; g: number; b: number }>>(new Map())
|
|
||||||
const tileEdgeData = ref<Map<string, number>>(new Map())
|
|
||||||
|
|
||||||
function areTilesRelated(tile1: Tile, tile2: Tile): boolean {
|
|
||||||
const colorSimilarityThreshold = 30 // Adjust this value as needed
|
|
||||||
const edgeComplexitySimilarityThreshold = 20 // Adjust this value as needed
|
|
||||||
|
|
||||||
const color1 = tileColorData.value.get(tile1.id)
|
|
||||||
const color2 = tileColorData.value.get(tile2.id)
|
|
||||||
const edge1 = tileEdgeData.value.get(tile1.id)
|
|
||||||
const edge2 = tileEdgeData.value.get(tile2.id)
|
|
||||||
|
|
||||||
if (!color1 || !color2 || edge1 === undefined || edge2 === undefined) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const colorDifference = Math.sqrt(Math.pow(color1.r - color2.r, 2) + Math.pow(color1.g - color2.g, 2) + Math.pow(color1.b - color2.b, 2))
|
|
||||||
|
|
||||||
const edgeComplexityDifference = Math.abs(edge1 - edge2)
|
|
||||||
|
|
||||||
const namePrefix1 = tile1.name.split('_')[0]
|
|
||||||
const namePrefix2 = tile2.name.split('_')[0]
|
|
||||||
|
|
||||||
return colorDifference <= colorSimilarityThreshold && edgeComplexityDifference <= edgeComplexitySimilarityThreshold && namePrefix1 === namePrefix2
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleTag = (tag: string) => {
|
const toggleTag = (tag: string) => {
|
||||||
if (selectedTags.value.includes(tag)) {
|
if (selectedTags.value.includes(tag)) {
|
||||||
selectedTags.value = selectedTags.value.filter((t) => t !== tag)
|
selectedTags.value = selectedTags.value.filter((t) => t !== tag)
|
||||||
@ -162,59 +129,6 @@ const toggleTag = (tag: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function processTile(tile: Tile) {
|
|
||||||
const img = new Image()
|
|
||||||
img.crossOrigin = 'Anonymous'
|
|
||||||
img.onload = () => {
|
|
||||||
const canvas = document.createElement('canvas')
|
|
||||||
const ctx = canvas.getContext('2d')
|
|
||||||
canvas.width = img.width
|
|
||||||
canvas.height = img.height
|
|
||||||
ctx!.drawImage(img, 0, 0, img.width, img.height)
|
|
||||||
|
|
||||||
const imageData = ctx!.getImageData(0, 0, canvas.width, canvas.height)
|
|
||||||
tileColorData.value.set(tile.id, getDominantColor(imageData))
|
|
||||||
tileEdgeData.value.set(tile.id, getEdgeComplexity(imageData))
|
|
||||||
}
|
|
||||||
img.src = `${config.server_endpoint}/textures/tiles/${tile.id}.png`
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDominantColor(imageData: ImageData) {
|
|
||||||
let r = 0,
|
|
||||||
g = 0,
|
|
||||||
b = 0,
|
|
||||||
total = 0
|
|
||||||
|
|
||||||
for (let i = 0; i < imageData.data.length; i += 4) {
|
|
||||||
if (imageData.data[i + 3] > 0) {
|
|
||||||
// Only consider non-transparent pixels
|
|
||||||
r += imageData.data[i]
|
|
||||||
g += imageData.data[i + 1]
|
|
||||||
b += imageData.data[i + 2]
|
|
||||||
total++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
r: Math.round(r / total),
|
|
||||||
g: Math.round(g / total),
|
|
||||||
b: Math.round(b / total)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEdgeComplexity(imageData: ImageData) {
|
|
||||||
let edgePixels = 0
|
|
||||||
for (let y = 0; y < imageData.height; y++) {
|
|
||||||
for (let x = 0; x < imageData.width; x++) {
|
|
||||||
const i = (y * imageData.width + x) * 4
|
|
||||||
if (imageData.data[i + 3] > 0 && (x === 0 || y === 0 || x === imageData.width - 1 || y === imageData.height - 1 || imageData.data[i - 1] === 0 || imageData.data[i + 7] === 0)) {
|
|
||||||
edgePixels++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return edgePixels
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTileCategory(tile: Tile): string {
|
function getTileCategory(tile: Tile): string {
|
||||||
return tileCategories.value.get(tile.id) || ''
|
return tileCategories.value.get(tile.id) || ''
|
||||||
}
|
}
|
||||||
@ -235,22 +149,19 @@ function isActiveTile(tile: Tile): boolean {
|
|||||||
return mapEditor.selectedTile.value === tile.id
|
return mapEditor.selectedTile.value === tile.id
|
||||||
}
|
}
|
||||||
|
|
||||||
let subscription: any = null
|
onMounted(async () => {
|
||||||
|
tiles.value = await tileStorage.getAll()
|
||||||
|
const initialBatchSize = 20
|
||||||
|
const initialTiles = tiles.value.slice(0, initialBatchSize)
|
||||||
|
initialTiles.forEach((tile) => tileProcessor.processTile(tile))
|
||||||
|
|
||||||
onMounted(() => {
|
// Process remaining tiles in background
|
||||||
subscription = liveQuery(() => tileStorage.liveQuery()).subscribe({
|
setTimeout(() => {
|
||||||
next: (result) => {
|
tiles.value.slice(initialBatchSize).forEach((tile) => tileProcessor.processTile(tile))
|
||||||
tiles.value = result
|
}, 1000)
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
console.error('Failed to fetch tiles:', error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (subscription) {
|
tileProcessor.cleanup()
|
||||||
subscription.unsubscribe()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex justify-center p-5">
|
<div class="flex justify-between p-5 w-[calc(100%_-_40px)] fixed bottom-0 left-0 z-20" :class="{ 'list-open': listOpen }">
|
||||||
<div class="toolbar fixed bottom-0 left-0 m-3 rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10">
|
<div class="toolbar rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10">
|
||||||
<div ref="toolbar" class="tools flex gap-2.5" v-if="mapEditor.currentMap.value">
|
<div ref="toolbar" class="tools flex gap-2.5" v-if="mapEditor.currentMap.value">
|
||||||
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditor.tool.value === 'move' }" @click="handleClick('move')">
|
<button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditor.tool.value === 'move' }" @click="handleClick('move')">
|
||||||
<img class="invert w-5 h-5" src="/assets/icons/mapEditor/move.svg" alt="Move camera" /> <span class="h-5" :class="{ 'ml-2.5': mapEditor.tool.value !== 'move' }">(M)</span>
|
<img class="invert w-5 h-5" src="/assets/icons/mapEditor/move.svg" alt="Move camera" /> <span class="h-5" :class="{ 'ml-2.5': mapEditor.tool.value !== 'move' }">(M)</span>
|
||||||
@ -40,7 +40,7 @@
|
|||||||
<div class="select" v-if="mapEditor.tool.value === 'eraser'">
|
<div class="select" v-if="mapEditor.tool.value === 'eraser'">
|
||||||
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectEraserOpen }">
|
<div class="select-trigger group capitalize flex gap-3.5" :class="{ open: selectEraserOpen }">
|
||||||
{{ mapEditor.drawMode.value.replace('_', ' ') }}
|
{{ mapEditor.drawMode.value.replace('_', ' ') }}
|
||||||
<img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/mapEditor/chevron.svg" />
|
<img class="group-[.open]:rotate-180 invert w-5 h-5 rotate-0 transition ease-in-out duration-200" src="/assets/icons/mapEditor/chevron.svg" alt="" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectEraserOpen">
|
<div class="flex flex-col absolute bottom-full mb-5 left-1/2 -translate-x-1/2 bg-gray rounded min-w-28 border border-gray-500 border-solid text-left" v-show="selectEraserOpen">
|
||||||
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('tile', 'eraser')">
|
<span class="py-2 px-2.5 relative hover:bg-cyan hover:text-white" @click="() => handleModeClick('tile', 'eraser')">
|
||||||
@ -72,49 +72,50 @@
|
|||||||
|
|
||||||
<div class="w-px bg-cyan"></div>
|
<div class="w-px bg-cyan"></div>
|
||||||
|
|
||||||
<label class="my-auto gap-0" for="checkbox">Continuous Drawing</label>
|
<button class="flex justify-center items-center min-w-10 p-0 relative" @click="isMapEditorSettingsModalOpen = !isMapEditorSettingsModalOpen"><img class="invert w-5 h-5" src="/assets/icons/mapEditor/settings.svg" alt="Map settings" /> <span class="h-5 ml-2.5">(S)</span></button>
|
||||||
<input type="checkbox" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="toolbar fixed bottom-0 right-0 m-3 rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10 space-x-2">
|
|
||||||
<button class="btn-cyan px-3.5" @click="() => emit('open-maps')">Load</button>
|
|
||||||
<button class="btn-cyan px-3.5" @click="() => emit('save')" v-if="mapEditor.currentMap.value">Save</button>
|
|
||||||
<button class="btn-cyan px-3.5" @click="() => emit('clear')" v-if="mapEditor.currentMap.value">Clear</button>
|
|
||||||
<button class="btn-cyan px-3.5" @click="() => emit('close-editor')">Exit</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="toolbar rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10 space-x-2">
|
||||||
|
<button class="btn-cyan px-3.5" @click="() => emit('open-maps')">Load</button>
|
||||||
|
<button class="btn-cyan px-3.5" @click="() => emit('save')" v-if="mapEditor.currentMap.value">Save</button>
|
||||||
|
<button class="btn-cyan px-3.5" @click="() => emit('clear')" v-if="mapEditor.currentMap.value">Clear</button>
|
||||||
|
<button class="btn-cyan px-3.5" @click="() => emit('close-editor')">Exit</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Modal :isModalOpen="isMapEditorSettingsModalOpen" @modal:close="() => (isMapEditorSettingsModalOpen = false)" :modal-width="300" :modal-height="350" :is-resizable="false" bg-style="none">
|
||||||
|
<template #modalHeader>
|
||||||
|
<h3 class="m-0 font-medium shrink-0 text-white">Map editor settings</h3>
|
||||||
|
</template>
|
||||||
|
<template #modalBody>
|
||||||
|
<div class="m-4 flex items-center space-x-2">
|
||||||
|
<input id="continuous-drawing" @change="handleCheck" v-model="checkboxValue" type="checkbox" class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" />
|
||||||
|
<label for="continuous-drawing" class="text-sm font-medium text-gray-200 cursor-pointer"> Continuous Drawing </label>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
import { onClickOutside } from '@vueuse/core'
|
import { onClickOutside } from '@vueuse/core'
|
||||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
const mapEditor = useMapEditorComposable()
|
const mapEditor = useMapEditorComposable()
|
||||||
|
|
||||||
const emit = defineEmits(['save', 'clear', 'open-maps', 'open-settings', 'close-editor', 'open-tile-list', 'open-map-object-list', 'close-lists'])
|
const emit = defineEmits(['save', 'clear', 'open-maps', 'open-settings', 'close-editor'])
|
||||||
|
|
||||||
// track when clicked outside of toolbar items
|
// States
|
||||||
const toolbar = ref(null)
|
const toolbar = ref(null)
|
||||||
|
const isMapEditorSettingsModalOpen = ref(false)
|
||||||
// track select state
|
const selectPencilOpen = ref(false)
|
||||||
let selectPencilOpen = ref(false)
|
const selectEraserOpen = ref(false)
|
||||||
let selectEraserOpen = ref(false)
|
const checkboxValue = ref<Boolean>(false)
|
||||||
|
const listOpen = ref(false)
|
||||||
let tileListShown = ref(false)
|
|
||||||
let mapObjectListShown = ref(false)
|
|
||||||
|
|
||||||
defineExpose({ tileListShown, mapObjectListShown })
|
|
||||||
|
|
||||||
// drawMode
|
// drawMode
|
||||||
function setDrawMode(value: string) {
|
function setDrawMode(value: string) {
|
||||||
if (mapEditor.tool.value === 'paint' || mapEditor.tool.value === 'pencil') {
|
|
||||||
emit('close-lists')
|
|
||||||
if (value === 'tile') emit('open-tile-list')
|
|
||||||
if (value === 'map_object') emit('open-map-object-list')
|
|
||||||
}
|
|
||||||
|
|
||||||
mapEditor.setDrawMode(value)
|
mapEditor.setDrawMode(value)
|
||||||
selectPencilOpen.value = false
|
selectPencilOpen.value = false
|
||||||
selectEraserOpen.value = false
|
selectEraserOpen.value = false
|
||||||
@ -131,14 +132,24 @@ function setEraserMode() {
|
|||||||
selectEraserOpen.value = false
|
selectEraserOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleCheck() {
|
||||||
|
mapEditor.setInputMode(checkboxValue.value ? 'hold' : 'tap')
|
||||||
|
}
|
||||||
|
|
||||||
function handleModeClick(mode: string, type: 'pencil' | 'eraser') {
|
function handleModeClick(mode: string, type: 'pencil' | 'eraser') {
|
||||||
setDrawMode(mode)
|
setDrawMode(mode)
|
||||||
type === 'pencil' ? setPencilMode() : setEraserMode()
|
type === 'pencil' ? setPencilMode() : setEraserMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClick(tool: string) {
|
function handleClick(tool: string) {
|
||||||
if (tool === 'settings') {
|
if (tool === 'mapEditorSettings') {
|
||||||
emit('open-settings')
|
isMapEditorSettingsModalOpen.value = true
|
||||||
|
listOpen.value = false
|
||||||
|
} else if (tool === 'settings') {
|
||||||
|
listOpen.value = false
|
||||||
|
} else if (tool === 'move') {
|
||||||
|
listOpen.value = false
|
||||||
|
mapEditor.setTool(tool)
|
||||||
} else {
|
} else {
|
||||||
mapEditor.setTool(tool)
|
mapEditor.setTool(tool)
|
||||||
}
|
}
|
||||||
@ -163,12 +174,15 @@ function initKeyShortcuts(event: KeyboardEvent) {
|
|||||||
// prevent if focused on composables
|
// prevent if focused on composables
|
||||||
if (document.activeElement?.tagName === 'INPUT') return
|
if (document.activeElement?.tagName === 'INPUT') return
|
||||||
|
|
||||||
|
if (event.ctrlKey) return
|
||||||
|
|
||||||
const keyActions: { [key: string]: string } = {
|
const keyActions: { [key: string]: string } = {
|
||||||
m: 'move',
|
m: 'move',
|
||||||
p: 'pencil',
|
p: 'pencil',
|
||||||
e: 'eraser',
|
e: 'eraser',
|
||||||
b: 'paint',
|
b: 'paint',
|
||||||
z: 'settings'
|
z: 'settings',
|
||||||
|
s: 'mapEditorSettings'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keyActions.hasOwnProperty(event.key)) {
|
if (keyActions.hasOwnProperty(event.key)) {
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { login } from '@/services/authentication'
|
import { login } from '@/services/authenticationService'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { newPassword } from '@/services/authentication'
|
import { newPassword } from '@/services/authenticationService'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { login, register } from '@/services/authentication'
|
import { login, register } from '@/services/authenticationService'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import { resetPassword } from '@/services/authentication'
|
import { resetPassword } from '@/services/authenticationService'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
@ -18,11 +18,11 @@
|
|||||||
<div class="absolute right-[calc(100%_+_16px)] -top-px flex gap-2 flex-col">
|
<div class="absolute right-[calc(100%_+_16px)] -top-px flex gap-2 flex-col">
|
||||||
<div v-for="character in characters" :key="character.id" class="character relative rounded default-border w-12 h-12 bg-[url('/assets/ui-texture.png')] after:absolute after:w-full after:h-px after:bg-gray-500" :class="{ active: selectedCharacterId === character.id }">
|
<div v-for="character in characters" :key="character.id" class="character relative rounded default-border w-12 h-12 bg-[url('/assets/ui-texture.png')] after:absolute after:w-full after:h-px after:bg-gray-500" :class="{ active: selectedCharacterId === character.id }">
|
||||||
<img src="/assets/placeholders/head.png" class="w-9 h-9 object-contain center-element" alt="Player head" />
|
<img src="/assets/placeholders/head.png" class="w-9 h-9 object-contain center-element" alt="Player head" />
|
||||||
<input class="h-full w-full absolute m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0" type="radio" name="character" :value="character.id" v-model="selectedCharacterId" />
|
<input class="h-full w-full absolute m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0 btn-sound" type="radio" name="character" :value="character.id" v-model="selectedCharacterId" />
|
||||||
</div>
|
</div>
|
||||||
<div class="character relative rounded default-border w-12 h-12 bg-[url('/assets/ui-texture.png')]" :class="{ active: characters.length == 0 }" v-if="characters.length < 4">
|
<div class="character relative rounded default-border w-12 h-12 bg-[url('/assets/ui-texture.png')]" :class="{ active: characters.length == 0 }" v-if="characters.length < 4">
|
||||||
<button class="p-0 h-full w-full flex flex-col justify-between focus-visible:outline-offset-0" @click="isCreateNewCharacterModalOpen = true">
|
<button class="p-0 h-full w-full flex flex-col justify-between focus-visible:outline-offset-0 btn-sound" @click="isCreateNewCharacterModalOpen = true">
|
||||||
<img class="w-6 h-6 object-contain center-element" draggable="false" src="/assets/icons/plus-icon.svg" />
|
<img class="w-6 h-6 object-contain center-element btn-sound" draggable="false" src="/assets/icons/plus-icon.svg" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -124,10 +124,12 @@
|
|||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
import { type CharacterHair, type Character as CharacterT, type Map } from '@/application/types'
|
import { type CharacterHair, type Character as CharacterT, type Map } from '@/application/types'
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
|
import { useSoundComposable } from '@/composables/useSoundComposable'
|
||||||
import { CharacterHairStorage } from '@/storage/storages'
|
import { CharacterHairStorage } from '@/storage/storages'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
const { playSound } = useSoundComposable()
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const isLoading = ref<boolean>(true)
|
const isLoading = ref<boolean>(true)
|
||||||
const characters = ref<CharacterT[]>([])
|
const characters = ref<CharacterT[]>([])
|
||||||
@ -165,11 +167,10 @@ function loginWithCharacter() {
|
|||||||
|
|
||||||
// Create character logics
|
// Create character logics
|
||||||
function createCharacter() {
|
function createCharacter() {
|
||||||
gameStore.connection?.on('character:create:success', (data: CharacterT) => {
|
gameStore.connection?.emit('character:create', { name: newCharacterName.value }, (success: boolean) => {
|
||||||
gameStore.setCharacter(data)
|
if (success) return
|
||||||
isCreateNewCharacterModalOpen.value = false
|
isCreateNewCharacterModalOpen.value = false
|
||||||
})
|
})
|
||||||
gameStore.connection?.emit('character:create', { name: newCharacterName.value })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch changes for selected character and update hairs
|
// Watch changes for selected character and update hairs
|
||||||
@ -179,6 +180,7 @@ watch(selectedCharacterId, (characterId) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
playSound('/assets/music/intro.mp3')
|
||||||
const characterHairStorage = new CharacterHairStorage()
|
const characterHairStorage = new CharacterHairStorage()
|
||||||
characterHairs.value = await characterHairStorage.getAll()
|
characterHairs.value = await characterHairStorage.getAll()
|
||||||
})
|
})
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex justify-center items-center h-dvh relative">
|
<div class="flex justify-center items-center h-dvh relative">
|
||||||
<Game :config="gameConfig" @create="createGame">
|
<Game :config="gameConfig" @create="createGame">
|
||||||
<Scene name="main" @preload="preloadScene" @create="createScene">
|
<Scene name="main" @preload="preloadScene">
|
||||||
<Menu />
|
<Menu />
|
||||||
<Hud />
|
<Hud />
|
||||||
<Hotkeys />
|
<Hotkeys />
|
||||||
@ -27,18 +27,23 @@ import Hotkeys from '@/components/game/gui/Hotkeys.vue'
|
|||||||
import Hud from '@/components/game/gui/Hud.vue'
|
import Hud from '@/components/game/gui/Hud.vue'
|
||||||
import Menu from '@/components/game/gui/Menu.vue'
|
import Menu from '@/components/game/gui/Menu.vue'
|
||||||
import Map from '@/components/game/map/Map.vue'
|
import Map from '@/components/game/map/Map.vue'
|
||||||
|
import { useSoundComposable } from '@/composables/useSoundComposable'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { Game, Scene } from 'phavuer'
|
import { Game, Scene } from 'phavuer'
|
||||||
import { onBeforeUnmount } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
const { playSound, stopSound } = useSoundComposable()
|
||||||
|
|
||||||
const gameConfig = {
|
const gameConfig = {
|
||||||
name: config.name,
|
name: config.name,
|
||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
height: window.innerHeight,
|
height: window.innerHeight,
|
||||||
type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS
|
type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS
|
||||||
resolution: 5
|
resolution: 5,
|
||||||
|
input: {
|
||||||
|
windowEvents: false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createGame = (game: Phaser.Game) => {
|
const createGame = (game: Phaser.Game) => {
|
||||||
@ -55,6 +60,8 @@ const createGame = (game: Phaser.Game) => {
|
|||||||
})
|
})
|
||||||
gameStore.disconnectSocket()
|
gameStore.disconnectSocket()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
playSound('/assets/sounds/connect.wav')
|
||||||
}
|
}
|
||||||
|
|
||||||
function preloadScene(scene: Phaser.Scene) {
|
function preloadScene(scene: Phaser.Scene) {
|
||||||
@ -63,7 +70,7 @@ function preloadScene(scene: Phaser.Scene) {
|
|||||||
scene.load.image('waypoint', '/assets/waypoint.png')
|
scene.load.image('waypoint', '/assets/waypoint.png')
|
||||||
}
|
}
|
||||||
|
|
||||||
function createScene(scene: Phaser.Scene) {}
|
onMounted(() => {
|
||||||
|
stopSound('/assets/music/intro.mp3')
|
||||||
onBeforeUnmount(() => {})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -8,8 +8,8 @@
|
|||||||
<div class="h-dvh flex items-center lg:justify-center flex-col px-8 max-lg:pt-20">
|
<div class="h-dvh flex items-center lg:justify-center flex-col px-8 max-lg:pt-20">
|
||||||
<!-- <img src="/assets/tlogo.png" class="mb-10 w-52" alt="Noxious logo" />-->
|
<!-- <img src="/assets/tlogo.png" class="mb-10 w-52" alt="Noxious logo" />-->
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<img src="/assets/ui-elements/login-ui-box-outer.svg" class="absolute w-full h-full" alt="UI box outer" />
|
<img src="/assets/ui-elements/login-ui-box-outer.svg" class="absolute w-full h-full" alt="" />
|
||||||
<img src="/assets/ui-elements/login-ui-box-inner.svg" class="absolute left-2 top-2 w-[calc(100%_-_16px)] h-[calc(100%_-_16px)]" alt="UI box inner" />
|
<img src="/assets/ui-elements/login-ui-box-inner.svg" class="absolute left-2 top-2 w-[calc(100%_-_16px)] h-[calc(100%_-_16px)]" alt="" />
|
||||||
|
|
||||||
<!-- Login Form -->
|
<!-- Login Form -->
|
||||||
<LoginForm v-if="currentForm === 'login' && !doesUrlHaveToken" @openResetPasswordModal="() => (isPasswordResetFormShown = true)" @switchToRegister="currentForm = 'register'" />
|
<LoginForm v-if="currentForm === 'login' && !doesUrlHaveToken" @openResetPasswordModal="() => (isPasswordResetFormShown = true)" @switchToRegister="currentForm = 'register'" />
|
||||||
|
@ -4,22 +4,11 @@
|
|||||||
<Scene name="main" @preload="preloadScene">
|
<Scene name="main" @preload="preloadScene">
|
||||||
<div v-if="!isLoaded" class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white text-3xl font-ui">Loading...</div>
|
<div v-if="!isLoaded" class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white text-3xl font-ui">Loading...</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<Map :key="mapEditor.currentMap.value?.id" />
|
<Map v-if="mapEditor.currentMap.value" :key="mapEditor.currentMap.value?.id" />
|
||||||
<Toolbar
|
<Toolbar ref="toolbar" @save="save" @clear="clear" @open-maps="mapModal?.open" @open-settings="mapSettingsModal?.open" @close-editor="mapEditor.toggleActive" />
|
||||||
ref="toolbar"
|
|
||||||
@save="save"
|
|
||||||
@clear="clear"
|
|
||||||
@open-maps="mapModal?.open"
|
|
||||||
@open-settings="mapSettingsModal?.open"
|
|
||||||
@close-editor="mapEditor.toggleActive"
|
|
||||||
@close-lists="tileModal?.close"
|
|
||||||
@closeLists="objectModal?.close"
|
|
||||||
@open-tile-list="tileModal?.open"
|
|
||||||
@open-map-object-list="objectModal?.open"
|
|
||||||
/>
|
|
||||||
<MapList ref="mapModal" @open-create-map="mapSettingsModal?.open" />
|
<MapList ref="mapModal" @open-create-map="mapSettingsModal?.open" />
|
||||||
<TileList ref="tileModal" />
|
<TileList v-if="mapEditor.tool.value === 'pencil' && mapEditor.drawMode.value === 'tile'" />
|
||||||
<ObjectList ref="objectModal" />
|
<MapObjectList v-if="mapEditor.tool.value === 'pencil' && mapEditor.drawMode.value === 'map_object'" />
|
||||||
<MapSettings ref="mapSettingsModal" />
|
<MapSettings ref="mapSettingsModal" />
|
||||||
<TeleportModal ref="teleportModal" />
|
<TeleportModal ref="teleportModal" />
|
||||||
</div>
|
</div>
|
||||||
@ -34,28 +23,24 @@ import 'phaser'
|
|||||||
import type { Map as MapT } from '@/application/types'
|
import type { Map as MapT } from '@/application/types'
|
||||||
import Map from '@/components/gameMaster/mapEditor/Map.vue'
|
import Map from '@/components/gameMaster/mapEditor/Map.vue'
|
||||||
import MapList from '@/components/gameMaster/mapEditor/partials/MapList.vue'
|
import MapList from '@/components/gameMaster/mapEditor/partials/MapList.vue'
|
||||||
import ObjectList from '@/components/gameMaster/mapEditor/partials/MapObjectList.vue'
|
import MapObjectList from '@/components/gameMaster/mapEditor/partials/MapObjectList.vue'
|
||||||
import MapSettings from '@/components/gameMaster/mapEditor/partials/MapSettings.vue'
|
import MapSettings from '@/components/gameMaster/mapEditor/partials/MapSettings.vue'
|
||||||
import TeleportModal from '@/components/gameMaster/mapEditor/partials/TeleportModal.vue'
|
import TeleportModal from '@/components/gameMaster/mapEditor/partials/TeleportModal.vue'
|
||||||
import TileList from '@/components/gameMaster/mapEditor/partials/TileList.vue'
|
import TileList from '@/components/gameMaster/mapEditor/partials/TileList.vue'
|
||||||
import Toolbar from '@/components/gameMaster/mapEditor/partials/Toolbar.vue'
|
import Toolbar from '@/components/gameMaster/mapEditor/partials/Toolbar.vue'
|
||||||
import { loadAllTilesIntoScene } from '@/composables/mapComposable'
|
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
|
import { loadAllTileTextures } from '@/services/mapService'
|
||||||
import { MapStorage } from '@/storage/storages'
|
import { MapStorage } from '@/storage/storages'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { Game, Scene } from 'phavuer'
|
import { Game, Scene } from 'phavuer'
|
||||||
import { ref, useTemplateRef, watch } from 'vue'
|
import { ref, useTemplateRef } from 'vue'
|
||||||
|
|
||||||
const mapStorage = new MapStorage()
|
const mapStorage = new MapStorage()
|
||||||
const mapEditor = useMapEditorComposable()
|
const mapEditor = useMapEditorComposable()
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
|
||||||
const toolbar = useTemplateRef('toolbar')
|
|
||||||
const mapModal = useTemplateRef('mapModal')
|
const mapModal = useTemplateRef('mapModal')
|
||||||
const tileModal = useTemplateRef('tileModal')
|
|
||||||
const objectModal = useTemplateRef('objectModal')
|
|
||||||
const mapSettingsModal = useTemplateRef('mapSettingsModal')
|
const mapSettingsModal = useTemplateRef('mapSettingsModal')
|
||||||
const teleportModal = useTemplateRef('teleportModal')
|
|
||||||
|
|
||||||
const isLoaded = ref(false)
|
const isLoaded = ref(false)
|
||||||
|
|
||||||
@ -64,7 +49,10 @@ const gameConfig = {
|
|||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
height: window.innerHeight,
|
height: window.innerHeight,
|
||||||
type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS
|
type: Phaser.AUTO, // AUTO, CANVAS, WEBGL, HEADLESS
|
||||||
resolution: 5
|
resolution: 5,
|
||||||
|
input: {
|
||||||
|
windowEvents: false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createGame = (game: Phaser.Game) => {
|
const createGame = (game: Phaser.Game) => {
|
||||||
@ -82,7 +70,7 @@ const preloadScene = async (scene: Phaser.Scene) => {
|
|||||||
scene.load.image('waypoint', '/assets/waypoint.png')
|
scene.load.image('waypoint', '/assets/waypoint.png')
|
||||||
|
|
||||||
// Get all tiles from IndexedDB and load them into the scene
|
// Get all tiles from IndexedDB and load them into the scene
|
||||||
await loadAllTilesIntoScene(scene)
|
await loadAllTileTextures(scene)
|
||||||
|
|
||||||
// Wait for all assets to be loaded before continuing
|
// Wait for all assets to be loaded before continuing
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
@ -104,9 +92,9 @@ function save() {
|
|||||||
height: currentMap.height,
|
height: currentMap.height,
|
||||||
tiles: currentMap.tiles,
|
tiles: currentMap.tiles,
|
||||||
pvp: currentMap.pvp,
|
pvp: currentMap.pvp,
|
||||||
mapEffects: currentMap.mapEffects?.map(({ id, effect, strength }) => ({ id, effect, strength })) ?? [],
|
mapEffects: currentMap.mapEffects,
|
||||||
mapEventTiles: currentMap.mapEventTiles?.map(({ id, type, positionX, positionY, teleport }) => ({ id, type, positionX, positionY, teleport })) ?? [],
|
mapEventTiles: currentMap.mapEventTiles,
|
||||||
placedMapObjects: currentMap.placedMapObjects?.map(({ id, mapObject, depth, isRotated, positionX, positionY }) => ({ id, mapObject, depth, isRotated, positionX, positionY })) ?? []
|
placedMapObjects: currentMap.placedMapObjects.map(({ id, mapObject, isRotated, positionX, positionY }) => ({ id, mapObject, isRotated, positionX, positionY })) ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:map:update', data, (response: MapT) => {
|
gameStore.connection?.emit('gm:map:update', data, (response: MapT) => {
|
||||||
@ -119,5 +107,6 @@ function clear() {
|
|||||||
|
|
||||||
// Clear placed objects, event tiles and tiles
|
// Clear placed objects, event tiles and tiles
|
||||||
mapEditor.clearMap()
|
mapEditor.clearMap()
|
||||||
|
mapEditor.triggerClearTiles()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -3,8 +3,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useCameraControls } from '@/composables/useCameraControls'
|
import { useControlsComposable } from '@/composables/useControlsComposable'
|
||||||
import { usePointerHandlers } from '@/composables/usePointerHandlers'
|
|
||||||
import { Image, useScene } from 'phavuer'
|
import { Image, useScene } from 'phavuer'
|
||||||
import { onBeforeUnmount, ref } from 'vue'
|
import { onBeforeUnmount, ref } from 'vue'
|
||||||
|
|
||||||
@ -14,45 +13,16 @@ type WayPoint = { visible: boolean; x: number; y: number }
|
|||||||
// Props
|
// Props
|
||||||
const props = defineProps<{ layer: Phaser.Tilemaps.TilemapLayer }>()
|
const props = defineProps<{ layer: Phaser.Tilemaps.TilemapLayer }>()
|
||||||
|
|
||||||
// Constants
|
|
||||||
const ZOOM_SETTINGS = {
|
|
||||||
WHEEL_FACTOR: 0.005,
|
|
||||||
KEY_FACTOR: 0.3,
|
|
||||||
MIN: 1,
|
|
||||||
MAX: 3
|
|
||||||
} as const
|
|
||||||
|
|
||||||
// Setup
|
// Setup
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
const waypoint = ref<WayPoint>({ visible: false, x: 0, y: 0 })
|
const waypoint = ref<WayPoint>({ visible: false, x: 0, y: 0 })
|
||||||
const { camera } = useCameraControls(scene)
|
const { setupControls, cleanupControls } = useControlsComposable(scene, props.layer, waypoint)
|
||||||
const { setupPointerHandlers, cleanupPointerHandlers } = usePointerHandlers(scene, props.layer, waypoint, camera)
|
|
||||||
|
|
||||||
// Handlers
|
|
||||||
function handleScrollZoom(pointer: Phaser.Input.Pointer) {
|
|
||||||
if (!(pointer.event instanceof WheelEvent && pointer.event.shiftKey)) return
|
|
||||||
|
|
||||||
const zoomLevel = Phaser.Math.Clamp(camera.zoom - pointer.event.deltaY * ZOOM_SETTINGS.WHEEL_FACTOR, ZOOM_SETTINGS.MIN, ZOOM_SETTINGS.MAX)
|
|
||||||
camera.setZoom(zoomLevel)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleKeyComboZoom(event: { keyCodes: number[] }) {
|
|
||||||
const deltaY = event.keyCodes[1] === 38 ? 1 : -1 // 38 is Up, 40 is Down
|
|
||||||
const zoomLevel = Phaser.Math.Clamp(camera.zoom + deltaY * ZOOM_SETTINGS.KEY_FACTOR, ZOOM_SETTINGS.MIN, ZOOM_SETTINGS.MAX)
|
|
||||||
camera.setZoom(zoomLevel)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Event setup
|
// Event setup
|
||||||
setupPointerHandlers()
|
setupControls()
|
||||||
scene.input.keyboard?.createCombo([16, 38], { resetOnMatch: true }) // Shift + Up
|
|
||||||
scene.input.keyboard?.createCombo([16, 40], { resetOnMatch: true }) // Shift + Down
|
|
||||||
scene.input.keyboard?.on('keycombomatch', handleKeyComboZoom)
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_WHEEL, handleScrollZoom)
|
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
cleanupPointerHandlers()
|
cleanupControls()
|
||||||
scene.input.keyboard?.off('keycombomatch')
|
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_WHEEL, handleScrollZoom)
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,7 +1,15 @@
|
|||||||
<template></template>
|
<template></template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { CharacterHairStorage, CharacterTypeStorage, MapObjectStorage, MapStorage, SpriteStorage, TileStorage } from '@/storage/storages'
|
import {
|
||||||
|
CharacterHairStorage,
|
||||||
|
CharacterTypeStorage,
|
||||||
|
MapObjectStorage,
|
||||||
|
MapStorage,
|
||||||
|
SoundStorage,
|
||||||
|
SpriteStorage,
|
||||||
|
TileStorage
|
||||||
|
} from '@/storage/storages'
|
||||||
import { TextureStorage } from '@/storage/textureStorage'
|
import { TextureStorage } from '@/storage/textureStorage'
|
||||||
import { onMounted, onUnmounted } from 'vue'
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
|
|
||||||
@ -12,6 +20,7 @@ const spriteStorage = new SpriteStorage()
|
|||||||
const characterTypeStorage = new CharacterTypeStorage()
|
const characterTypeStorage = new CharacterTypeStorage()
|
||||||
const characterHairStorage = new CharacterHairStorage()
|
const characterHairStorage = new CharacterHairStorage()
|
||||||
const textureStorage = new TextureStorage()
|
const textureStorage = new TextureStorage()
|
||||||
|
const soundStorage = new SoundStorage()
|
||||||
|
|
||||||
let currentString = ''
|
let currentString = ''
|
||||||
|
|
||||||
@ -32,6 +41,7 @@ async function handleKeyPress(event: KeyboardEvent) {
|
|||||||
await characterTypeStorage.destroy()
|
await characterTypeStorage.destroy()
|
||||||
await characterHairStorage.destroy()
|
await characterHairStorage.destroy()
|
||||||
await textureStorage.destroy()
|
await textureStorage.destroy()
|
||||||
|
await soundStorage.destroy()
|
||||||
currentString = '' // Reset
|
currentString = '' // Reset
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,11 +14,11 @@
|
|||||||
<slot name="modalHeader" />
|
<slot name="modalHeader" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2.5">
|
<div class="flex gap-2.5">
|
||||||
<button v-if="canFullScreen" @click="toggleFullScreen" class="w-5 h-5 m-0 p-0 relative hover:scale-110 transition-transform duration-300 ease-in-out">
|
<button v-if="canFullScreen" @click="toggleFullScreen" class="w-5 h-5 m-0 p-0 relative hover:scale-110 transition-transform duration-300 ease-in-out btn-sound">
|
||||||
<img :alt="isFullScreen ? 'exit full-screen' : 'full-screen'" :src="isFullScreen ? '/assets/icons/modal/minimize.svg' : '/assets/icons/modal/increase-size-option.svg'" class="w-3.5 h-3.5 invert" draggable="false" />
|
<img :alt="isFullScreen ? 'exit full-screen' : 'full-screen'" :src="isFullScreen ? '/assets/icons/modal/minimize.svg' : '/assets/icons/modal/increase-size-option.svg'" class="w-3.5 h-3.5 invert" draggable="false" />
|
||||||
</button>
|
</button>
|
||||||
<button v-if="closable" @click="closeModal" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
|
<button v-if="closable" @click="closeModal" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out btn-sound">
|
||||||
<img alt="close" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" draggable="false" />
|
<img alt="close" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full btn-sound" draggable="false" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -86,7 +86,11 @@ const emit = defineEmits<{
|
|||||||
defineExpose({
|
defineExpose({
|
||||||
open: () => (isModalOpenRef.value = true),
|
open: () => (isModalOpenRef.value = true),
|
||||||
close: () => (isModalOpenRef.value = false),
|
close: () => (isModalOpenRef.value = false),
|
||||||
toggle: () => (isModalOpenRef.value = !isModalOpenRef.value)
|
toggle: () => (isModalOpenRef.value = !isModalOpenRef.value),
|
||||||
|
setPosition: (a: number, b: number) => {
|
||||||
|
x.value = a
|
||||||
|
y.value = b
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const isModalOpenRef = ref(props.isModalOpen)
|
const isModalOpenRef = ref(props.isModalOpen)
|
||||||
|
69
src/composables/controls/useBaseControlsComposable.ts
Normal file
69
src/composables/controls/useBaseControlsComposable.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import config from '@/application/config'
|
||||||
|
import { getTile, tileToWorldXY } from '@/services/mapService'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { ref, type Ref } from 'vue'
|
||||||
|
|
||||||
|
export function useBaseControlsComposable(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const pointerStartPosition = ref({ x: 0, y: 0 })
|
||||||
|
const dragThreshold = 5 // pixels
|
||||||
|
|
||||||
|
function updateWaypoint(worldX: number, worldY: number) {
|
||||||
|
const pointerTile = getTile(layer, worldX, worldY)
|
||||||
|
if (!pointerTile) {
|
||||||
|
waypoint.value.visible = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const worldPoint = tileToWorldXY(layer, pointerTile.x, pointerTile.y)
|
||||||
|
if (!worldPoint.worldPositionX || !worldPoint.worldPositionX) return
|
||||||
|
|
||||||
|
waypoint.value = {
|
||||||
|
visible: true,
|
||||||
|
x: worldPoint.worldPositionX,
|
||||||
|
y: worldPoint.worldPositionY + config.tile_size.height + 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragMap(pointer: Phaser.Input.Pointer) {
|
||||||
|
if (!gameStore.game.isPlayerDraggingCamera) return
|
||||||
|
|
||||||
|
const deltaX = pointer.x - pointerStartPosition.value.x
|
||||||
|
const deltaY = pointer.y - pointerStartPosition.value.y
|
||||||
|
|
||||||
|
if (Math.abs(deltaX) <= dragThreshold && Math.abs(deltaY) <= dragThreshold) return
|
||||||
|
|
||||||
|
const scrollX = camera.scrollX - deltaX / camera.zoom
|
||||||
|
const scrollY = camera.scrollY - deltaY / camera.zoom
|
||||||
|
|
||||||
|
camera.setScroll(scrollX, scrollY)
|
||||||
|
pointerStartPosition.value = { x: pointer.x, y: pointer.y }
|
||||||
|
}
|
||||||
|
|
||||||
|
function startDragging(pointer: Phaser.Input.Pointer) {
|
||||||
|
pointerStartPosition.value = { x: pointer.x, y: pointer.y }
|
||||||
|
gameStore.setPlayerDraggingCamera(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopDragging() {
|
||||||
|
gameStore.setPlayerDraggingCamera(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleZoom(pointer: Phaser.Input.Pointer) {
|
||||||
|
if (pointer.event instanceof WheelEvent && pointer.event.shiftKey) {
|
||||||
|
const deltaY = pointer.event.deltaY
|
||||||
|
const zoomLevel = camera.zoom - deltaY * 0.005
|
||||||
|
if (zoomLevel > 0 && zoomLevel < 3) {
|
||||||
|
camera.setZoom(zoomLevel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateWaypoint,
|
||||||
|
handleDragMap,
|
||||||
|
startDragging,
|
||||||
|
stopDragging,
|
||||||
|
handleZoom,
|
||||||
|
pointerStartPosition
|
||||||
|
}
|
||||||
|
}
|
114
src/composables/controls/useGameControlsComposable.ts
Normal file
114
src/composables/controls/useGameControlsComposable.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { getTile } from '@/services/mapService'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import type { Ref } from 'vue'
|
||||||
|
import { useBaseControlsComposable } from './useBaseControlsComposable'
|
||||||
|
|
||||||
|
export function useGameControlsComposable(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const baseHandlers = useBaseControlsComposable(scene, layer, waypoint, camera)
|
||||||
|
|
||||||
|
function handlePointerDown(pointer: Phaser.Input.Pointer) {
|
||||||
|
baseHandlers.startDragging(pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerMove(pointer: Phaser.Input.Pointer) {
|
||||||
|
baseHandlers.updateWaypoint(pointer.worldX, pointer.worldY)
|
||||||
|
baseHandlers.handleDragMap(pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerUp(pointer: Phaser.Input.Pointer) {
|
||||||
|
baseHandlers.stopDragging()
|
||||||
|
|
||||||
|
const pointerTile = getTile(layer, pointer.worldX, pointer.worldY)
|
||||||
|
if (!pointerTile) return
|
||||||
|
|
||||||
|
gameStore.connection?.emit('map:character:move', {
|
||||||
|
positionX: pointerTile.x,
|
||||||
|
positionY: pointerTile.y
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const pressedKeys = new Set<string>()
|
||||||
|
let moveInterval: number | null = null
|
||||||
|
|
||||||
|
function handleKeyDown(event: KeyboardEvent) {
|
||||||
|
if (!gameStore.character) return
|
||||||
|
|
||||||
|
// console.log(event.key)
|
||||||
|
|
||||||
|
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) {
|
||||||
|
pressedKeys.add(event.key)
|
||||||
|
|
||||||
|
// Start movement loop if not already running
|
||||||
|
if (!moveInterval) {
|
||||||
|
moveInterval = window.setInterval(moveCharacter, 100) // Adjust timing as needed
|
||||||
|
moveCharacter() // Move immediately on first press
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attack on CTRL
|
||||||
|
if (event.key === 'Control') {
|
||||||
|
gameStore.connection?.emit('map:character:attack')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyUp(event: KeyboardEvent) {
|
||||||
|
pressedKeys.delete(event.key)
|
||||||
|
|
||||||
|
// If no movement keys are pressed, clear the interval
|
||||||
|
if (pressedKeys.size === 0 && moveInterval) {
|
||||||
|
clearInterval(moveInterval)
|
||||||
|
moveInterval = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveCharacter() {
|
||||||
|
if (!gameStore.character) return
|
||||||
|
const { positionX, positionY } = gameStore.character
|
||||||
|
|
||||||
|
if (pressedKeys.has('ArrowLeft')) {
|
||||||
|
gameStore.connection?.emit('map:character:move', {
|
||||||
|
positionX: positionX - 1,
|
||||||
|
positionY: positionY
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (pressedKeys.has('ArrowRight')) {
|
||||||
|
gameStore.connection?.emit('map:character:move', {
|
||||||
|
positionX: positionX + 1,
|
||||||
|
positionY: positionY
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (pressedKeys.has('ArrowUp')) {
|
||||||
|
gameStore.connection?.emit('map:character:move', {
|
||||||
|
positionX: positionX,
|
||||||
|
positionY: positionY - 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (pressedKeys.has('ArrowDown')) {
|
||||||
|
gameStore.connection?.emit('map:character:move', {
|
||||||
|
positionX: positionX,
|
||||||
|
positionY: positionY + 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setupControls = () => {
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_WHEEL, baseHandlers.handleZoom)
|
||||||
|
scene.input.keyboard!.on('keydown', handleKeyDown)
|
||||||
|
scene.input.keyboard!.on('keyup', handleKeyUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanupControls = () => {
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_WHEEL, baseHandlers.handleZoom)
|
||||||
|
scene.input.keyboard!.off('keydown', handleKeyDown)
|
||||||
|
scene.input.keyboard!.off('keyup', handleKeyUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { setupControls, cleanupControls }
|
||||||
|
}
|
42
src/composables/controls/useMapEditorControlsComposable.ts
Normal file
42
src/composables/controls/useMapEditorControlsComposable.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
|
import { computed, type Ref } from 'vue'
|
||||||
|
import { useBaseControlsComposable } from './useBaseControlsComposable'
|
||||||
|
|
||||||
|
export function useMapEditorControlsComposable(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
|
||||||
|
const mapEditor = useMapEditorComposable()
|
||||||
|
const baseHandlers = useBaseControlsComposable(scene, layer, waypoint, camera)
|
||||||
|
const isMoveTool = computed(() => mapEditor.tool.value === 'move')
|
||||||
|
|
||||||
|
function handlePointerDown(pointer: Phaser.Input.Pointer) {
|
||||||
|
if (isMoveTool.value || pointer.event.shiftKey) {
|
||||||
|
baseHandlers.startDragging(pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerMove(pointer: Phaser.Input.Pointer) {
|
||||||
|
if (isMoveTool.value || pointer.event.shiftKey) {
|
||||||
|
baseHandlers.handleDragMap(pointer)
|
||||||
|
}
|
||||||
|
baseHandlers.updateWaypoint(pointer.worldX, pointer.worldY)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerUp(pointer: Phaser.Input.Pointer) {
|
||||||
|
baseHandlers.stopDragging()
|
||||||
|
}
|
||||||
|
|
||||||
|
const setupControls = () => {
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
||||||
|
scene.input.on(Phaser.Input.Events.POINTER_WHEEL, baseHandlers.handleZoom)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanupControls = () => {
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_WHEEL, baseHandlers.handleZoom)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { setupControls, cleanupControls }
|
||||||
|
}
|
@ -1,70 +0,0 @@
|
|||||||
import config from '@/application/config'
|
|
||||||
import { getTile, tileToWorldXY } from '@/composables/mapComposable'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import { ref, type Ref } from 'vue'
|
|
||||||
|
|
||||||
export function useGamePointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const pointerStartPosition = ref({ x: 0, y: 0 })
|
|
||||||
const dragThreshold = 5 // pixels
|
|
||||||
|
|
||||||
function updateWaypoint(worldX: number, worldY: number) {
|
|
||||||
const pointerTile = getTile(layer, worldX, worldY)
|
|
||||||
if (!pointerTile) {
|
|
||||||
waypoint.value.visible = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const worldPoint = tileToWorldXY(layer, pointerTile.x, pointerTile.y)
|
|
||||||
if (!worldPoint.worldPositionX || !worldPoint.worldPositionX) return
|
|
||||||
|
|
||||||
waypoint.value = {
|
|
||||||
visible: true,
|
|
||||||
x: worldPoint.worldPositionX,
|
|
||||||
y: worldPoint.worldPositionY + config.tile_size.height + 15
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePointerDown(pointer: Phaser.Input.Pointer) {
|
|
||||||
pointerStartPosition.value = pointer.position
|
|
||||||
gameStore.setPlayerDraggingCamera(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePointerMove(pointer: Phaser.Input.Pointer) {
|
|
||||||
updateWaypoint(pointer.worldX, pointer.worldY)
|
|
||||||
|
|
||||||
if (!gameStore.game.isPlayerDraggingCamera) return
|
|
||||||
|
|
||||||
// If the distance is less than the drag threshold, return
|
|
||||||
// We do this to prevent the camera from scrolling too quickly
|
|
||||||
const distance = Phaser.Math.Distance.Between(pointerStartPosition.value.x, pointerStartPosition.value.y, pointer.x, pointer.y)
|
|
||||||
if (distance <= dragThreshold) return
|
|
||||||
|
|
||||||
camera.setScroll(camera.scrollX - (pointer.x - pointer.prevPosition.x) / camera.zoom, camera.scrollY - (pointer.y - pointer.prevPosition.y) / camera.zoom)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePointerUp(pointer: Phaser.Input.Pointer) {
|
|
||||||
gameStore.setPlayerDraggingCamera(false)
|
|
||||||
|
|
||||||
const pointerTile = getTile(layer, pointer.worldX, pointer.worldY)
|
|
||||||
if (!pointerTile) return
|
|
||||||
|
|
||||||
gameStore.connection?.emit('map:character:move', {
|
|
||||||
positionX: pointerTile.x,
|
|
||||||
positionY: pointerTile.y
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const setupPointerHandlers = () => {
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
|
||||||
}
|
|
||||||
|
|
||||||
const cleanupPointerHandlers = () => {
|
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
|
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
|
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
|
||||||
}
|
|
||||||
|
|
||||||
return { setupPointerHandlers, cleanupPointerHandlers }
|
|
||||||
}
|
|
@ -1,86 +0,0 @@
|
|||||||
import config from '@/application/config'
|
|
||||||
import { getTile, tileToWorldXY } from '@/composables/mapComposable'
|
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import { computed, ref, type Ref } from 'vue'
|
|
||||||
|
|
||||||
export function useMapEditorPointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const mapEditor = useMapEditorComposable()
|
|
||||||
const isMoveTool = computed(() => mapEditor.tool.value === 'move')
|
|
||||||
const pointerStartPosition = ref({ x: 0, y: 0 })
|
|
||||||
const dragThreshold = 5 // pixels
|
|
||||||
|
|
||||||
function updateWaypoint(worldX: number, worldY: number) {
|
|
||||||
const pointerTile = getTile(layer, worldX, worldY)
|
|
||||||
if (!pointerTile) {
|
|
||||||
waypoint.value.visible = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const worldPoint = tileToWorldXY(layer, pointerTile.x, pointerTile.y)
|
|
||||||
if (!worldPoint.worldPositionX || !worldPoint.worldPositionX) return
|
|
||||||
|
|
||||||
waypoint.value = {
|
|
||||||
visible: true,
|
|
||||||
x: worldPoint.worldPositionX,
|
|
||||||
y: worldPoint.worldPositionY + config.tile_size.height + 15
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePointerDown(pointer: Phaser.Input.Pointer) {
|
|
||||||
pointerStartPosition.value = { x: pointer.x, y: pointer.y }
|
|
||||||
if (isMoveTool.value || pointer.event.shiftKey) {
|
|
||||||
gameStore.setPlayerDraggingCamera(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function dragMap(pointer: Phaser.Input.Pointer) {
|
|
||||||
if (!gameStore.game.isPlayerDraggingCamera) return
|
|
||||||
|
|
||||||
const distance = Phaser.Math.Distance.Between(pointerStartPosition.value.x, pointerStartPosition.value.y, pointer.x, pointer.y)
|
|
||||||
|
|
||||||
// If the distance is less than the drag threshold, return
|
|
||||||
// We do this to prevent the camera from scrolling too quickly
|
|
||||||
|
|
||||||
if (distance <= dragThreshold) return
|
|
||||||
|
|
||||||
camera.setScroll(camera.scrollX - (pointer.x - pointer.prevPosition.x) / camera.zoom, camera.scrollY - (pointer.y - pointer.prevPosition.y) / camera.zoom)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePointerMove(pointer: Phaser.Input.Pointer) {
|
|
||||||
if (isMoveTool.value || pointer.event.shiftKey) {
|
|
||||||
dragMap(pointer)
|
|
||||||
}
|
|
||||||
updateWaypoint(pointer.worldX, pointer.worldY)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePointerUp(pointer: Phaser.Input.Pointer) {
|
|
||||||
gameStore.setPlayerDraggingCamera(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleZoom(pointer: Phaser.Input.Pointer) {
|
|
||||||
if (pointer.event instanceof WheelEvent && pointer.event.shiftKey) {
|
|
||||||
const deltaY = pointer.event.deltaY
|
|
||||||
const zoomLevel = camera.zoom - deltaY * 0.005
|
|
||||||
if (zoomLevel > 0 && zoomLevel < 3) {
|
|
||||||
camera.setZoom(zoomLevel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const setupPointerHandlers = () => {
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_WHEEL, handleZoom)
|
|
||||||
}
|
|
||||||
|
|
||||||
const cleanupPointerHandlers = () => {
|
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_DOWN, handlePointerDown)
|
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
|
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_WHEEL, handleZoom)
|
|
||||||
}
|
|
||||||
|
|
||||||
return { setupPointerHandlers, cleanupPointerHandlers }
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import { useMapStore } from '@/stores/mapStore'
|
|
||||||
|
|
||||||
export function useCameraControls(scene: Phaser.Scene) {
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const camera = scene.cameras.main
|
|
||||||
|
|
||||||
const onPointerDown = () => gameStore.setPlayerDraggingCamera(true)
|
|
||||||
const onPointerUp = () => gameStore.setPlayerDraggingCamera(false)
|
|
||||||
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_DOWN, onPointerDown)
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_UP, onPointerUp)
|
|
||||||
|
|
||||||
return { camera }
|
|
||||||
}
|
|
160
src/composables/useCharacterSpriteComposable.ts
Normal file
160
src/composables/useCharacterSpriteComposable.ts
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
import config from '@/application/config'
|
||||||
|
import { Direction } from '@/application/enums'
|
||||||
|
import { type MapCharacter } from '@/application/types'
|
||||||
|
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/services/mapService'
|
||||||
|
import { loadSpriteTextures } from '@/services/textureService'
|
||||||
|
import { CharacterTypeStorage } from '@/storage/storages'
|
||||||
|
import { refObj } from 'phavuer'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
export function useCharacterSpriteComposable(scene: Phaser.Scene, tilemap: Phaser.Tilemaps.Tilemap, mapCharacter: MapCharacter) {
|
||||||
|
const characterContainer = refObj<Phaser.GameObjects.Container>()
|
||||||
|
const characterSpriteId = ref('')
|
||||||
|
const characterSprite = refObj<Phaser.GameObjects.Sprite>()
|
||||||
|
const currentPositionX = ref(0)
|
||||||
|
const currentPositionY = ref(0)
|
||||||
|
const isometricDepth = ref(1)
|
||||||
|
const isInitialPosition = ref(true)
|
||||||
|
const tween = ref<Phaser.Tweens.Tween | null>(null)
|
||||||
|
|
||||||
|
const updateIsometricDepth = (positionX: number, positionY: number) => {
|
||||||
|
isometricDepth.value = calculateIsometricDepth(positionX, positionY, 30, 95, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePosition = (positionX: number, positionY: number) => {
|
||||||
|
const newPositionX = tileToWorldX(tilemap, positionX, positionY)
|
||||||
|
const newPositionY = tileToWorldY(tilemap, positionX, positionY)
|
||||||
|
|
||||||
|
if (isInitialPosition.value) {
|
||||||
|
currentPositionX.value = newPositionX
|
||||||
|
currentPositionY.value = newPositionY
|
||||||
|
isInitialPosition.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tween.value?.isPlaying()) {
|
||||||
|
tween.value.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
const tileDistance = Math.sqrt(Math.pow((newPositionX - currentPositionX.value) / config.tile_size.width, 2) + Math.pow((newPositionY - currentPositionY.value) / config.tile_size.height, 2))
|
||||||
|
|
||||||
|
const baseDuration = 300 // milliseconds per tile
|
||||||
|
const duration = Math.min(baseDuration * tileDistance, baseDuration)
|
||||||
|
|
||||||
|
tween.value = tilemap.scene.tweens.add({
|
||||||
|
targets: characterContainer.value,
|
||||||
|
x: newPositionX,
|
||||||
|
y: newPositionY,
|
||||||
|
duration,
|
||||||
|
ease: 'Linear',
|
||||||
|
onStart: () => {
|
||||||
|
updateIsometricDepth(positionX, positionY)
|
||||||
|
},
|
||||||
|
onUpdate: () => {
|
||||||
|
updateIsometricDepth(positionX, positionY)
|
||||||
|
currentPositionX.value = characterContainer.value?.x ?? currentPositionX.value
|
||||||
|
currentPositionY.value = characterContainer.value?.y ?? currentPositionY.value
|
||||||
|
},
|
||||||
|
onComplete: () => {
|
||||||
|
updateIsometricDepth(positionX, positionY)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const playAnimation = (animation: string, loop = false, ignoreIfPlaying = true) => {
|
||||||
|
if (!characterSprite.value || !characterSpriteId.value) return
|
||||||
|
|
||||||
|
const fullAnimationName = `${characterSpriteId.value}-${animation}_${currentDirection.value}`
|
||||||
|
|
||||||
|
// Remove any existing animation complete listeners
|
||||||
|
characterSprite.value.off(Phaser.Animations.Events.ANIMATION_COMPLETE)
|
||||||
|
|
||||||
|
// Add new listener
|
||||||
|
characterSprite.value.on(Phaser.Animations.Events.ANIMATION_COMPLETE, () => {
|
||||||
|
characterSprite.value!.setFrame(0)
|
||||||
|
characterSprite.value!.setTexture(charTexture.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
characterSprite.value.anims.play(
|
||||||
|
{
|
||||||
|
key: fullAnimationName,
|
||||||
|
repeat: loop ? -1 : 0
|
||||||
|
},
|
||||||
|
ignoreIfPlaying
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const calcDirection = (oldPositionX: number, oldPositionY: number, newPositionX: number, newPositionY: number): Direction => {
|
||||||
|
if (newPositionY < oldPositionY || newPositionX < oldPositionX) return Direction.NEGATIVE
|
||||||
|
if (newPositionX > oldPositionX || newPositionY > oldPositionY) return Direction.POSITIVE
|
||||||
|
return Direction.UNCHANGED
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFlippedX = computed(() => [6, 4].includes(mapCharacter.character.rotation ?? 0))
|
||||||
|
|
||||||
|
const currentDirection = computed(() => {
|
||||||
|
return [0, 6].includes(mapCharacter.character.rotation ?? 0) ? 'left_up' : 'right_down'
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentAction = computed(() => {
|
||||||
|
return mapCharacter.isMoving ? 'walk' : 'idle'
|
||||||
|
})
|
||||||
|
|
||||||
|
const charTexture = computed(() => {
|
||||||
|
const spriteId = characterSpriteId.value ?? 'idle_right_down'
|
||||||
|
return `${spriteId}-${currentAction.value}_${currentDirection.value}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateSprite = () => {
|
||||||
|
if (!characterSprite.value) return
|
||||||
|
|
||||||
|
if (mapCharacter.isMoving) {
|
||||||
|
characterSprite.value.anims.play(charTexture.value, true)
|
||||||
|
} else {
|
||||||
|
characterSprite.value.anims.stop()
|
||||||
|
characterSprite.value.setFrame(0)
|
||||||
|
characterSprite.value.setTexture(charTexture.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const initializeSprite = async () => {
|
||||||
|
const characterTypeStorage = new CharacterTypeStorage()
|
||||||
|
const spriteId = await characterTypeStorage.getSpriteId(mapCharacter.character.characterType!)
|
||||||
|
if (!spriteId) return
|
||||||
|
|
||||||
|
characterSpriteId.value = spriteId
|
||||||
|
await loadSpriteTextures(scene, spriteId)
|
||||||
|
|
||||||
|
if (characterContainer.value) {
|
||||||
|
characterContainer.value.setName(mapCharacter.character.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (characterSprite.value) {
|
||||||
|
characterSprite.value.setTexture(charTexture.value)
|
||||||
|
characterSprite.value.setFlipX(isFlippedX.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePosition(mapCharacter.character.positionX, mapCharacter.character.positionY)
|
||||||
|
updateIsometricDepth(mapCharacter.character.positionX, mapCharacter.character.positionY)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
tween.value?.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
characterContainer,
|
||||||
|
characterSpriteId,
|
||||||
|
characterSprite,
|
||||||
|
currentPositionX,
|
||||||
|
currentPositionY,
|
||||||
|
isometricDepth,
|
||||||
|
isFlippedX,
|
||||||
|
updatePosition,
|
||||||
|
playAnimation,
|
||||||
|
calcDirection,
|
||||||
|
updateSprite,
|
||||||
|
initializeSprite,
|
||||||
|
cleanup
|
||||||
|
}
|
||||||
|
}
|
18
src/composables/useControlsComposable.ts
Normal file
18
src/composables/useControlsComposable.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { useGameControlsComposable } from '@/composables/controls/useGameControlsComposable'
|
||||||
|
import { useMapEditorControlsComposable } from '@/composables/controls/useMapEditorControlsComposable'
|
||||||
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
|
import { computed, type Ref } from 'vue'
|
||||||
|
|
||||||
|
export function useControlsComposable(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>) {
|
||||||
|
const camera = scene.cameras.main
|
||||||
|
const gameHandlers = useGameControlsComposable(scene, layer, waypoint, camera)
|
||||||
|
const mapEditorHandlers = useMapEditorControlsComposable(scene, layer, waypoint, camera)
|
||||||
|
|
||||||
|
const mapEditor = useMapEditorComposable()
|
||||||
|
const currentHandlers = computed(() => (mapEditor.active.value ? mapEditorHandlers : gameHandlers))
|
||||||
|
|
||||||
|
const setupControls = () => currentHandlers.value.setupControls()
|
||||||
|
const cleanupControls = () => currentHandlers.value.cleanupControls()
|
||||||
|
|
||||||
|
return { setupControls, cleanupControls, camera }
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import type { Map, MapObject } from '@/application/types'
|
import type { Map, MapObject, PlacedMapObject, UUID } from '@/application/types'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
export type TeleportSettings = {
|
export type TeleportSettings = {
|
||||||
@ -12,16 +12,25 @@ const currentMap = ref<Map | null>(null)
|
|||||||
const active = ref(false)
|
const active = ref(false)
|
||||||
const tool = ref('move')
|
const tool = ref('move')
|
||||||
const drawMode = ref('tile')
|
const drawMode = ref('tile')
|
||||||
|
const inputMode = ref('tap')
|
||||||
const selectedTile = ref('')
|
const selectedTile = ref('')
|
||||||
const selectedMapObject = ref<MapObject | null>(null)
|
const selectedMapObject = ref<MapObject | null>(null)
|
||||||
|
const movingPlacedObject = ref<PlacedMapObject | null>(null)
|
||||||
|
const selectedPlacedObject = ref<PlacedMapObject | null>(null)
|
||||||
const shouldClearTiles = ref(false)
|
const shouldClearTiles = ref(false)
|
||||||
const teleportSettings = ref<TeleportSettings>({
|
const teleportSettings = ref<TeleportSettings>({
|
||||||
toMapId: '',
|
toMapId: '1000',
|
||||||
toPositionX: 0,
|
toPositionX: 0,
|
||||||
toPositionY: 0,
|
toPositionY: 0,
|
||||||
toRotation: 0
|
toRotation: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We can update origin X and Y in src/components/gameMaster/mapEditor/partials/SelectedPlacedMapObject.vue
|
||||||
|
* and this will trigger a refresh for spawned mao objects
|
||||||
|
*/
|
||||||
|
const refreshMapObject = ref(0)
|
||||||
|
|
||||||
export function useMapEditorComposable() {
|
export function useMapEditorComposable() {
|
||||||
const loadMap = (map: Map) => {
|
const loadMap = (map: Map) => {
|
||||||
currentMap.value = map
|
currentMap.value = map
|
||||||
@ -37,7 +46,6 @@ export function useMapEditorComposable() {
|
|||||||
if (!currentMap.value) return
|
if (!currentMap.value) return
|
||||||
currentMap.value.placedMapObjects = []
|
currentMap.value.placedMapObjects = []
|
||||||
currentMap.value.mapEventTiles = []
|
currentMap.value.mapEventTiles = []
|
||||||
currentMap.value.tiles = []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleActive = () => {
|
const toggleActive = () => {
|
||||||
@ -53,12 +61,16 @@ export function useMapEditorComposable() {
|
|||||||
drawMode.value = mode
|
drawMode.value = mode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setInputMode = (mode: string) => {
|
||||||
|
inputMode.value = mode
|
||||||
|
}
|
||||||
|
|
||||||
const setSelectedTile = (tile: string) => {
|
const setSelectedTile = (tile: string) => {
|
||||||
selectedTile.value = tile
|
selectedTile.value = tile
|
||||||
}
|
}
|
||||||
|
|
||||||
const setSelectedMapObject = (object: MapObject) => {
|
const setSelectedMapObject = (mapObject: MapObject) => {
|
||||||
selectedMapObject.value = object
|
selectedMapObject.value = mapObject
|
||||||
}
|
}
|
||||||
|
|
||||||
const setTeleportSettings = (settings: TeleportSettings) => {
|
const setTeleportSettings = (settings: TeleportSettings) => {
|
||||||
@ -73,12 +85,18 @@ export function useMapEditorComposable() {
|
|||||||
shouldClearTiles.value = false
|
shouldClearTiles.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function triggerMapObjectRefresh() {
|
||||||
|
refreshMapObject.value++ // Increment to trigger watchers
|
||||||
|
}
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
tool.value = 'move'
|
tool.value = 'move'
|
||||||
drawMode.value = 'tile'
|
drawMode.value = 'tile'
|
||||||
|
inputMode.value = 'tap'
|
||||||
selectedTile.value = ''
|
selectedTile.value = ''
|
||||||
selectedMapObject.value = null
|
selectedMapObject.value = null
|
||||||
shouldClearTiles.value = false
|
shouldClearTiles.value = false
|
||||||
|
refreshMapObject.value = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -87,10 +105,14 @@ export function useMapEditorComposable() {
|
|||||||
active,
|
active,
|
||||||
tool,
|
tool,
|
||||||
drawMode,
|
drawMode,
|
||||||
|
inputMode,
|
||||||
selectedTile,
|
selectedTile,
|
||||||
selectedMapObject,
|
selectedMapObject,
|
||||||
|
movingPlacedObject,
|
||||||
|
selectedPlacedObject,
|
||||||
shouldClearTiles,
|
shouldClearTiles,
|
||||||
teleportSettings,
|
teleportSettings,
|
||||||
|
refreshMapObject,
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
loadMap,
|
loadMap,
|
||||||
@ -99,11 +121,13 @@ export function useMapEditorComposable() {
|
|||||||
toggleActive,
|
toggleActive,
|
||||||
setTool,
|
setTool,
|
||||||
setDrawMode,
|
setDrawMode,
|
||||||
|
setInputMode,
|
||||||
setSelectedTile,
|
setSelectedTile,
|
||||||
setSelectedMapObject,
|
setSelectedMapObject,
|
||||||
setTeleportSettings,
|
setTeleportSettings,
|
||||||
triggerClearTiles,
|
triggerClearTiles,
|
||||||
resetClearTilesFlag,
|
resetClearTilesFlag,
|
||||||
|
triggerMapObjectRefresh,
|
||||||
reset
|
reset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
|
||||||
import { computed, watch, type Ref } from 'vue'
|
|
||||||
import { useGamePointerHandlers } from './pointerHandlers/useGamePointerHandlers'
|
|
||||||
import { useMapEditorPointerHandlers } from './pointerHandlers/useMapEditorPointerHandlers'
|
|
||||||
|
|
||||||
export function usePointerHandlers(scene: Phaser.Scene, layer: Phaser.Tilemaps.TilemapLayer, waypoint: Ref<{ visible: boolean; x: number; y: number }>, camera: Phaser.Cameras.Scene2D.Camera) {
|
|
||||||
const mapEditor = useMapEditorComposable()
|
|
||||||
const gameHandlers = useGamePointerHandlers(scene, layer, waypoint, camera)
|
|
||||||
const mapEditorHandlers = useMapEditorPointerHandlers(scene, layer, waypoint, camera)
|
|
||||||
|
|
||||||
const currentHandlers = computed(() => (mapEditor.active.value ? mapEditorHandlers : gameHandlers))
|
|
||||||
|
|
||||||
const setupPointerHandlers = () => currentHandlers.value.setupPointerHandlers()
|
|
||||||
const cleanupPointerHandlers = () => currentHandlers.value.cleanupPointerHandlers()
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => mapEditor.active.value,
|
|
||||||
() => {
|
|
||||||
cleanupPointerHandlers()
|
|
||||||
setupPointerHandlers()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return { setupPointerHandlers, cleanupPointerHandlers }
|
|
||||||
}
|
|
164
src/composables/useSoundComposable.ts
Normal file
164
src/composables/useSoundComposable.ts
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
import { SoundStorage } from '@/storage/storages'
|
||||||
|
|
||||||
|
interface CachedSound {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
base64: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Core storage instances
|
||||||
|
const soundStorage = new SoundStorage()
|
||||||
|
const activeSounds = new Map<string, HTMLAudioElement[]>()
|
||||||
|
const audioCache = new Map<string, HTMLAudioElement>()
|
||||||
|
|
||||||
|
export function useSoundComposable() {
|
||||||
|
/**
|
||||||
|
* Converts a sound URL to base64 format
|
||||||
|
*/
|
||||||
|
async function soundToBase64(url: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob()
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onloadend = () => resolve((reader.result as string).split(',')[1])
|
||||||
|
reader.onerror = reject
|
||||||
|
reader.readAsDataURL(blob)
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error converting sound to base64:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preloads a sound file and caches it for future use
|
||||||
|
*/
|
||||||
|
async function preloadSound(soundUrl: string): Promise<HTMLAudioElement> {
|
||||||
|
if (audioCache.has(soundUrl)) {
|
||||||
|
return audioCache.get(soundUrl)!
|
||||||
|
}
|
||||||
|
|
||||||
|
let audio: HTMLAudioElement
|
||||||
|
const cachedSound = await soundStorage.getById(soundUrl)
|
||||||
|
|
||||||
|
if (cachedSound) {
|
||||||
|
audio = new Audio(`data:audio/mpeg;base64,${cachedSound.base64}`)
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const base64 = await soundToBase64(soundUrl)
|
||||||
|
const soundData: CachedSound = {
|
||||||
|
id: soundUrl,
|
||||||
|
name: soundUrl.split('/').pop() || soundUrl,
|
||||||
|
base64
|
||||||
|
}
|
||||||
|
await soundStorage.add(soundData)
|
||||||
|
audio = new Audio(`data:audio/mpeg;base64,${base64}`)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to load and cache sound ${soundUrl}:`, error)
|
||||||
|
audio = new Audio(soundUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
audio.load()
|
||||||
|
audioCache.set(soundUrl, audio)
|
||||||
|
return audio
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plays a sound with optional looping and duplicate prevention
|
||||||
|
*/
|
||||||
|
async function playSound(soundUrl: string, loop: boolean = false, ignoreIfPlaying: boolean = false): Promise<void> {
|
||||||
|
try {
|
||||||
|
const playingSounds = activeSounds.get(soundUrl) || []
|
||||||
|
|
||||||
|
if (ignoreIfPlaying && playingSounds.some((audio) => !audio.paused)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stopSound(soundUrl)
|
||||||
|
|
||||||
|
const audio = await preloadSound(soundUrl)
|
||||||
|
const playingAudio = audio.cloneNode() as HTMLAudioElement
|
||||||
|
playingAudio.loop = loop
|
||||||
|
|
||||||
|
if (!activeSounds.has(soundUrl)) {
|
||||||
|
activeSounds.set(soundUrl, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
activeSounds.get(soundUrl)!.push(playingAudio)
|
||||||
|
|
||||||
|
// Cleanup when sound ends
|
||||||
|
playingAudio.addEventListener(
|
||||||
|
'ended',
|
||||||
|
() => {
|
||||||
|
const sounds = activeSounds.get(soundUrl)
|
||||||
|
if (!sounds) return
|
||||||
|
|
||||||
|
const index = sounds.indexOf(playingAudio)
|
||||||
|
if (index > -1) {
|
||||||
|
sounds.splice(index, 1)
|
||||||
|
}
|
||||||
|
if (sounds.length === 0) {
|
||||||
|
activeSounds.delete(soundUrl)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
await playingAudio.play()
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to play sound ${soundUrl}:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops all instances of a specific sound
|
||||||
|
*/
|
||||||
|
function stopSound(soundUrl: string): void {
|
||||||
|
const sounds = activeSounds.get(soundUrl)
|
||||||
|
if (!sounds) return
|
||||||
|
|
||||||
|
sounds.forEach((audio) => {
|
||||||
|
audio.pause()
|
||||||
|
audio.currentTime = 0
|
||||||
|
})
|
||||||
|
activeSounds.delete(soundUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops all currently playing sounds
|
||||||
|
*/
|
||||||
|
function stopAllSounds(): void {
|
||||||
|
activeSounds.forEach((_, url) => stopSound(url))
|
||||||
|
activeSounds.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all cached sounds and stops playback
|
||||||
|
*/
|
||||||
|
async function clearSoundCache(): Promise<void> {
|
||||||
|
stopAllSounds()
|
||||||
|
audioCache.clear()
|
||||||
|
await soundStorage.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preloads multiple sounds simultaneously
|
||||||
|
*/
|
||||||
|
async function preloadSounds(soundUrls: string[]): Promise<HTMLAudioElement[]> {
|
||||||
|
return Promise.all(soundUrls.map(preloadSound))
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
playSound,
|
||||||
|
stopSound,
|
||||||
|
stopAllSounds,
|
||||||
|
clearSoundCache,
|
||||||
|
preloadSounds
|
||||||
|
}
|
||||||
|
}
|
109
src/composables/useTileProcessingComposable.ts
Normal file
109
src/composables/useTileProcessingComposable.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import config from '@/application/config'
|
||||||
|
import type { Tile } from '@/application/types'
|
||||||
|
import type { TileAnalysisResult, TileWorkerMessage } from '@/types/tileTypes'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
// Constants for image processing
|
||||||
|
const DOWNSCALE_WIDTH = 16
|
||||||
|
const DOWNSCALE_HEIGHT = 8
|
||||||
|
const COLOR_SIMILARITY_THRESHOLD = 30
|
||||||
|
const EDGE_SIMILARITY_THRESHOLD = 20
|
||||||
|
const BATCH_SIZE = 8
|
||||||
|
|
||||||
|
export function useTileProcessingComposable() {
|
||||||
|
const tileAnalysisCache = ref<Map<string, { color: { r: number; g: number; b: number }; edge: number; namePrefix: string }>>(new Map())
|
||||||
|
const processingQueue = ref<Tile[]>([])
|
||||||
|
let isProcessing = false
|
||||||
|
|
||||||
|
const NUM_WORKERS = 4
|
||||||
|
const workers = Array.from({ length: NUM_WORKERS }, () => new Worker(new URL('@/workers/tileAnalyzerWorker.ts', import.meta.url), { type: 'module' }))
|
||||||
|
let currentWorker = 0
|
||||||
|
|
||||||
|
// Modify worker message handling
|
||||||
|
workers.forEach((worker) => {
|
||||||
|
worker.onmessage = (e: MessageEvent<TileAnalysisResult>) => {
|
||||||
|
const { tileId, color, edge, namePrefix } = e.data
|
||||||
|
tileAnalysisCache.value.set(tileId, { color, edge, namePrefix })
|
||||||
|
isProcessing = false
|
||||||
|
processBatch()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function processTileAsync(tile: Tile, worker: Worker): Promise<void> {
|
||||||
|
if (tileAnalysisCache.value.has(tile.id)) return
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const img = new Image()
|
||||||
|
img.crossOrigin = 'Anonymous'
|
||||||
|
img.onload = () => {
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
if (!ctx) {
|
||||||
|
resolve()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.width = DOWNSCALE_WIDTH
|
||||||
|
canvas.height = DOWNSCALE_HEIGHT
|
||||||
|
ctx.drawImage(img, 0, 0, DOWNSCALE_WIDTH, DOWNSCALE_HEIGHT)
|
||||||
|
|
||||||
|
const imageData = ctx.getImageData(0, 0, DOWNSCALE_WIDTH, DOWNSCALE_HEIGHT)
|
||||||
|
const message: TileWorkerMessage = {
|
||||||
|
imageData,
|
||||||
|
tileId: tile.id,
|
||||||
|
tileName: tile.name
|
||||||
|
}
|
||||||
|
worker.postMessage(message)
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
img.onerror = () => resolve()
|
||||||
|
img.src = `${config.server_endpoint}/textures/tiles/${tile.id}.png`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function processBatch() {
|
||||||
|
if (isProcessing || processingQueue.value.length === 0) return
|
||||||
|
isProcessing = true
|
||||||
|
|
||||||
|
const batch = processingQueue.value.splice(0, BATCH_SIZE)
|
||||||
|
Promise.all(
|
||||||
|
batch.map((tile) => {
|
||||||
|
currentWorker = (currentWorker + 1) % NUM_WORKERS
|
||||||
|
return processTileAsync(tile, workers[currentWorker])
|
||||||
|
})
|
||||||
|
).then(() => {
|
||||||
|
isProcessing = false
|
||||||
|
if (processingQueue.value.length > 0) {
|
||||||
|
setTimeout(processBatch, 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function processTile(tile: Tile) {
|
||||||
|
if (!processingQueue.value.includes(tile)) {
|
||||||
|
processingQueue.value.push(tile)
|
||||||
|
processBatch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function areTilesRelated(tile1: Tile, tile2: Tile): boolean {
|
||||||
|
const data1 = tileAnalysisCache.value.get(tile1.id)
|
||||||
|
const data2 = tileAnalysisCache.value.get(tile2.id)
|
||||||
|
|
||||||
|
if (!data1 || !data2) return false
|
||||||
|
|
||||||
|
const colorDifference = Math.sqrt(Math.pow(data1.color.r - data2.color.r, 2) + Math.pow(data1.color.g - data2.color.g, 2) + Math.pow(data1.color.b - data2.color.b, 2))
|
||||||
|
|
||||||
|
return colorDifference <= COLOR_SIMILARITY_THRESHOLD && Math.abs(data1.edge - data2.edge) <= EDGE_SIMILARITY_THRESHOLD && data1.namePrefix === data2.namePrefix
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
workers.forEach((worker) => worker.terminate())
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
processTile,
|
||||||
|
areTilesRelated,
|
||||||
|
cleanup
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,4 @@
|
|||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
import { getDomain } from '@/application/utilities'
|
|
||||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
@ -22,7 +21,7 @@ export async function login(username: string, password: string) {
|
|||||||
try {
|
try {
|
||||||
const response = await axios.post(`${config.server_endpoint}/login`, { username, password })
|
const response = await axios.post(`${config.server_endpoint}/login`, { username, password })
|
||||||
useCookies().set('token', response.data.data.token as string, {
|
useCookies().set('token', response.data.data.token as string, {
|
||||||
domain: getDomain()
|
domain: config.domain
|
||||||
})
|
})
|
||||||
return { success: true, token: response.data.data.token }
|
return { success: true, token: response.data.data.token }
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
@ -1,7 +1,7 @@
|
|||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
import type { HttpResponse, TextureData, Tile as TileT, UUID } from '@/application/types'
|
import type { MapObject, Map as MapT, TextureData, Tile as TileT, UUID } from '@/application/types'
|
||||||
import { unduplicateArray } from '@/application/utilities'
|
import { unduplicateArray } from '@/application/utilities'
|
||||||
import { loadTexture } from '@/composables/gameComposable'
|
import { loadTexture } from '@/services/textureService'
|
||||||
import { MapStorage, TileStorage } from '@/storage/storages'
|
import { MapStorage, TileStorage } from '@/storage/storages'
|
||||||
|
|
||||||
import Tilemap = Phaser.Tilemaps.Tilemap
|
import Tilemap = Phaser.Tilemaps.Tilemap
|
||||||
@ -39,11 +39,6 @@ export function tileToWorldY(layer: TilemapLayer | Tilemap, positionX: number, p
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Can also be used to replace tiles
|
* Can also be used to replace tiles
|
||||||
* @param map
|
|
||||||
* @param layer
|
|
||||||
* @param positionX
|
|
||||||
* @param positionY
|
|
||||||
* @param tileName
|
|
||||||
*/
|
*/
|
||||||
export function placeTile(map: Tilemap, layer: TilemapLayer, positionX: number, positionY: number, tileName: string) {
|
export function placeTile(map: Tilemap, layer: TilemapLayer, positionX: number, positionY: number, tileName: string) {
|
||||||
let tileImg = map.getTileset(tileName) as Tileset
|
let tileImg = map.getTileset(tileName) as Tileset
|
||||||
@ -53,8 +48,8 @@ export function placeTile(map: Tilemap, layer: TilemapLayer, positionX: number,
|
|||||||
layer.putTileAt(tileImg.firstgid, positionX, positionY)
|
layer.putTileAt(tileImg.firstgid, positionX, positionY)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setLayerTiles(map: Tilemap, layer: TilemapLayer, tiles: string[][]) {
|
export function placeTiles(map: Tilemap, layer: TilemapLayer, tiles: string[][]) {
|
||||||
if (!tiles) return
|
if (!map || !layer || !tiles) return
|
||||||
|
|
||||||
tiles.forEach((row: string[], y: number) => {
|
tiles.forEach((row: string[], y: number) => {
|
||||||
row.forEach((tile: string, x: number) => {
|
row.forEach((tile: string, x: number) => {
|
||||||
@ -70,15 +65,16 @@ export function createTileArray(width: number, height: number, tile: string = 'b
|
|||||||
export const calculateIsometricDepth = (positionX: number, positionY: number, width: number = 0, height: number = 0, isCharacter: boolean = false) => {
|
export const calculateIsometricDepth = (positionX: number, positionY: number, width: number = 0, height: number = 0, isCharacter: boolean = false) => {
|
||||||
const baseDepth = positionX + positionY
|
const baseDepth = positionX + positionY
|
||||||
if (isCharacter) {
|
if (isCharacter) {
|
||||||
return baseDepth // @TODO: Fix collision, this is a hack
|
return baseDepth
|
||||||
}
|
}
|
||||||
return baseDepth + (width + height) / (2 * config.tile_size.width)
|
return baseDepth + (width + height) / (2 * config.tile_size.width)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getTiles(tiles: TileT[], scene: Phaser.Scene) {
|
async function loadTileTextures(tiles: TileT[], scene: Phaser.Scene) {
|
||||||
// Load each tile into the scene
|
// Load each tile into the scene
|
||||||
for (const tile of tiles) {
|
for (let tile of tiles) {
|
||||||
if (!tile) continue
|
if (!tile?.id || !tile?.updatedAt) continue
|
||||||
|
|
||||||
const textureData = {
|
const textureData = {
|
||||||
key: tile.id,
|
key: tile.id,
|
||||||
data: '/textures/tiles/' + tile.id + '.png',
|
data: '/textures/tiles/' + tile.id + '.png',
|
||||||
@ -89,28 +85,69 @@ async function getTiles(tiles: TileT[], scene: Phaser.Scene) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadMapTilesIntoScene(map_id: UUID, scene: Phaser.Scene) {
|
export async function loadTileTexturesFromMapTileArray(map_id: string, scene: Phaser.Scene) {
|
||||||
const tileStorage = new TileStorage()
|
|
||||||
const mapStorage = new MapStorage()
|
const mapStorage = new MapStorage()
|
||||||
const map = await mapStorage.get(map_id)
|
const tileStorage = new TileStorage()
|
||||||
|
|
||||||
|
const map = await mapStorage.getById(map_id)
|
||||||
if (!map) return
|
if (!map) return
|
||||||
|
|
||||||
const tileArray = unduplicateArray(map.tiles)
|
const tileArray = unduplicateArray(map.tiles)
|
||||||
const tiles = await tileStorage.getByIds(tileArray)
|
const tiles = await tileStorage.getByIds(tileArray)
|
||||||
|
if (!tiles) return
|
||||||
|
|
||||||
await getTiles(tiles, scene)
|
await loadTileTextures(tiles, scene)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadTilesIntoScene(tileIds: string[], scene: Phaser.Scene) {
|
export async function loadAllTileTextures(scene: Phaser.Scene) {
|
||||||
const tileStorage = new TileStorage()
|
|
||||||
const tiles = await tileStorage.getByIds(tileIds)
|
|
||||||
|
|
||||||
await getTiles(tiles, scene)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadAllTilesIntoScene(scene: Phaser.Scene) {
|
|
||||||
const tileStorage = new TileStorage()
|
const tileStorage = new TileStorage()
|
||||||
const tiles = await tileStorage.getAll()
|
const tiles = await tileStorage.getAll()
|
||||||
|
if (!tiles) return
|
||||||
|
|
||||||
await getTiles(tiles, scene)
|
await loadTileTextures(tiles, scene)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadMapObjectTextures(mapObjects: MapObject[], scene: Phaser.Scene) {
|
||||||
|
for (const mapObject of mapObjects) {
|
||||||
|
const textureData = {
|
||||||
|
key: mapObject.id,
|
||||||
|
data: '/textures/map_objects/' + mapObject.id + '.png',
|
||||||
|
group: 'map_objects',
|
||||||
|
updatedAt: mapObject.updatedAt,
|
||||||
|
frameWidth: mapObject.frameWidth,
|
||||||
|
frameHeight: mapObject.frameHeight
|
||||||
|
} as TextureData
|
||||||
|
await loadTexture(scene, textureData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTileMap(scene: Phaser.Scene, map: MapT) {
|
||||||
|
const mapConfig = new Phaser.Tilemaps.MapData({
|
||||||
|
width: map.width,
|
||||||
|
height: map.height,
|
||||||
|
tileWidth: config.tile_size.width,
|
||||||
|
tileHeight: config.tile_size.height,
|
||||||
|
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC,
|
||||||
|
format: Phaser.Tilemaps.Formats.ARRAY_2D
|
||||||
|
})
|
||||||
|
|
||||||
|
return new Phaser.Tilemaps.Tilemap(scene, mapConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTileLayer(tileMap: Phaser.Tilemaps.Tilemap, tilesArray: string[]) {
|
||||||
|
// Load tiles into tileset
|
||||||
|
const tilesetImages = tilesArray.map((tile: string, index: number) => {
|
||||||
|
return tileMap.addTilesetImage(tile, tile, config.tile_size.width, config.tile_size.height, 1, 2, index + 1, { x: 0, y: -config.tile_size.height })
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add blank tile
|
||||||
|
tilesetImages.push(tileMap.addTilesetImage('blank_tile', 'blank_tile', config.tile_size.width, config.tile_size.height, 1, 2, 0, { x: 0, y: -config.tile_size.height }))
|
||||||
|
|
||||||
|
// Create layer
|
||||||
|
const layer = tileMap.createBlankLayer('tiles', tilesetImages as Tileset[], 0, config.tile_size.height) as Phaser.Tilemaps.TilemapLayer
|
||||||
|
|
||||||
|
layer.setDepth(0)
|
||||||
|
layer.setCullPadding(2, 2)
|
||||||
|
|
||||||
|
return layer
|
||||||
}
|
}
|
@ -60,7 +60,7 @@ export async function loadSpriteTextures(scene: Phaser.Scene, sprite_id: string)
|
|||||||
if (!sprite_id) return false
|
if (!sprite_id) return false
|
||||||
|
|
||||||
const spriteStorage = new SpriteStorage()
|
const spriteStorage = new SpriteStorage()
|
||||||
const sprite = await spriteStorage.get(sprite_id)
|
const sprite = await spriteStorage.getById(sprite_id)
|
||||||
|
|
||||||
if (!sprite) {
|
if (!sprite) {
|
||||||
console.error('Failed to load sprite:', sprite_id)
|
console.error('Failed to load sprite:', sprite_id)
|
@ -14,7 +14,7 @@ export class BaseStorage<T extends { id: string }> {
|
|||||||
|
|
||||||
async add(item: T, overwrite = false) {
|
async add(item: T, overwrite = false) {
|
||||||
try {
|
try {
|
||||||
const existing = await this.get(item.id)
|
const existing = await this.getById(item.id)
|
||||||
if (existing && !overwrite) return
|
if (existing && !overwrite) return
|
||||||
|
|
||||||
await this.dexie.table(this.tableName).put({ ...item })
|
await this.dexie.table(this.tableName).put({ ...item })
|
||||||
@ -39,7 +39,7 @@ export class BaseStorage<T extends { id: string }> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(id: string): Promise<T | null> {
|
async getById(id: string): Promise<T | null> {
|
||||||
try {
|
try {
|
||||||
const item = await this.dexie.table(this.tableName).get(id)
|
const item = await this.dexie.table(this.tableName).get(id)
|
||||||
return item || null
|
return item || null
|
||||||
|
@ -31,7 +31,7 @@ export class CharacterTypeStorage extends BaseStorage<any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getSpriteId(characterTypeId: string) {
|
async getSpriteId(characterTypeId: string) {
|
||||||
const characterType = await this.get(characterTypeId)
|
const characterType = await this.getById(characterTypeId)
|
||||||
return characterType?.sprite
|
return characterType?.sprite
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -42,7 +42,13 @@ export class CharacterHairStorage extends BaseStorage<any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getSpriteId(characterTypeId: string) {
|
async getSpriteId(characterTypeId: string) {
|
||||||
const characterType = await this.get(characterTypeId)
|
const characterType = await this.getById(characterTypeId)
|
||||||
return characterType?.sprite
|
return characterType?.sprite
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class SoundStorage extends BaseStorage<{ id: string; name: string; base64: string }> {
|
||||||
|
constructor() {
|
||||||
|
super('sounds', 'id, name, createdAt, updatedAt')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
import type { Character, Notification, TextureData, User, WorldSettings } from '@/application/types'
|
import type { Character, Notification, User, WorldSettings } from '@/application/types'
|
||||||
import { getDomain } from '@/application/utilities'
|
|
||||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { io, Socket } from 'socket.io-client'
|
import { io, Socket } from 'socket.io-client'
|
||||||
@ -74,7 +73,7 @@ export const useGameStore = defineStore('game', {
|
|||||||
},
|
},
|
||||||
initConnection() {
|
initConnection() {
|
||||||
this.connection = io(config.server_endpoint, {
|
this.connection = io(config.server_endpoint, {
|
||||||
secure: !config.development,
|
secure: config.environment === 'production',
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
transports: ['websocket'],
|
transports: ['websocket'],
|
||||||
reconnectionAttempts: 5
|
reconnectionAttempts: 5
|
||||||
@ -102,7 +101,7 @@ export const useGameStore = defineStore('game', {
|
|||||||
this.connection?.disconnect()
|
this.connection?.disconnect()
|
||||||
|
|
||||||
useCookies().remove('token', {
|
useCookies().remove('token', {
|
||||||
domain: getDomain()
|
domain: config.domain
|
||||||
})
|
})
|
||||||
|
|
||||||
this.connection = null
|
this.connection = null
|
||||||
|
@ -18,7 +18,7 @@ export const useMapStore = defineStore('map', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
setMapId(mapId: UUID) {
|
setMapId(mapId: string) {
|
||||||
this.mapId = mapId
|
this.mapId = mapId
|
||||||
},
|
},
|
||||||
setCharacters(characters: MapCharacter[]) {
|
setCharacters(characters: MapCharacter[]) {
|
||||||
@ -27,9 +27,11 @@ export const useMapStore = defineStore('map', {
|
|||||||
addCharacter(character: MapCharacter) {
|
addCharacter(character: MapCharacter) {
|
||||||
this.characters.push(character)
|
this.characters.push(character)
|
||||||
},
|
},
|
||||||
updateCharacter(updatedCharacter: MapCharacter) {
|
updateCharacterProperty<K extends keyof MapCharacter>(characterId: string, property: K, value: MapCharacter[K]) {
|
||||||
const index = this.characters.findIndex((char) => char.character.id === updatedCharacter.character.id)
|
const character = this.characters.find((char) => char.character.id === characterId)
|
||||||
if (index !== -1) this.characters[index] = updatedCharacter
|
if (character) {
|
||||||
|
character[property] = value
|
||||||
|
}
|
||||||
},
|
},
|
||||||
removeCharacter(characterId: UUID) {
|
removeCharacter(characterId: UUID) {
|
||||||
this.characters = this.characters.filter((char) => char.character.id !== characterId)
|
this.characters = this.characters.filter((char) => char.character.id !== characterId)
|
||||||
|
20
src/types/tileTypes.ts
Normal file
20
src/types/tileTypes.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
export interface TileAnalysisResult {
|
||||||
|
tileId: string
|
||||||
|
color: {
|
||||||
|
r: number
|
||||||
|
g: number
|
||||||
|
b: number
|
||||||
|
}
|
||||||
|
edge: number
|
||||||
|
namePrefix: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TileWorkerMessage {
|
||||||
|
imageData: ImageData
|
||||||
|
tileId: string
|
||||||
|
tileName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TileCache {
|
||||||
|
[key: string]: TileAnalysisResult
|
||||||
|
}
|
65
src/workers/tileAnalyzerWorker.ts
Normal file
65
src/workers/tileAnalyzerWorker.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import type { TileAnalysisResult } from '@/types/tileTypes'
|
||||||
|
|
||||||
|
const PIXEL_SAMPLE_RATE = 4
|
||||||
|
|
||||||
|
self.onmessage = async (e: MessageEvent) => {
|
||||||
|
const { imageData, tileId, tileName } = e.data
|
||||||
|
const result = analyzeTile(imageData, tileId, tileName)
|
||||||
|
self.postMessage(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
function analyzeTile(imageData: ImageData, tileId: string, tileName: string): TileAnalysisResult {
|
||||||
|
const { r, g, b } = getDominantColorFast(imageData)
|
||||||
|
const edge = getEdgeComplexityFast(imageData)
|
||||||
|
const namePrefix = tileName.split('_')[0]
|
||||||
|
return {
|
||||||
|
tileId,
|
||||||
|
color: { r, g, b },
|
||||||
|
edge,
|
||||||
|
namePrefix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDominantColorFast(imageData: ImageData) {
|
||||||
|
const data = new Uint8ClampedArray(imageData.data.buffer)
|
||||||
|
let r = 0,
|
||||||
|
g = 0,
|
||||||
|
b = 0,
|
||||||
|
total = 0
|
||||||
|
const length = data.length
|
||||||
|
|
||||||
|
for (let i = 0; i < length; i += 4 * PIXEL_SAMPLE_RATE) {
|
||||||
|
if (data[i + 3] > 0) {
|
||||||
|
r += data[i]
|
||||||
|
g += data[i + 1]
|
||||||
|
b += data[i + 2]
|
||||||
|
total++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return total > 0
|
||||||
|
? {
|
||||||
|
r: Math.round(r / total),
|
||||||
|
g: Math.round(g / total),
|
||||||
|
b: Math.round(b / total)
|
||||||
|
}
|
||||||
|
: { r: 0, g: 0, b: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEdgeComplexityFast(imageData: ImageData) {
|
||||||
|
const data = new Uint8ClampedArray(imageData.data.buffer)
|
||||||
|
const width = imageData.width
|
||||||
|
const height = imageData.height
|
||||||
|
let edgePixels = 0
|
||||||
|
|
||||||
|
// Only check every other row/column
|
||||||
|
for (let y = 0; y < height; y += PIXEL_SAMPLE_RATE * 2) {
|
||||||
|
for (let x = 0; x < width; x += PIXEL_SAMPLE_RATE * 2) {
|
||||||
|
const i = (y * width + x) * 4
|
||||||
|
if (data[i + 3] > 0 && (x === 0 || y === 0 || x >= width - PIXEL_SAMPLE_RATE || y >= height - PIXEL_SAMPLE_RATE || data[i - 4 * PIXEL_SAMPLE_RATE + 3] === 0)) {
|
||||||
|
edgePixels++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return edgePixels * PIXEL_SAMPLE_RATE * 2
|
||||||
|
}
|
Reference in New Issue
Block a user