Compare commits
211 Commits
feature/#3
...
main
Author | SHA1 | Date | |
---|---|---|---|
af5e2449b5 | |||
5392093d71 | |||
27c775821a | |||
0d5acd48ce | |||
fc34a488d9 | |||
0b1e95f80f | |||
9f176aae45 | |||
4903a83c71 | |||
ba3ed8c099 | |||
2881d5f251 | |||
32ca61cc50 | |||
6897ad0f1e | |||
db1766026e | |||
3f28d85c30 | |||
a85ad94f15 | |||
e6c684e066 | |||
142d991265 | |||
9e5dcc31fa | |||
b5c5837105 | |||
57b503142e | |||
208b58d05f | |||
87c04b6de5 | |||
84f8db5e10 | |||
84b34c4f85 | |||
2495e14ece | |||
febb924f75 | |||
dc2afba82b | |||
ed0a02a795 | |||
3670eb8736 | |||
d85bf4846b | |||
51e885cfdf | |||
ffc7efb17c | |||
a0da0266d3 | |||
2281c2c5e0 | |||
0e3a0e3dba | |||
ed992e1c2d | |||
65b011982a | |||
489c6c3ba0 | |||
db650449ac | |||
2d7d598c94 | |||
7097eb1580 | |||
d51fbc8030 | |||
b5b6d0adcc | |||
4b7b6e4885 | |||
4042808d4e | |||
a6d6d894a9 | |||
0c61fe77de | |||
bfb2bcb939 | |||
af5a97f66d | |||
79fa54b1bb | |||
dbb4cae154 | |||
9a8220e4e0 | |||
bc0db8b32b | |||
ad611ef593 | |||
d819a84a37 | |||
15dc331a43 | |||
920baaebde | |||
b569888682 | |||
94eab073e6 | |||
d843b954ab | |||
337446497b | |||
d8805dd775 | |||
4c040c21d6 | |||
d0af83ec60 | |||
2de34d2034 | |||
132121c082 | |||
201f628bfa | |||
af99d66595 | |||
56f30093f6 | |||
8f26a40a0e | |||
110fd4e608 | |||
c1edf31ca0 | |||
90c0ed3141 | |||
bcf0d2832d | |||
8bf67ab168 | |||
f83e2bf8c8 | |||
8b0bf6534e | |||
5e243e5201 | |||
c82db9813e | |||
579749f4e0 | |||
ddc26a021b | |||
2d6b1ff1e0 | |||
16720777c9 | |||
41e7832cbe | |||
e6412d8a65 | |||
faa8e5def9 | |||
beed1d6903 | |||
2ebcc24390 | |||
2e3ff803f6 | |||
dd1cc795de | |||
59243e0e17 | |||
87ffc98cce | |||
0c450b24ed | |||
9459639497 | |||
5f2c7a09b1 | |||
13e8c1b4dd | |||
b27a2e8779 | |||
b3c9e3ca3d | |||
31a91c3f9f | |||
5d4de60f90 | |||
4070bcf048 | |||
04203cb9c1 | |||
592d1df9bf | |||
9413fdbb2f | |||
34caac562c | |||
52dafb8643 | |||
390187f353 | |||
cbd111a05b | |||
5ef11f3157 | |||
c56c2796c4 | |||
c228af7bb6 | |||
f45a51c230 | |||
790a62c600 | |||
82a854e647 | |||
3bcb16fa9c | |||
f79ebedc62 | |||
44b0368276 | |||
b8b985470f | |||
39e00c6feb | |||
6de0bb200d | |||
2a00e206eb | |||
8f9b19ba8b | |||
d997a33b86 | |||
9749b02ccf | |||
f83d5eabee | |||
a9cedba4e0 | |||
49dcd92a9e | |||
d010159989 | |||
275dd95c69 | |||
e3c3d4d420 | |||
87e7f14469 | |||
723aa59142 | |||
c369719564 | |||
2d8c421ac6 | |||
1137c95ff3 | |||
4b56da0fa0 | |||
c21e78c2ec | |||
fcf96a25ae | |||
cf9deebc94 | |||
ca307d4de3 | |||
4c4e8ffe02 | |||
369522fda3 | |||
dc7e20842a | |||
75c9d5f349 | |||
b35794d6d3 | |||
6ba4c1b843 | |||
6a52546a08 | |||
8133bd02df | |||
e720a1098e | |||
48d1d920be | |||
7542fd70ed | |||
9f866fea72 | |||
ec6f3031b8 | |||
838610d041 | |||
fb3a59aa59 | |||
ccb64fc048 | |||
db52bcfff3 | |||
12735756d7 | |||
6383320e8c | |||
557b8aaabb | |||
c09e9ea841 | |||
c2d41a63a7 | |||
122a178feb | |||
909dbf4280 | |||
8add054f63 | |||
04d55f994e | |||
b83c340385 | |||
d5984f1c3f | |||
7071d934b4 | |||
15b212160d | |||
2a2841cf16 | |||
a545018639 | |||
90f3056e08 | |||
7730fd81bd | |||
b195f1399f | |||
3c06f7db97 | |||
6c7864b4d4 | |||
0c9a41c286 | |||
dffdd0542f | |||
d2abf8fda8 | |||
fdbc101f96 | |||
7ff1de4018 | |||
f258c65403 | |||
bab13646ed | |||
adc3eba237 | |||
2097a51f07 | |||
50daf01a01 | |||
14474f7665 | |||
f14d9baaa1 | |||
d2b6d8dcb3 | |||
027fdd7dac | |||
2b40741ca7 | |||
aee18956f3 | |||
cf54ab842a | |||
d25100c810 | |||
cd1daf9345 | |||
0ecd951710 | |||
ff9dcb91b0 | |||
841ec0f3df | |||
90d7252784 | |||
554497ecbc | |||
efeae337ab | |||
ad47b37279 | |||
5e11b67774 | |||
7daefb74eb | |||
4adcf8d61d | |||
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
@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
# Global options
|
||||||
|
admin off # Disable admin API
|
||||||
|
|
||||||
|
# Global logging configuration
|
||||||
|
log {
|
||||||
|
output file /var/log/caddy/access.log
|
||||||
|
format json
|
||||||
|
level INFO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
noxious.gg {
|
||||||
|
# Root directory for your Vue app
|
||||||
|
root * ./dist
|
||||||
|
|
||||||
|
# Enable compression with optimal settings
|
||||||
|
encode zstd gzip
|
||||||
|
|
||||||
|
# Handle SPA routing
|
||||||
|
try_files {path} /index.html
|
||||||
|
|
||||||
|
# Serve static files with optimizations
|
||||||
|
file_server
|
||||||
|
|
||||||
|
# Enhanced security headers
|
||||||
|
header {
|
||||||
|
# Existing headers with improvements
|
||||||
|
X-Frame-Options "SAMEORIGIN"
|
||||||
|
X-XSS-Protection "1; mode=block"
|
||||||
|
X-Content-Type-Options "nosniff"
|
||||||
|
Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
|
||||||
|
# Additional security headers
|
||||||
|
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||||
|
Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"
|
||||||
|
|
||||||
|
# Remove server information
|
||||||
|
-Server
|
||||||
|
}
|
||||||
|
|
||||||
|
# Improved cache configuration for static assets
|
||||||
|
@static {
|
||||||
|
file
|
||||||
|
path *.js *.css *.png *.jpg *.jpeg *.gif *.ico *.svg *.woff *.woff2 *.ttf *.eot
|
||||||
|
}
|
||||||
|
header @static {
|
||||||
|
Cache-Control "public, max-age=31536000, immutable"
|
||||||
|
Vary Accept-Encoding
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cache control for HTML files
|
||||||
|
@html {
|
||||||
|
file
|
||||||
|
path *.html
|
||||||
|
}
|
||||||
|
header @html {
|
||||||
|
Cache-Control "no-cache, must-revalidate"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Handle errors
|
||||||
|
handle_errors {
|
||||||
|
respond "{http.error.status_code} {http.error.status_text}" {http.error.status_code}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Improved redirect configuration
|
||||||
|
www.noxious.gg {
|
||||||
|
redir https://noxious.gg{uri} permanent
|
||||||
|
}
|
32
Dockerfile
@ -1,32 +0,0 @@
|
|||||||
# Build stage
|
|
||||||
FROM node:22.4.1-alpine as builder
|
|
||||||
WORKDIR /usr/src/app
|
|
||||||
COPY package*.json ./
|
|
||||||
RUN npm ci
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Set environment variables
|
|
||||||
ARG VITE_NAME=${VITE_NAME}
|
|
||||||
ENV VITE_NAME=${VITE_NAME}
|
|
||||||
|
|
||||||
ARG VITE_DEVELOPMENT=${VITE_DEVELOPMENT}
|
|
||||||
ENV VITE_DEVELOPMENT=${VITE_DEVELOPMENT}
|
|
||||||
|
|
||||||
ARG VITE_SERVER_ENDPOINT=${VITE_SERVER_ENDPOINT}
|
|
||||||
ENV VITE_SERVER_ENDPOINT=${VITE_SERVER_ENDPOINT}
|
|
||||||
|
|
||||||
ARG VITE_TILE_SIZE_X=${VITE_TILE_SIZE_X}
|
|
||||||
ENV VITE_TILE_SIZE_X=${VITE_TILE_SIZE_X}
|
|
||||||
|
|
||||||
ARG VITE_TILE_SIZE_Y=${VITE_TILE_SIZE_Y}
|
|
||||||
ENV VITE_TILE_SIZE_Y=${VITE_TILE_SIZE_Y}
|
|
||||||
|
|
||||||
# Build the application
|
|
||||||
RUN npm run build-ntc
|
|
||||||
|
|
||||||
# Production stage
|
|
||||||
FROM nginx:1.26.1-alpine
|
|
||||||
COPY --from=builder /usr/src/app/dist /usr/share/nginx/html
|
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
|
||||||
EXPOSE 80
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
|
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"schemaVersion": 2,
|
|
||||||
"dockerfilePath" :"./Dockerfile"
|
|
||||||
}
|
|
16
nginx.conf
@ -1,16 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name localhost;
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
# Redirect example
|
|
||||||
location /discord {
|
|
||||||
return 301 https://discord.gg/JTev3nzeDa;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Serve static files
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
}
|
|
||||||
}
|
|
1578
package-lock.json
generated
21
package.json
@ -16,17 +16,22 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vueuse/core": "^10.5.0",
|
"@vueuse/core": "^10.5.0",
|
||||||
"@vueuse/integrations": "^10.5.0",
|
"@vueuse/integrations": "^10.5.0",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.9",
|
||||||
"dexie": "^4.0.8",
|
"dexie": "^4.0.11",
|
||||||
"phaser": "^3.86.0",
|
"phaser": "^3.88.2",
|
||||||
"pinia": "^2.1.6",
|
"phaser3-rex-plugins": "^1.80.13",
|
||||||
"socket.io-client": "^4.8.0",
|
"phavuer": "^0.16.5",
|
||||||
|
"pinia": "^2.3.1",
|
||||||
|
"sharp": "^0.33.5",
|
||||||
|
"socket.io-client": "^4.8.1",
|
||||||
"universal-cookie": "^6.1.3",
|
"universal-cookie": "^6.1.3",
|
||||||
"vue": "^3.5.12",
|
"vite-plugin-image-optimizer": "^1.1.8",
|
||||||
"zod": "^3.22.2"
|
"vue": "^3.5.13",
|
||||||
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ianvs/prettier-plugin-sort-imports": "^4.4.0",
|
"@ianvs/prettier-plugin-sort-imports": "^4.4.0",
|
||||||
|
"@tauri-apps/cli": "^2.2.7",
|
||||||
"@tsconfig/node20": "^20.1.4",
|
"@tsconfig/node20": "^20.1.4",
|
||||||
"@types/jsdom": "^21.1.7",
|
"@types/jsdom": "^21.1.7",
|
||||||
"@types/node": "^20.14.11",
|
"@types/node": "^20.14.11",
|
||||||
@ -36,8 +41,6 @@
|
|||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"jsdom": "^24.1.1",
|
"jsdom": "^24.1.1",
|
||||||
"npm-run-all2": "^6.2.3",
|
"npm-run-all2": "^6.2.3",
|
||||||
"phaser3-rex-plugins": "^1.80.8",
|
|
||||||
"phavuer": "^0.16.1",
|
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"sass": "^1.79.4",
|
"sass": "^1.79.4",
|
||||||
|
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
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M16.3333 17.5L11.0833 12.25C10.6667 12.5833 10.1875 12.8472 9.64583 13.0417C9.10417 13.2361 8.52778 13.3333 7.91667 13.3333C6.40278 13.3333 5.12153 12.809 4.07292 11.7604C3.02431 10.7118 2.5 9.43056 2.5 7.91667C2.5 6.40278 3.02431 5.12153 4.07292 4.07292C5.12153 3.02431 6.40278 2.5 7.91667 2.5C9.43056 2.5 10.7118 3.02431 11.7604 4.07292C12.809 5.12153 13.3333 6.40278 13.3333 7.91667C13.3333 8.52778 13.2361 9.10417 13.0417 9.64583C12.8472 10.1875 12.5833 10.6667 12.25 11.0833L17.5 16.3333L16.3333 17.5ZM7.91667 11.6667C8.95833 11.6667 9.84375 11.3021 10.5729 10.5729C11.3021 9.84375 11.6667 8.95833 11.6667 7.91667C11.6667 6.875 11.3021 5.98958 10.5729 5.26042C9.84375 4.53125 8.95833 4.16667 7.91667 4.16667C6.875 4.16667 5.98958 4.53125 5.26042 5.26042C4.53125 5.98958 4.16667 6.875 4.16667 7.91667C4.16667 8.95833 4.53125 9.84375 5.26042 10.5729C5.98958 11.3021 6.875 11.6667 7.91667 11.6667Z" fill="#808080"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
59
public/assets/icons/mapEditor/settings.svg
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M490.667,405.333h-56.811C424.619,374.592,396.373,352,362.667,352s-61.931,22.592-71.189,53.333H21.333
|
||||||
|
C9.557,405.333,0,414.891,0,426.667S9.557,448,21.333,448h270.144c9.237,30.741,37.483,53.333,71.189,53.333
|
||||||
|
s61.931-22.592,71.189-53.333h56.811c11.797,0,21.333-9.557,21.333-21.333S502.464,405.333,490.667,405.333z M362.667,458.667
|
||||||
|
c-17.643,0-32-14.357-32-32s14.357-32,32-32s32,14.357,32,32S380.309,458.667,362.667,458.667z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M490.667,64h-56.811c-9.259-30.741-37.483-53.333-71.189-53.333S300.736,33.259,291.477,64H21.333
|
||||||
|
C9.557,64,0,73.557,0,85.333s9.557,21.333,21.333,21.333h270.144C300.736,137.408,328.96,160,362.667,160
|
||||||
|
s61.931-22.592,71.189-53.333h56.811c11.797,0,21.333-9.557,21.333-21.333S502.464,64,490.667,64z M362.667,117.333
|
||||||
|
c-17.643,0-32-14.357-32-32c0-17.643,14.357-32,32-32s32,14.357,32,32C394.667,102.976,380.309,117.333,362.667,117.333z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M490.667,234.667H220.523c-9.259-30.741-37.483-53.333-71.189-53.333s-61.931,22.592-71.189,53.333H21.333
|
||||||
|
C9.557,234.667,0,244.224,0,256c0,11.776,9.557,21.333,21.333,21.333h56.811c9.259,30.741,37.483,53.333,71.189,53.333
|
||||||
|
s61.931-22.592,71.189-53.333h270.144c11.797,0,21.333-9.557,21.333-21.333C512,244.224,502.464,234.667,490.667,234.667z
|
||||||
|
M149.333,288c-17.643,0-32-14.357-32-32s14.357-32,32-32c17.643,0,32,14.357,32,32S166.976,288,149.333,288z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 946 KiB |
BIN
public/assets/music/intro.mp3
Normal file
BIN
public/assets/sounds/attack.wav
Normal file
BIN
public/assets/sounds/button-click.wav
Normal file
BIN
public/assets/sounds/connect.wav
Normal file
BIN
public/assets/sounds/walk.wav
Normal file
4
src-tauri/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
/target/
|
||||||
|
/gen/schemas
|
5149
src-tauri/Cargo.lock
generated
Normal file
25
src-tauri/Cargo.toml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
[package]
|
||||||
|
name = "app"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "A Tauri App"
|
||||||
|
authors = ["you"]
|
||||||
|
license = ""
|
||||||
|
repository = ""
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.77.2"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "app_lib"
|
||||||
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "2.0.4", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde_json = "1.0"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
log = "0.4"
|
||||||
|
tauri = { version = "2.2.4", features = [] }
|
||||||
|
tauri-plugin-log = "2.0.0-rc"
|
3
src-tauri/build.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
11
src-tauri/capabilities/default.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "enables the default permissions",
|
||||||
|
"windows": [
|
||||||
|
"main"
|
||||||
|
],
|
||||||
|
"permissions": [
|
||||||
|
"core:default"
|
||||||
|
]
|
||||||
|
}
|
BIN
src-tauri/icons/128x128.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
After Width: | Height: | Size: 9.0 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
After Width: | Height: | Size: 7.4 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
src-tauri/icons/icon.png
Normal file
After Width: | Height: | Size: 49 KiB |
16
src-tauri/src/lib.rs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
pub fn run() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.setup(|app| {
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
app.handle().plugin(
|
||||||
|
tauri_plugin_log::Builder::default()
|
||||||
|
.level(log::LevelFilter::Info)
|
||||||
|
.build(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
|
}
|
6
src-tauri/src/main.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
app_lib::run();
|
||||||
|
}
|
37
src-tauri/tauri.conf.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||||
|
"productName": "noxious",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"identifier": "com.noxious.app",
|
||||||
|
"build": {
|
||||||
|
"frontendDist": "../dist",
|
||||||
|
"devUrl": "http://localhost:5173",
|
||||||
|
"beforeDevCommand": "npm run dev",
|
||||||
|
"beforeBuildCommand": "npm run build-only"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"title": "Noxious",
|
||||||
|
"width": 800,
|
||||||
|
"height": 600,
|
||||||
|
"resizable": true,
|
||||||
|
"fullscreen": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": "all",
|
||||||
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
25
src/App.vue
@ -1,8 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<Debug />
|
<Debug />
|
||||||
<Notifications />
|
<Notifications />
|
||||||
<BackgroundImageLoader />
|
<GmPanel v-if="gameStore.character?.role === 'gm'" />
|
||||||
<GmPanel v-if="gameStore.character?.role === 'gm'" @open-map-editor="mapEditor.toggleActive" />
|
|
||||||
<component :is="currentScreen" />
|
<component :is="currentScreen" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -13,21 +12,23 @@ import Game from '@/components/screens/Game.vue'
|
|||||||
import Loading from '@/components/screens/Loading.vue'
|
import Loading from '@/components/screens/Loading.vue'
|
||||||
import Login from '@/components/screens/Login.vue'
|
import Login from '@/components/screens/Login.vue'
|
||||||
import MapEditor from '@/components/screens/MapEditor.vue'
|
import MapEditor from '@/components/screens/MapEditor.vue'
|
||||||
import BackgroundImageLoader from '@/components/utilities/BackgroundImageLoader.vue'
|
|
||||||
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 { socketManager } from '@/managers/SocketManager'
|
||||||
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
|
||||||
if (!gameStore.connection) return Login
|
if (!socketManager.connection) return Login
|
||||||
if (!gameStore.token) return Login
|
if (!socketManager.token) return Login
|
||||||
if (!gameStore.character) return Characters
|
if (!gameStore.character) return Characters
|
||||||
if (mapEditor.active.value) return MapEditor
|
if (mapEditor.active.value) return MapEditor
|
||||||
return Game
|
return Game
|
||||||
@ -42,13 +43,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),
|
||||||
|
@ -3,3 +3,61 @@ export enum Direction {
|
|||||||
NEGATIVE,
|
NEGATIVE,
|
||||||
UNCHANGED
|
UNCHANGED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum SocketEvent {
|
||||||
|
CONNECT_ERROR = 'connect_error',
|
||||||
|
RECONNECT_FAILED = 'reconnect_failed',
|
||||||
|
CLOSE = '52',
|
||||||
|
DATA = '51',
|
||||||
|
CHARACTER_CONNECT = '50',
|
||||||
|
CHARACTER_CREATE = '49',
|
||||||
|
CHARACTER_DELETE = '48',
|
||||||
|
CHARACTER_LIST = '47',
|
||||||
|
GM_CHARACTERHAIR_CREATE = '46',
|
||||||
|
GM_CHARACTERHAIR_REMOVE = '45',
|
||||||
|
GM_CHARACTERHAIR_LIST = '44',
|
||||||
|
GM_CHARACTERHAIR_UPDATE = '43',
|
||||||
|
GM_CHARACTERTYPE_CREATE = '42',
|
||||||
|
GM_CHARACTERTYPE_REMOVE = '41',
|
||||||
|
GM_CHARACTERTYPE_LIST = '40',
|
||||||
|
GM_CHARACTERTYPE_UPDATE = '39',
|
||||||
|
GM_ITEM_CREATE = '38',
|
||||||
|
GM_ITEM_REMOVE = '37',
|
||||||
|
GM_ITEM_LIST = '36',
|
||||||
|
GM_ITEM_UPDATE = '35',
|
||||||
|
GM_MAPOBJECT_LIST = '34',
|
||||||
|
GM_MAPOBJECT_REMOVE = '33',
|
||||||
|
GM_MAPOBJECT_UPDATE = '32',
|
||||||
|
GM_MAPOBJECT_UPLOAD = '31',
|
||||||
|
GM_SPRITE_COPY = '30',
|
||||||
|
GM_SPRITE_CREATE = '29',
|
||||||
|
GM_SPRITE_DELETE = '28',
|
||||||
|
GM_SPRITE_LIST = '27',
|
||||||
|
GM_SPRITE_UPDATE = '26',
|
||||||
|
GM_TILE_DELETE = '25',
|
||||||
|
GM_TILE_LIST = '24',
|
||||||
|
GM_TILE_UPDATE = '23',
|
||||||
|
GM_TILE_UPLOAD = '22',
|
||||||
|
GM_MAP_CREATE = '21',
|
||||||
|
GM_MAP_DELETE = '20',
|
||||||
|
GM_MAP_REQUEST = '19',
|
||||||
|
GM_MAP_UPDATE = '18',
|
||||||
|
MAP_CHARACTER_MOVEERROR = '17',
|
||||||
|
DISCONNECT = 'disconnect',
|
||||||
|
USER_DISCONNECT = '15',
|
||||||
|
LOGIN = '14',
|
||||||
|
LOGGED_IN = '13',
|
||||||
|
NOTIFICATION = '12',
|
||||||
|
DATE = '11',
|
||||||
|
FAILED = '10',
|
||||||
|
COMPLETED = '9',
|
||||||
|
CONNECTION = 'connection',
|
||||||
|
WEATHER = '7',
|
||||||
|
CHARACTER_DISCONNECT = '6',
|
||||||
|
MAP_CHARACTER_ATTACK = '5',
|
||||||
|
MAP_CHARACTER_TELEPORT = '4',
|
||||||
|
MAP_CHARACTER_JOIN = '3',
|
||||||
|
MAP_CHARACTER_LEAVE = '2',
|
||||||
|
MAP_CHARACTER_MOVE = '1',
|
||||||
|
CHAT_MESSAGE = '0'
|
||||||
|
}
|
||||||
|
@ -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,9 +34,10 @@ export type Tile = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type MapObject = {
|
export type MapObject = {
|
||||||
id: UUID
|
id: string
|
||||||
name: string
|
name: string
|
||||||
tags: any | null
|
tags: string[]
|
||||||
|
depthOffsets: number[]
|
||||||
originX: number
|
originX: number
|
||||||
originY: number
|
originY: number
|
||||||
frameRate: number
|
frameRate: number
|
||||||
@ -47,7 +48,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 +63,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 +79,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 +100,8 @@ export enum MapEventTileType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type MapEventTile = {
|
export type MapEventTile = {
|
||||||
id: UUID
|
id: string
|
||||||
mapId: UUID
|
map: string
|
||||||
type: MapEventTileType
|
type: MapEventTileType
|
||||||
positionX: number
|
positionX: number
|
||||||
positionY: number
|
positionY: number
|
||||||
@ -111,7 +109,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 +118,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 +138,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,16 +149,17 @@ export type CharacterType = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type CharacterHair = {
|
export type CharacterHair = {
|
||||||
id: UUID
|
id: string
|
||||||
name: string
|
name: string
|
||||||
sprite?: Sprite
|
sprite: string | Sprite
|
||||||
gender: CharacterGender
|
gender: CharacterGender
|
||||||
|
color: string
|
||||||
isSelectable: boolean
|
isSelectable: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Character = {
|
export type Character = {
|
||||||
id: UUID
|
id: string
|
||||||
userId: UUID
|
userid: string
|
||||||
user: User
|
user: User
|
||||||
name: string
|
name: string
|
||||||
hitpoints: number
|
hitpoints: number
|
||||||
@ -187,14 +186,14 @@ export type MapCharacter = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
@ -209,8 +208,10 @@ export enum CharacterEquipmentSlotType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Sprite = {
|
export type Sprite = {
|
||||||
id: UUID
|
id: string
|
||||||
name: string
|
name: string
|
||||||
|
width: number | null
|
||||||
|
height: number | null
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
spriteActions: SpriteAction[]
|
spriteActions: SpriteAction[]
|
||||||
@ -256,6 +257,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>) {
|
||||||
@ -38,10 +21,20 @@ export async function downloadCache<T extends { id: string; updatedAt: Date }>(e
|
|||||||
}
|
}
|
||||||
|
|
||||||
const items = response.data ?? []
|
const items = response.data ?? []
|
||||||
|
const serverItemIds = new Set(items.map((item) => item.id))
|
||||||
|
|
||||||
|
// Remove items that don't exist on server
|
||||||
|
const existingItems = await storage.getAll()
|
||||||
|
for (const existingItem of existingItems) {
|
||||||
|
if (!serverItemIds.has(existingItem.id)) {
|
||||||
|
await storage.delete(existingItem.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update or add new items
|
||||||
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 {
|
||||||
@ -118,7 +124,16 @@ button {
|
|||||||
|
|
||||||
&.active,
|
&.active,
|
||||||
&:hover {
|
&:hover {
|
||||||
@apply bg-red-400;
|
@apply bg-red-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-indigo {
|
||||||
|
@apply bg-indigo-500 text-gray-50 text-base leading-5 rounded py-2.5;
|
||||||
|
|
||||||
|
&.active,
|
||||||
|
&:hover {
|
||||||
|
@apply bg-indigo-600;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,6 +164,10 @@ button {
|
|||||||
@apply bg-gray bg-none;
|
@apply bg-gray bg-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-open {
|
||||||
|
@apply w-[calc(75%_-_40px)] max-xl:w-[calc(100%_-_360px)];
|
||||||
|
}
|
||||||
|
|
||||||
.hair-deselect:has(:checked) {
|
.hair-deselect:has(:checked) {
|
||||||
img {
|
img {
|
||||||
@apply brightness-200;
|
@apply brightness-200;
|
||||||
|
@ -2,25 +2,25 @@
|
|||||||
<Container ref="characterContainer" :x="currentPositionX" :y="currentPositionY" :depth="isometricDepth">
|
<Container ref="characterContainer" :x="currentPositionX" :y="currentPositionY" :depth="isometricDepth">
|
||||||
<ChatBubble :mapCharacter="props.mapCharacter" />
|
<ChatBubble :mapCharacter="props.mapCharacter" />
|
||||||
<HealthBar :mapCharacter="props.mapCharacter" />
|
<HealthBar :mapCharacter="props.mapCharacter" />
|
||||||
<CharacterHair :mapCharacter="props.mapCharacter" />
|
<CharacterHair :mapCharacter="props.mapCharacter" :flipX="isFlippedX" />
|
||||||
<Sprite ref="characterSprite" :origin-y="1" :flipX="isFlippedX" />
|
<Sprite ref="characterSprite" :flipX="isFlippedX" />
|
||||||
</Container>
|
</Container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Direction } from '@/application/enums'
|
|
||||||
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 { useCharacterSprite } from '@/composables/useCharacterSpriteComposable'
|
import { useCharacterSpriteComposable } from '@/composables/useCharacterSpriteComposable'
|
||||||
|
import { useSoundComposable } from '@/composables/useSoundComposable'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useMapStore } from '@/stores/mapStore'
|
import { useMapStore } from '@/stores/mapStore'
|
||||||
import { Container, Sprite, useScene } from 'phavuer'
|
import { Container, Sprite, useScene } from 'phavuer'
|
||||||
import { onMounted, onUnmounted, watch } from 'vue'
|
import { onMounted, onUnmounted, watch } from 'vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
tilemap: Phaser.Tilemaps.Tilemap
|
tileMap: Phaser.Tilemaps.Tilemap
|
||||||
mapCharacter: MapCharacter
|
mapCharacter: MapCharacter
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
@ -28,14 +28,14 @@ const gameStore = useGameStore()
|
|||||||
const mapStore = useMapStore()
|
const mapStore = useMapStore()
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
|
|
||||||
const { characterContainer, characterSprite, currentPositionX, currentPositionY, isometricDepth, isFlippedX, updatePosition, calcDirection, updateSprite, initializeSprite, cleanup } = useCharacterSprite(scene, props.tilemap, props.mapCharacter)
|
const { characterContainer, characterSprite, currentPositionX, currentPositionY, isometricDepth, isFlippedX, updatePosition, playAnimation, updateSprite, initializeSprite, cleanup } = useCharacterSpriteComposable(scene, props.tileMap, props.mapCharacter)
|
||||||
|
const { playSound, stopSound } = useSoundComposable()
|
||||||
|
|
||||||
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) {
|
||||||
@ -43,6 +43,39 @@ 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,
|
||||||
@ -51,14 +84,15 @@ watch(
|
|||||||
rotation: props.mapCharacter.character.rotation,
|
rotation: props.mapCharacter.character.rotation,
|
||||||
isAttacking: props.mapCharacter.isAttacking
|
isAttacking: props.mapCharacter.isAttacking
|
||||||
}),
|
}),
|
||||||
handlePositionUpdate
|
async (oldValues, newValues) => {
|
||||||
|
handlePositionUpdate(oldValues, newValues)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await initializeSprite()
|
await initializeSprite()
|
||||||
|
|
||||||
if (props.mapCharacter.character.id === gameStore.character!.id) {
|
if (props.mapCharacter.character.id === gameStore.character!.id) {
|
||||||
mapStore.setCharacterLoaded(true)
|
|
||||||
scene.cameras.main.startFollow(characterContainer.value as Phaser.GameObjects.Container)
|
scene.cameras.main.startFollow(characterContainer.value as Phaser.GameObjects.Container)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,51 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Image v-bind="imageProps" v-if="gameStore.getLoadedAsset(texture)" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import type { MapCharacter, Sprite as SpriteT } from '@/application/types'
|
|
||||||
import { loadSpriteTextures } from '@/composables/gameComposable'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import { Image, useScene } from 'phavuer'
|
|
||||||
import { computed } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
mapCharacter: MapCharacter
|
|
||||||
currentX: number
|
|
||||||
currentY: number
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const scene = useScene()
|
|
||||||
|
|
||||||
const texture = computed(() => {
|
|
||||||
const { rotation, characterHair } = props.mapCharacter.character
|
|
||||||
const spriteId = characterHair?.sprite?.id
|
|
||||||
const direction = [0, 6].includes(rotation) ? 'back' : 'front'
|
|
||||||
|
|
||||||
return `${spriteId}-${direction}`
|
|
||||||
})
|
|
||||||
|
|
||||||
const isFlippedX = computed(() => [6, 4].includes(props.mapCharacter.character.rotation ?? 0))
|
|
||||||
|
|
||||||
const imageProps = computed(() => {
|
|
||||||
// Get the current sprite action based on direction
|
|
||||||
const direction = [0, 6].includes(props.mapCharacter.character.rotation ?? 0) ? 'back' : 'front'
|
|
||||||
const spriteAction = props.mapCharacter.character.characterHair?.sprite?.spriteActions?.find((spriteAction) => spriteAction.action === direction)
|
|
||||||
|
|
||||||
return {
|
|
||||||
depth: 1,
|
|
||||||
originX: Number(spriteAction?.originX) ?? 0,
|
|
||||||
originY: Number(spriteAction?.originY) ?? 0,
|
|
||||||
flipX: isFlippedX.value,
|
|
||||||
texture: texture.value
|
|
||||||
// y: props.mapCharacter.isMoving ? Math.floor(Date.now() / 250) % 2 : 0
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
loadSpriteTextures(scene, props.mapCharacter.character.characterHair?.sprite as SpriteT)
|
|
||||||
.then(() => {})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Error loading texture:', error)
|
|
||||||
})
|
|
||||||
</script>
|
|
@ -1,54 +1,63 @@
|
|||||||
<template>
|
<template>
|
||||||
<Image v-bind="imageProps" v-if="gameStore.isTextureLoaded(texture)" />
|
<Image ref="image" v-if="hairSpriteId" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<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 { Image, refObj, useScene } from 'phavuer'
|
||||||
import { Image, useScene } from 'phavuer'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
mapCharacter: MapCharacter
|
mapCharacter: MapCharacter
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
const hairSpriteId = ref('')
|
const hairSpriteId = ref('')
|
||||||
const sprite = ref<SpriteT | null>(null)
|
const hairSprite = ref<SpriteT | null>(null)
|
||||||
|
const characterSpriteHeight = ref(0)
|
||||||
|
const image = refObj<Phaser.GameObjects.Image>()
|
||||||
|
|
||||||
|
const flipX = computed(() => [6, 0].includes(props.mapCharacter.character.rotation ?? 0))
|
||||||
const texture = computed(() => {
|
const texture = computed(() => {
|
||||||
const { rotation } = props.mapCharacter.character
|
const direction = flipX.value ? 'back' : 'front'
|
||||||
const direction = [0, 6].includes(rotation) ? 'back' : 'front'
|
|
||||||
|
|
||||||
return `${hairSpriteId.value}-${direction}`
|
return `${hairSpriteId.value}-${direction}`
|
||||||
})
|
})
|
||||||
|
|
||||||
const isFlippedX = computed(() => [6, 4].includes(props.mapCharacter.character.rotation ?? 0))
|
watch(
|
||||||
|
() => props.mapCharacter.character,
|
||||||
const imageProps = computed(() => {
|
(newValue) => {
|
||||||
const direction = [0, 6].includes(props.mapCharacter.character.rotation ?? 0) ? 'back' : 'front'
|
if (!image.value) return
|
||||||
const spriteAction = sprite.value?.spriteActions?.find((spriteAction) => spriteAction.action === direction)
|
image.value.setTexture(texture.value)
|
||||||
|
},
|
||||||
return {
|
{ deep: true }
|
||||||
depth: 9999,
|
)
|
||||||
originX: Number(spriteAction?.originX) ?? 0,
|
|
||||||
originY: Number(spriteAction?.originY) ?? 0,
|
|
||||||
flipX: isFlippedX.value,
|
|
||||||
texture: texture.value
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const characterHairStorage = new CharacterHairStorage()
|
if (!props.mapCharacter.character.characterType || !props.mapCharacter.character.characterHair) return
|
||||||
const spriteId = await characterHairStorage.getSpriteId(props.mapCharacter.character.characterHair!)
|
|
||||||
if (!spriteId) return
|
|
||||||
|
|
||||||
hairSpriteId.value = spriteId
|
const characterTypeStorage = new CharacterTypeStorage()
|
||||||
|
const characterHairStorage = new CharacterHairStorage()
|
||||||
const spriteStorage = new SpriteStorage()
|
const spriteStorage = new SpriteStorage()
|
||||||
sprite.value = await spriteStorage.get(spriteId)
|
|
||||||
await loadSpriteTextures(scene, spriteId)
|
const characterType = await characterTypeStorage.getById(props.mapCharacter.character.characterType!)
|
||||||
|
if (!characterType) return
|
||||||
|
characterSpriteHeight.value = 100
|
||||||
|
|
||||||
|
hairSpriteId.value = await characterHairStorage.getSpriteId(props.mapCharacter.character.characterHair)
|
||||||
|
if (!hairSpriteId.value) return
|
||||||
|
|
||||||
|
hairSprite.value = await spriteStorage.getById(hairSpriteId.value)
|
||||||
|
if (!hairSprite.value) return
|
||||||
|
|
||||||
|
await loadSpriteTextures(scene, hairSpriteId.value)
|
||||||
|
|
||||||
|
if (!image.value) return
|
||||||
|
|
||||||
|
image.value.setOrigin(0.5, 2.15)
|
||||||
|
image.value.setTexture(texture.value)
|
||||||
|
image.value.setSize(30, 40)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Container ref="characterChatContainer" :depth="999">
|
<Container ref="characterChatContainer">
|
||||||
<RoundRectangle @create="createChatBubble" :origin-x="0.5" :origin-y="7.5" :fillColor="0xffffff" :width="194" :height="21" :radius="20" />
|
<RoundRectangle @create="createChatBubble" :origin-x="0.5" :origin-y="7.5" :fillColor="0xffffff" :width="194" :height="21" :radius="20" />
|
||||||
<Text @create="createChatText" :style="{ fontSize: 13, fontFamily: 'Arial', color: '#000' }" />
|
<Text @create="createChatText" :style="{ fontSize: 13, fontFamily: 'Arial', color: '#000' }" />
|
||||||
</Container>
|
</Container>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<div class="w-full md:min-w-[350px] max-w-[750px] flex flex-col absolute left-1/2 -translate-x-1/2 bottom-5">
|
<div class="w-full md:min-w-[350px] max-w-[750px] flex flex-col absolute left-1/2 -translate-x-1/2 bottom-5">
|
||||||
<div ref="chatWindow" class="w-full overflow-auto h-32 mb-5 bg-gray rounded-md border-2 border-solid border-gray-500 text-gray-300" v-show="gameStore.uiSettings.isChatOpen">
|
<div ref="chatWindow" class="w-full overflow-auto h-32 mb-5 bg-gray rounded-md border-2 border-solid border-gray-500 text-gray-300" v-show="gameStore.uiSettings.isChatOpen">
|
||||||
<div v-for="message in chats" class="flex-col py-2 items-center p-3">
|
<div v-for="message in chats" class="flex-col py-2 items-center p-3">
|
||||||
<span class="text-ellipsis overflow-hidden whitespace-nowrap text-sm" :class="{ 'text-cyan-300': gameStore.character?.role == 'gm' }">{{ message.character.name }}</span>
|
<span class="text-ellipsis overflow-hidden whitespace-nowrap text-sm" :class="{ 'text-cyan-300': gameStore.character?.role == 'gm' }">{{ message.character }}</span>
|
||||||
<p class="text-gray-50 m-0">{{ message.message }}</p>
|
<p class="text-gray-50 m-0">{{ message.message }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -21,7 +21,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Chat } from '@/application/types'
|
import { SocketEvent } from '@/application/enums'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useMapStore } from '@/stores/mapStore'
|
import { useMapStore } from '@/stores/mapStore'
|
||||||
import { onClickOutside, useFocus } from '@vueuse/core'
|
import { onClickOutside, useFocus } from '@vueuse/core'
|
||||||
@ -30,10 +31,9 @@ import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
|||||||
|
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const mapStore = useMapStore()
|
|
||||||
|
|
||||||
const message = ref('')
|
const message = ref('')
|
||||||
const chats = ref([] as Chat[])
|
const chats = ref<{ character: string; message: string }[]>([])
|
||||||
const chatWindow = ref<HTMLElement | null>(null)
|
const chatWindow = ref<HTMLElement | null>(null)
|
||||||
const chatInput = ref<HTMLElement | null>(null)
|
const chatInput = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ function unfocusChat(event: Event, targetElement: HTMLElement) {
|
|||||||
|
|
||||||
const sendMessage = () => {
|
const sendMessage = () => {
|
||||||
if (!message.value.trim()) return
|
if (!message.value.trim()) return
|
||||||
gameStore.connection?.emit('chat:message', { message: message.value }, (response: boolean) => {})
|
socketManager.emit(SocketEvent.CHAT_MESSAGE, { message: message.value }, (response: boolean) => {})
|
||||||
message.value = ''
|
message.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,21 +79,30 @@ const scrollToBottom = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.on('chat:message', (data: Chat) => {
|
socketManager.on(SocketEvent.CHAT_MESSAGE, (data: { character: string; message: string }) => {
|
||||||
chats.value.push(data)
|
if (!data.character || !data.message) return
|
||||||
|
|
||||||
|
chats.value.push({ character: data.character, message: data.message })
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
|
|
||||||
if (!mapStore.characterLoaded) return
|
const characterContainer = scene.children.getByName(data.character) as Phaser.GameObjects.Container
|
||||||
|
if (!characterContainer) {
|
||||||
|
console.log('No character container found')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const characterContainer = scene.children.getByName(data.character.name) as Phaser.GameObjects.Container
|
const characterChatContainer = characterContainer.getByName(data.character + '_chatContainer') as Phaser.GameObjects.Container
|
||||||
if (!characterContainer) return
|
if (!characterChatContainer) {
|
||||||
|
console.log('No character chat container found')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const characterChatContainer = characterContainer.getByName(data.character.name + '_chatContainer') as Phaser.GameObjects.Container
|
const chatBubble = characterChatContainer.getByName(data.character + '_chatBubble') as Phaser.GameObjects.Container
|
||||||
if (!characterChatContainer) return
|
const chatText = characterChatContainer.getByName(data.character + '_chatText') as Phaser.GameObjects.Text
|
||||||
|
if (!chatText || !chatBubble) {
|
||||||
const chatBubble = characterChatContainer.getByName(data.character.name + '_chatBubble') as Phaser.GameObjects.Container
|
console.log('No chat text or bubble found')
|
||||||
const chatText = characterChatContainer.getByName(data.character.name + '_chatText') as Phaser.GameObjects.Text
|
return
|
||||||
if (!chatText || !chatBubble) return
|
}
|
||||||
|
|
||||||
function calculateTextWidth(text: string, font: string, fontSize: number): number {
|
function calculateTextWidth(text: string, font: string, fontSize: number): number {
|
||||||
// Create a canvas element
|
// Create a canvas element
|
||||||
@ -144,7 +153,7 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
gameStore.connection?.off('chat:message')
|
socketManager.off(SocketEvent.CHAT_MESSAGE)
|
||||||
removeEventListener('keydown', focusChat)
|
removeEventListener('keydown', focusChat)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="absolute top-0 right-4 hidden lg:block">
|
<div class="absolute top-0 right-4 hidden lg:block" v-if="gameStore.world.date && typeof gameStore.world.date === 'object'">
|
||||||
<p class="text-white text-lg">{{ gameStore.world.date.toLocaleString() }}</p>
|
<p class="text-white text-lg">
|
||||||
|
{{ useDateFormat(gameStore.world.date, 'YYYY/MM/DD HH:mm') }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { SocketEvent } from '@/application/enums'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useDateFormat } from '@vueuse/core'
|
||||||
import { onUnmounted } from 'vue'
|
import { onUnmounted } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
|
||||||
// Listen for new date from socket
|
|
||||||
gameStore.connection?.on('date', (data: Date) => {
|
|
||||||
gameStore.world.date = new Date(data)
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
gameStore.connection?.off('date')
|
socketManager.off(SocketEvent.DATE)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,14 +1,49 @@
|
|||||||
<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 { SocketEvent } from '@/application/enums'
|
||||||
|
import type { MapCharacter, UUID } from '@/application/types'
|
||||||
import Character from '@/components/game/character/Character.vue'
|
import Character from '@/components/game/character/Character.vue'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
|
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
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
socketManager.on(SocketEvent.MAP_CHARACTER_JOIN, (data: MapCharacter) => {
|
||||||
|
mapStore.addCharacter(data)
|
||||||
|
})
|
||||||
|
|
||||||
|
socketManager.on(SocketEvent.MAP_CHARACTER_LEAVE, (characterId: UUID) => {
|
||||||
|
mapStore.removeCharacter(characterId)
|
||||||
|
})
|
||||||
|
|
||||||
|
socketManager.on(SocketEvent.MAP_CHARACTER_MOVE, ([characterId, posX, posY, rot, isMoving]: [UUID, number, number, number, boolean]) => {
|
||||||
|
mapStore.updateCharacterPosition([characterId, posX, posY, rot, isMoving])
|
||||||
|
|
||||||
|
if (characterId === gameStore.character?.id) {
|
||||||
|
gameStore.character!.positionX = posX
|
||||||
|
gameStore.character!.positionY = posY
|
||||||
|
gameStore.character!.rotation = rot
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
socketManager.on(SocketEvent.MAP_CHARACTER_ATTACK, (characterId: UUID) => {
|
||||||
|
mapStore.updateCharacterProperty(characterId, 'isAttacking', true)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
socketManager.off(SocketEvent.MAP_CHARACTER_ATTACK)
|
||||||
|
socketManager.off(SocketEvent.MAP_CHARACTER_MOVE)
|
||||||
|
socketManager.off(SocketEvent.MAP_CHARACTER_JOIN)
|
||||||
|
socketManager.off(SocketEvent.MAP_CHARACTER_LEAVE)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,56 +1,71 @@
|
|||||||
<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 { SocketEvent } from '@/application/enums'
|
||||||
|
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 { socketManager } from '@/managers/SocketManager'
|
||||||
|
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) => {
|
socketManager.on(SocketEvent.MAP_CHARACTER_TELEPORT, (data: mapLoadData) => {
|
||||||
mapStore.setMapId(data.mapId)
|
mapStore.setMapId(data.mapId)
|
||||||
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:attack', (characterId: UUID) => {
|
await loadTileTexturesFromMapTileArray(mapStore.mapId, scene)
|
||||||
mapStore.updateCharacterProperty(characterId, 'isAttacking', true)
|
|
||||||
})
|
|
||||||
|
|
||||||
gameStore.connection?.on('map:character:move', (data: { characterId: UUID; positionX: number; positionY: number; rotation: number; isMoving: boolean }) => {
|
tileMap.value = createTileMap(scene, map)
|
||||||
mapStore.updateCharacterPosition(data)
|
tileMapLayer.value = createTileLayer(tileMap.value, unduplicateArray(map.tiles))
|
||||||
// @TODO: Replace with universal class, composable or store
|
}
|
||||||
if (data.characterId === gameStore.character?.id) {
|
|
||||||
gameStore.character!.positionX = data.positionX
|
watch(
|
||||||
gameStore.character!.positionY = data.positionY
|
() => mapStore.mapId,
|
||||||
gameStore.character!.rotation = data.rotation
|
async () => {
|
||||||
|
await initialize()
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!mapStore.mapId) return
|
||||||
|
await initialize()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
mapStore.reset()
|
if (tileMap.value) {
|
||||||
gameStore.connection?.off('map:character:teleport')
|
tileMap.value.destroyLayer('tiles')
|
||||||
gameStore.connection?.off('map:character:join')
|
tileMap.value.removeAllLayers()
|
||||||
gameStore.connection?.off('map:character:leave')
|
tileMap.value.destroy()
|
||||||
gameStore.connection?.off('map:character:move')
|
}
|
||||||
|
|
||||||
|
socketManager.off(SocketEvent.MAP_CHARACTER_TELEPORT)
|
||||||
})
|
})
|
||||||
</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
|
||||||
|
102
src/components/game/map/partials/ImageGroup.vue
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<template>
|
||||||
|
<Zone :depth="baseDepth" :origin-x="mapObj?.originX" :origin-y="mapObj?.originY" :width="mapObj?.frameWidth" :height="mapObj?.frameHeight" :x="x" :y="y" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { MapObject, PlacedMapObject } from '@/application/types'
|
||||||
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
|
import { calculateIsometricDepth } from '@/services/mapService'
|
||||||
|
import { onPreUpdate, useScene, Zone } from 'phavuer'
|
||||||
|
import { computed, onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
obj?: PlacedMapObject
|
||||||
|
mapObj?: MapObject
|
||||||
|
x?: number
|
||||||
|
y?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const mapEditor = useMapEditorComposable()
|
||||||
|
const scene = useScene()
|
||||||
|
|
||||||
|
const group = scene.add.group()
|
||||||
|
const partitionPoints = computed(() => {
|
||||||
|
if (!props.mapObj?.frameWidth || !props.mapObj?.depthOffsets.length) return []
|
||||||
|
|
||||||
|
const sliceCount = props.mapObj.depthOffsets.length
|
||||||
|
return Array.from({ length: sliceCount + 1 }, (_, i) => i * (props.mapObj!.frameWidth / sliceCount))
|
||||||
|
})
|
||||||
|
|
||||||
|
let baseDepth = 0
|
||||||
|
|
||||||
|
const createImagePartition = (startX: number, endX: number, depthOffset: number): void => {
|
||||||
|
if (!props.mapObj?.id) return
|
||||||
|
|
||||||
|
const img = scene.add.image(0, 0, props.mapObj.id)
|
||||||
|
img.setOrigin(props.mapObj.originX, props.mapObj.originY)
|
||||||
|
img.setCrop(startX, 0, endX, props.mapObj.frameHeight)
|
||||||
|
img.setDepth(baseDepth + depthOffset)
|
||||||
|
group.add(img)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateGroupProperties = (): void => {
|
||||||
|
if (!props.obj || !props.x || !props.y) return
|
||||||
|
|
||||||
|
const isMoving = mapEditor.movingPlacedObject.value?.id === props.obj.id
|
||||||
|
const isSelected = mapEditor.selectedMapObject.value?.id === props.obj.id
|
||||||
|
const isPlacedSelected = mapEditor.selectedPlacedObject.value?.id === props.obj.id
|
||||||
|
|
||||||
|
baseDepth = calculateIsometricDepth(props.obj.positionX, props.obj.positionY)
|
||||||
|
|
||||||
|
group.setXY(props.x, props.y)
|
||||||
|
group.setAlpha(isMoving || isSelected ? 0.5 : 1)
|
||||||
|
group.setTint(isPlacedSelected ? 0x00ff00 : 0xffffff)
|
||||||
|
group.setDepth(baseDepth)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateImageProperties = (): void => {
|
||||||
|
const orderedImages = group.getChildren() as Phaser.GameObjects.Image[]
|
||||||
|
|
||||||
|
orderedImages.forEach((image, index) => {
|
||||||
|
if (!props.obj || !props.mapObj || !props.x) return
|
||||||
|
|
||||||
|
image.flipX = props.obj.isRotated
|
||||||
|
|
||||||
|
if (props.obj.isRotated) {
|
||||||
|
const offsetNum = props.mapObj.depthOffsets.length
|
||||||
|
const xOffset = props.mapObj.frameWidth / offsetNum
|
||||||
|
image.x = props.x + (index < offsetNum / 2 ? -xOffset : xOffset)
|
||||||
|
image.setDepth(baseDepth - props.mapObj.depthOffsets[index])
|
||||||
|
} else {
|
||||||
|
image.x = props.x
|
||||||
|
image.setDepth(baseDepth + props.mapObj.depthOffsets[index])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onPreUpdate(() => {
|
||||||
|
updateGroupProperties()
|
||||||
|
updateImageProperties()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Initial setup
|
||||||
|
const initializeGroup = (): void => {
|
||||||
|
if (!props.mapObj || !props.x || !props.y || !props.obj) return
|
||||||
|
|
||||||
|
baseDepth = calculateIsometricDepth(props.obj.positionX, props.obj.positionY)
|
||||||
|
group.setXY(props.x, props.y)
|
||||||
|
group.setOrigin(props.mapObj.originX, props.mapObj.originY)
|
||||||
|
|
||||||
|
const points = partitionPoints.value
|
||||||
|
for (let i = 0; i < points.length - 1; i++) {
|
||||||
|
createImagePartition(points[i], points[i + 1], props.mapObj.depthOffsets[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeGroup()
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
group.destroy(true, true)
|
||||||
|
})
|
||||||
|
</script>
|
@ -1,43 +1,70 @@
|
|||||||
<template>
|
<template>
|
||||||
<Image v-if="gameStore.isTextureLoaded(props.placedMapObject.mapObject.id)" v-bind="imageProps" />
|
<ImageGroup v-bind="groupProps" v-if="mapObject && gameStore.isTextureLoaded(props.placedMapObject.mapObject as string)" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PlacedMapObject, TextureData } from '@/application/types'
|
import type { MapObject, PlacedMapObject } from '@/application/types'
|
||||||
import { loadTexture } from '@/composables/gameComposable'
|
import ImageGroup from '@/components/game/map/partials/ImageGroup.vue'
|
||||||
import { calculateIsometricDepth, tileToWorldX, tileToWorldY } from '@/composables/mapComposable'
|
import { 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 { useScene } from 'phavuer'
|
||||||
import { computed, onMounted } from 'vue'
|
import { computed, onMounted, ref } 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 imageProps = computed(() => ({
|
const gameStore = useGameStore()
|
||||||
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),
|
const mapObject = ref<MapObject>()
|
||||||
y: tileToWorldY(props.tilemap, props.placedMapObject.positionX, props.placedMapObject.positionY),
|
|
||||||
flipX: props.placedMapObject.isRotated,
|
const groupProps = computed(() => ({
|
||||||
texture: props.placedMapObject.mapObject.id,
|
...calculateObjectPlacement(props.placedMapObject),
|
||||||
originY: Number(props.placedMapObject.mapObject.originX),
|
mapObj: mapObject.value,
|
||||||
originX: Number(props.placedMapObject.mapObject.originY)
|
obj: props.placedMapObject
|
||||||
}))
|
}))
|
||||||
|
|
||||||
loadTexture(scene, {
|
async function initialize() {
|
||||||
key: props.placedMapObject.mapObject.id,
|
if (!props.placedMapObject.mapObject) return
|
||||||
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)
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(async () => {})
|
/**
|
||||||
|
* Check if mapObject is an string or object, if its an object we assume its a mapObject and change it to a string
|
||||||
|
* We do this because this component is shared with the map editor, which gets sent the mapObject as an object by the server
|
||||||
|
*/
|
||||||
|
if (typeof props.placedMapObject.mapObject === 'object') {
|
||||||
|
// @ts-ignore
|
||||||
|
props.placedMapObject.mapObject = props.placedMapObject.mapObject.id
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapObjectStorage = new MapObjectStorage()
|
||||||
|
const _mapObject = await mapObjectStorage.getById(props.placedMapObject.mapObject as string)
|
||||||
|
if (!_mapObject) return
|
||||||
|
|
||||||
|
console.log(_mapObject)
|
||||||
|
|
||||||
|
mapObject.value = _mapObject
|
||||||
|
|
||||||
|
await loadMapObjectTextures([_mapObject], scene)
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateObjectPlacement(mapObj: PlacedMapObject): { x: number; y: number } {
|
||||||
|
let position = tileToWorldXY(props.tileMapLayer, mapObj.positionX, mapObj.positionY)
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: position.worldPositionX,
|
||||||
|
y: position.worldPositionY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await initialize()
|
||||||
|
})
|
||||||
</script>
|
</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>
|
||||||
@ -24,9 +24,8 @@ import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
|||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
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>
|
||||||
|
@ -20,12 +20,29 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
|
<div class="space-x-6 flex items-center">
|
||||||
|
<label for="color">Color</label>
|
||||||
|
<input v-model="characterColor" class="input-field" type="text" name="color" placeholder="Character Hair Color" />
|
||||||
|
<div class="h-[38px] w-[38px] rounded" :style="{ backgroundColor: characterColor }"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-field-half">
|
||||||
<label for="spriteId">Sprite</label>
|
<label for="spriteId">Sprite</label>
|
||||||
<select v-model="characterSpriteId" class="input-field" name="spriteId">
|
<select v-model="characterSpriteId" class="input-field" name="spriteId">
|
||||||
<option disabled selected value="">Select sprite</option>
|
<option disabled selected value="">Select sprite</option>
|
||||||
<option v-for="sprite in assetManagerStore.spriteList" :key="sprite.id" :value="sprite.id">{{ sprite.name }}</option>
|
<option v-for="sprite in assetManagerStore.spriteList" :key="sprite.id" :value="sprite.id">{{ sprite.name }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-field-half">
|
||||||
|
<label>Preview</label>
|
||||||
|
<div v-if="characterSpriteId" class="flex flex-col">
|
||||||
|
<div class="p-3 pb-5 min-h-32 block rounded-md default-border bg-gray-800">
|
||||||
|
<div class="flex items-center justify-center p-1 h-full bg-gray-700 rounded">
|
||||||
|
<img :src="config.server_endpoint + '/textures/sprites/' + characterSpriteId + '/front.png'" class="max-w-[200px] max-h-[200px] object-contain" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
||||||
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeCharacterHair">Remove</button>
|
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.prevent="removeCharacterHair">Remove</button>
|
||||||
</form>
|
</form>
|
||||||
@ -34,48 +51,50 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import config from '@/application/config'
|
||||||
|
import { SocketEvent } from '@/application/enums'
|
||||||
import type { CharacterGender, CharacterHair, Sprite } from '@/application/types'
|
import type { CharacterGender, CharacterHair, Sprite } from '@/application/types'
|
||||||
|
import { downloadCache } from '@/application/utilities'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
|
import { CharacterHairStorage } from '@/storage/storages'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const assetManagerStore = useAssetManagerStore()
|
const assetManagerStore = useAssetManagerStore()
|
||||||
|
|
||||||
const selectedCharacterHair = computed(() => assetManagerStore.selectedCharacterHair)
|
const selectedCharacterHair = computed(() => assetManagerStore.selectedCharacterHair)
|
||||||
|
|
||||||
const characterName = ref('')
|
const characterName = ref('')
|
||||||
const characterGender = ref<CharacterGender>('MALE' as CharacterGender.MALE)
|
const characterGender = ref<CharacterGender>('MALE' as CharacterGender.MALE)
|
||||||
|
const characterColor = ref<string>('#000000')
|
||||||
const characterIsSelectable = ref<boolean>(false)
|
const characterIsSelectable = ref<boolean>(false)
|
||||||
const characterSpriteId = ref<string | null | undefined>(null)
|
const characterSpriteId = ref<string | null | undefined>(null)
|
||||||
|
|
||||||
const genderOptions: CharacterGender[] = ['MALE' as CharacterGender.MALE, 'FEMALE' as CharacterGender.FEMALE]
|
const genderOptions: CharacterGender[] = ['MALE' as CharacterGender.MALE, 'FEMALE' as CharacterGender.FEMALE]
|
||||||
|
|
||||||
if (!selectedCharacterHair.value) {
|
|
||||||
console.error('No character hair selected')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedCharacterHair.value) {
|
if (selectedCharacterHair.value) {
|
||||||
characterName.value = selectedCharacterHair.value.name
|
characterName.value = selectedCharacterHair.value.name
|
||||||
characterGender.value = selectedCharacterHair.value.gender
|
characterGender.value = selectedCharacterHair.value.gender
|
||||||
|
characterColor.value = selectedCharacterHair.value.color
|
||||||
characterIsSelectable.value = selectedCharacterHair.value.isSelectable
|
characterIsSelectable.value = selectedCharacterHair.value.isSelectable
|
||||||
characterSpriteId.value = selectedCharacterHair.value.sprite?.id
|
characterSpriteId.value = selectedCharacterHair.value.sprite?.id
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeCharacterHair() {
|
async function removeCharacterHair() {
|
||||||
if (!selectedCharacterHair.value) return
|
if (!selectedCharacterHair.value) return
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:characterHair:remove', { id: selectedCharacterHair.value.id }, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_REMOVE, { id: selectedCharacterHair.value.id }, async (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to remove character hair')
|
console.error('Failed to remove character hair')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
refreshCharacterHairList()
|
|
||||||
|
await downloadCache('character_hair', new CharacterHairStorage())
|
||||||
|
await refreshCharacterHairList()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshCharacterHairList(unsetSelectedCharacterHair = true) {
|
async function refreshCharacterHairList(unsetSelectedCharacterHair = true) {
|
||||||
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
|
||||||
assetManagerStore.setCharacterHairList(response)
|
assetManagerStore.setCharacterHairList(response)
|
||||||
|
|
||||||
if (unsetSelectedCharacterHair) {
|
if (unsetSelectedCharacterHair) {
|
||||||
@ -84,21 +103,24 @@ function refreshCharacterHairList(unsetSelectedCharacterHair = true) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveCharacterHair() {
|
async function saveCharacterHair() {
|
||||||
const characterHairData = {
|
const characterHairData = {
|
||||||
id: selectedCharacterHair.value!.id,
|
id: selectedCharacterHair.value!.id,
|
||||||
name: characterName.value,
|
name: characterName.value,
|
||||||
gender: characterGender.value,
|
gender: characterGender.value,
|
||||||
|
color: characterColor.value,
|
||||||
isSelectable: characterIsSelectable.value,
|
isSelectable: characterIsSelectable.value,
|
||||||
spriteId: characterSpriteId.value
|
spriteId: characterSpriteId.value
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:characterHair:update', characterHairData, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_UPDATE, characterHairData, async (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to save character type')
|
console.error('Failed to save character type')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
refreshCharacterHairList(false)
|
|
||||||
|
await downloadCache('character_hair', new CharacterHairStorage())
|
||||||
|
await refreshCharacterHairList(false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,6 +128,7 @@ watch(selectedCharacterHair, (characterHair: CharacterHair | null) => {
|
|||||||
if (!characterHair) return
|
if (!characterHair) return
|
||||||
characterName.value = characterHair.name
|
characterName.value = characterHair.name
|
||||||
characterGender.value = characterHair.gender
|
characterGender.value = characterHair.gender
|
||||||
|
characterColor.value = characterHair.color
|
||||||
characterIsSelectable.value = characterHair.isSelectable
|
characterIsSelectable.value = characterHair.isSelectable
|
||||||
characterSpriteId.value = characterHair.sprite?.id
|
characterSpriteId.value = characterHair.sprite?.id
|
||||||
})
|
})
|
||||||
@ -113,7 +136,7 @@ watch(selectedCharacterHair, (characterHair: CharacterHair | null) => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!selectedCharacterHair.value) return
|
if (!selectedCharacterHair.value) return
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||||
assetManagerStore.setSpriteList(response)
|
assetManagerStore.setSpriteList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -32,7 +32,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { SocketEvent } from '@/application/enums'
|
||||||
import type { CharacterHair } from '@/application/types'
|
import type { CharacterHair } from '@/application/types'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useVirtualList } from '@vueuse/core'
|
import { useVirtualList } from '@vueuse/core'
|
||||||
@ -52,13 +54,13 @@ const handleSearch = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createNewCharacterHair = () => {
|
const createNewCharacterHair = () => {
|
||||||
gameStore.connection?.emit('gm:characterHair:create', {}, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_CREATE, {}, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to create new character type')
|
console.error('Failed to create new character type')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
|
||||||
assetManagerStore.setCharacterHairList(response)
|
assetManagerStore.setCharacterHairList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -92,7 +94,7 @@ function toTop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
gameStore.connection?.emit('gm:characterHair:list', {}, (response: CharacterHair[]) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERHAIR_LIST, {}, (response: CharacterHair[]) => {
|
||||||
assetManagerStore.setCharacterHairList(response)
|
assetManagerStore.setCharacterHairList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -40,12 +40,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { SocketEvent } from '@/application/enums'
|
||||||
import type { CharacterGender, CharacterRace, CharacterType, Sprite } from '@/application/types'
|
import type { CharacterGender, CharacterRace, CharacterType, Sprite } from '@/application/types'
|
||||||
|
import { downloadCache } from '@/application/utilities'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
|
import { CharacterTypeStorage } from '@/storage/storages'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const assetManagerStore = useAssetManagerStore()
|
const assetManagerStore = useAssetManagerStore()
|
||||||
|
|
||||||
const selectedCharacterType = computed(() => assetManagerStore.selectedCharacterType)
|
const selectedCharacterType = computed(() => assetManagerStore.selectedCharacterType)
|
||||||
@ -71,20 +73,22 @@ if (selectedCharacterType.value) {
|
|||||||
characterSpriteId.value = selectedCharacterType.value.sprite?.id
|
characterSpriteId.value = selectedCharacterType.value.sprite?.id
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeCharacterType() {
|
async function removeCharacterType() {
|
||||||
if (!selectedCharacterType.value) return
|
if (!selectedCharacterType.value) return
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:characterType:remove', { id: selectedCharacterType.value.id }, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_REMOVE, { id: selectedCharacterType.value.id }, async (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to remove character type')
|
console.error('Failed to remove character type')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
refreshCharacterTypeList()
|
|
||||||
|
await downloadCache('character_types', new CharacterTypeStorage())
|
||||||
|
await refreshCharacterTypeList()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshCharacterTypeList(unsetSelectedCharacterType = true) {
|
async function refreshCharacterTypeList(unsetSelectedCharacterType = true) {
|
||||||
gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
|
||||||
assetManagerStore.setCharacterTypeList(response)
|
assetManagerStore.setCharacterTypeList(response)
|
||||||
|
|
||||||
if (unsetSelectedCharacterType) {
|
if (unsetSelectedCharacterType) {
|
||||||
@ -93,7 +97,7 @@ function refreshCharacterTypeList(unsetSelectedCharacterType = true) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveCharacterType() {
|
async function saveCharacterType() {
|
||||||
const characterTypeData = {
|
const characterTypeData = {
|
||||||
id: selectedCharacterType.value!.id,
|
id: selectedCharacterType.value!.id,
|
||||||
name: characterName.value,
|
name: characterName.value,
|
||||||
@ -103,12 +107,14 @@ function saveCharacterType() {
|
|||||||
spriteId: characterSpriteId.value
|
spriteId: characterSpriteId.value
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:characterType:update', characterTypeData, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_UPDATE, characterTypeData, async (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to save character type')
|
console.error('Failed to save character type')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
refreshCharacterTypeList(false)
|
|
||||||
|
await downloadCache('character_types', new CharacterTypeStorage())
|
||||||
|
await refreshCharacterTypeList(false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,7 +130,7 @@ watch(selectedCharacterType, (characterType: CharacterType | null) => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!selectedCharacterType.value) return
|
if (!selectedCharacterType.value) return
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||||
assetManagerStore.setSpriteList(response)
|
assetManagerStore.setSpriteList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -32,7 +32,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { SocketEvent } from '@/application/enums'
|
||||||
import type { CharacterType } from '@/application/types'
|
import type { CharacterType } from '@/application/types'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useVirtualList } from '@vueuse/core'
|
import { useVirtualList } from '@vueuse/core'
|
||||||
@ -52,13 +54,13 @@ const handleSearch = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createNewCharacterType = () => {
|
const createNewCharacterType = () => {
|
||||||
gameStore.connection?.emit('gm:characterType:create', {}, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_CREATE, {}, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to create new character type')
|
console.error('Failed to create new character type')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
|
||||||
assetManagerStore.setCharacterTypeList(response)
|
assetManagerStore.setCharacterTypeList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -92,7 +94,7 @@ function toTop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
gameStore.connection?.emit('gm:characterType:list', {}, (response: CharacterType[]) => {
|
socketManager.emit(SocketEvent.GM_CHARACTERTYPE_LIST, {}, (response: CharacterType[]) => {
|
||||||
assetManagerStore.setCharacterTypeList(response)
|
assetManagerStore.setCharacterTypeList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -44,7 +44,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { SocketEvent } from '@/application/enums'
|
||||||
import type { Item, ItemRarity, ItemType, Sprite } from '@/application/types'
|
import type { Item, ItemRarity, ItemType, Sprite } from '@/application/types'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
@ -80,7 +82,7 @@ if (selectedItem.value) {
|
|||||||
function removeItem() {
|
function removeItem() {
|
||||||
if (!selectedItem.value) return
|
if (!selectedItem.value) return
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:item:remove', { id: selectedItem.value.id }, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_ITEM_REMOVE, { id: selectedItem.value.id }, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to remove item')
|
console.error('Failed to remove item')
|
||||||
return
|
return
|
||||||
@ -90,7 +92,7 @@ function removeItem() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function refreshItemList(unsetSelectedItem = true) {
|
function refreshItemList(unsetSelectedItem = true) {
|
||||||
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => {
|
socketManager.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
|
||||||
assetManagerStore.setItemList(response)
|
assetManagerStore.setItemList(response)
|
||||||
|
|
||||||
if (unsetSelectedItem) {
|
if (unsetSelectedItem) {
|
||||||
@ -110,7 +112,7 @@ function saveItem() {
|
|||||||
spriteId: itemSpriteId.value
|
spriteId: itemSpriteId.value
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:item:update', itemData, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_ITEM_UPDATE, itemData, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to save item')
|
console.error('Failed to save item')
|
||||||
return
|
return
|
||||||
@ -132,7 +134,7 @@ watch(selectedItem, (item: Item | null) => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!selectedItem.value) return
|
if (!selectedItem.value) return
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||||
assetManagerStore.setSpriteList(response)
|
assetManagerStore.setSpriteList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -29,7 +29,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { SocketEvent } from '@/application/enums'
|
||||||
import type { Item } from '@/application/types'
|
import type { Item } from '@/application/types'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useVirtualList } from '@vueuse/core'
|
import { useVirtualList } from '@vueuse/core'
|
||||||
@ -48,13 +50,13 @@ const handleSearch = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createNewItem = () => {
|
const createNewItem = () => {
|
||||||
gameStore.connection?.emit('gm:item:create', {}, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_ITEM_CREATE, {}, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to create new item')
|
console.error('Failed to create new item')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => {
|
socketManager.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
|
||||||
assetManagerStore.setItemList(response)
|
assetManagerStore.setItemList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -88,7 +90,7 @@ function toTop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
gameStore.connection?.emit('gm:item:list', {}, (response: Item[]) => {
|
socketManager.emit(SocketEvent.GM_ITEM_LIST, {}, (response: Item[]) => {
|
||||||
assetManagerStore.setItemList(response)
|
assetManagerStore.setItemList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,8 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full overflow-auto">
|
<div class="h-full overflow-auto">
|
||||||
<div class="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray">
|
<div class="relative p-2.5 flex flex-col items-center justify-center h-72 rounded-md default-border bg-gray">
|
||||||
<img class="max-h-56" :src="`${config.server_endpoint}/textures/map_objects/${selectedMapObject?.id}.png`" :alt="'Object ' + selectedMapObject?.id" />
|
<div class="grid grid-cols-[160px_auto_max-content] gap-12">
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" checked v-model="showOrigin" /><label>Show Origin</label>
|
||||||
|
<br />
|
||||||
|
<input type="checkbox" checked v-model="showPartitionOverlay" /><label>Show Partitions</label>
|
||||||
|
</div>
|
||||||
|
<div class="relative w-fit h-fit">
|
||||||
|
<img class="max-h-56" :src="`${config.server_endpoint}/textures/map_objects/${selectedMapObject?.id}.png`" :alt="'Object ' + selectedMapObject?.id" ref="imageRef" />
|
||||||
|
<svg ref="svg" class="absolute top-0 left-0 w-full h-full inline-block pointer-events-none">
|
||||||
|
<circle v-if="showOrigin && svg" r="4" :cx="mapObjectOriginX * width" :cy="mapObjectOriginY * height" stroke="white" stroke-width="2" />
|
||||||
|
<rect v-if="showPartitionOverlay && svg" v-for="(offset, index) in mapObjectDepthOffsets" style="opacity: 0.5" stroke="red" :x="index * (width / mapObjectDepthOffsets.length)" :width="width / mapObjectDepthOffsets.length" :y="0" :height="height" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="btn-cyan px-4 py-1.5 min-w-24" @click="mapObjectDepthOffsets.push(0)">Add Partition</button>
|
||||||
|
<p>Depth Offset</p>
|
||||||
|
<div class="text-white grid grid-cols-[120px_80px_auto] items-baseline gap-2" v-for="(offset, index) in mapObjectDepthOffsets">
|
||||||
|
<input class="input-field max-h-4 mt-2" type="number" :value="offset" @change="setPartitionDepth($event, index)" />
|
||||||
|
<button @click="mapObjectDepthOffsets.splice(index, 1)">Remove</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-5 block">
|
<div class="mt-5 block">
|
||||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject">
|
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveObject">
|
||||||
<div class="form-field-full">
|
<div class="form-field-full">
|
||||||
@ -44,24 +66,34 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
|
import { SocketEvent } from '@/application/enums'
|
||||||
import type { MapObject } from '@/application/types'
|
import type { MapObject } from '@/application/types'
|
||||||
|
import { downloadCache } from '@/application/utilities'
|
||||||
import ChipsInput from '@/components/forms/ChipsInput.vue'
|
import ChipsInput from '@/components/forms/ChipsInput.vue'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
|
import { MapObjectStorage } from '@/storage/storages'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useElementSize } from '@vueuse/core'
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { Rectangle } from 'phavuer'
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const assetManagerStore = useAssetManagerStore()
|
const assetManagerStore = useAssetManagerStore()
|
||||||
|
|
||||||
const selectedMapObject = computed(() => assetManagerStore.selectedMapObject)
|
const selectedMapObject = computed(() => assetManagerStore.selectedMapObject)
|
||||||
|
const svg = useTemplateRef('svg')
|
||||||
|
const { width, height } = useElementSize(svg)
|
||||||
|
|
||||||
const mapObjectName = ref('')
|
const mapObjectName = ref('')
|
||||||
const mapObjectTags = ref<string[]>([])
|
const mapObjectTags = ref<string[]>([])
|
||||||
|
const mapObjectDepthOffsets = ref<number[]>([])
|
||||||
const mapObjectOriginX = ref(0)
|
const mapObjectOriginX = ref(0)
|
||||||
const mapObjectOriginY = ref(0)
|
const mapObjectOriginY = ref(0)
|
||||||
const mapObjectFrameRate = ref(0)
|
const mapObjectFrameRate = ref(0)
|
||||||
const mapObjectFrameWidth = ref(0)
|
const mapObjectFrameWidth = ref(0)
|
||||||
const mapObjectFrameHeight = ref(0)
|
const mapObjectFrameHeight = ref(0)
|
||||||
|
const imageRef = ref<HTMLImageElement | null>(null)
|
||||||
|
const showOrigin = ref(true)
|
||||||
|
const showPartitionOverlay = ref(true)
|
||||||
|
|
||||||
if (!selectedMapObject.value) {
|
if (!selectedMapObject.value) {
|
||||||
console.error('No map mapObject selected')
|
console.error('No map mapObject selected')
|
||||||
@ -70,6 +102,7 @@ if (!selectedMapObject.value) {
|
|||||||
if (selectedMapObject.value) {
|
if (selectedMapObject.value) {
|
||||||
mapObjectName.value = selectedMapObject.value.name
|
mapObjectName.value = selectedMapObject.value.name
|
||||||
mapObjectTags.value = selectedMapObject.value.tags
|
mapObjectTags.value = selectedMapObject.value.tags
|
||||||
|
mapObjectDepthOffsets.value = selectedMapObject.value.depthOffsets
|
||||||
mapObjectOriginX.value = selectedMapObject.value.originX
|
mapObjectOriginX.value = selectedMapObject.value.originX
|
||||||
mapObjectOriginY.value = selectedMapObject.value.originY
|
mapObjectOriginY.value = selectedMapObject.value.originY
|
||||||
mapObjectFrameRate.value = selectedMapObject.value.frameRate
|
mapObjectFrameRate.value = selectedMapObject.value.frameRate
|
||||||
@ -77,18 +110,23 @@ if (selectedMapObject.value) {
|
|||||||
mapObjectFrameHeight.value = selectedMapObject.value.frameHeight
|
mapObjectFrameHeight.value = selectedMapObject.value.frameHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeObject() {
|
const setPartitionDepth = (event: any, idx: number) => (mapObjectDepthOffsets.value[idx] = Number.parseInt(event.target.value))
|
||||||
gameStore.connection?.emit('gm:mapObject:remove', { mapObject: selectedMapObject.value?.id }, (response: boolean) => {
|
|
||||||
|
async function removeObject() {
|
||||||
|
if (!selectedMapObject.value) return
|
||||||
|
socketManager.emit(SocketEvent.GM_MAPOBJECT_REMOVE, { mapObjectId: selectedMapObject.value.id }, async (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to remove mapObject')
|
console.error('Failed to remove mapObject')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
refreshObjectList()
|
|
||||||
|
await downloadCache('map_objects', new MapObjectStorage())
|
||||||
|
await refreshObjectList()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshObjectList(unsetSelectedMapObject = true) {
|
async function refreshObjectList(unsetSelectedMapObject = true) {
|
||||||
gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => {
|
socketManager.emit(SocketEvent.GM_MAPOBJECT_LIST, {}, (response: MapObject[]) => {
|
||||||
assetManagerStore.setMapObjectList(response)
|
assetManagerStore.setMapObjectList(response)
|
||||||
|
|
||||||
if (unsetSelectedMapObject) {
|
if (unsetSelectedMapObject) {
|
||||||
@ -97,30 +135,32 @@ function refreshObjectList(unsetSelectedMapObject = true) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveObject() {
|
async function saveObject() {
|
||||||
if (!selectedMapObject.value) {
|
if (!selectedMapObject.value) {
|
||||||
console.error('No mapObject selected')
|
console.error('No mapObject selected')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
socketManager.emit(
|
||||||
gameStore.connection?.emit(
|
SocketEvent.GM_MAPOBJECT_UPDATE,
|
||||||
'gm:mapObject:update',
|
|
||||||
{
|
{
|
||||||
id: selectedMapObject.value.id,
|
id: selectedMapObject.value.id,
|
||||||
name: mapObjectName.value,
|
name: mapObjectName.value,
|
||||||
tags: mapObjectTags.value,
|
tags: mapObjectTags.value,
|
||||||
|
depthOffsets: mapObjectDepthOffsets.value,
|
||||||
originX: mapObjectOriginX.value,
|
originX: mapObjectOriginX.value,
|
||||||
originY: mapObjectOriginY.value,
|
originY: mapObjectOriginY.value,
|
||||||
frameRate: mapObjectFrameRate.value,
|
frameRate: mapObjectFrameRate.value,
|
||||||
frameWidth: mapObjectFrameWidth.value,
|
frameWidth: mapObjectFrameWidth.value,
|
||||||
frameHeight: mapObjectFrameHeight.value
|
frameHeight: mapObjectFrameHeight.value
|
||||||
},
|
},
|
||||||
(response: boolean) => {
|
async (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to save mapObject')
|
console.error('Failed to save mapObject')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
refreshObjectList(false)
|
|
||||||
|
await downloadCache('map_objects', new MapObjectStorage())
|
||||||
|
await refreshObjectList(false)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -129,6 +169,7 @@ watch(selectedMapObject, (mapObject: MapObject | null) => {
|
|||||||
if (!mapObject) return
|
if (!mapObject) return
|
||||||
mapObjectName.value = mapObject.name
|
mapObjectName.value = mapObject.name
|
||||||
mapObjectTags.value = mapObject.tags
|
mapObjectTags.value = mapObject.tags
|
||||||
|
mapObjectDepthOffsets.value = mapObject.depthOffsets
|
||||||
mapObjectOriginX.value = mapObject.originX
|
mapObjectOriginX.value = mapObject.originX
|
||||||
mapObjectOriginY.value = mapObject.originY
|
mapObjectOriginY.value = mapObject.originY
|
||||||
mapObjectFrameRate.value = mapObject.frameRate
|
mapObjectFrameRate.value = mapObject.frameRate
|
||||||
@ -140,7 +181,37 @@ onMounted(() => {
|
|||||||
if (!selectedMapObject.value) return
|
if (!selectedMapObject.value) return
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// function startDragging(index: number, event: MouseEvent) {
|
||||||
|
// isDragging.value = true
|
||||||
|
// draggedPointIndex.value = index
|
||||||
|
//
|
||||||
|
// const moveHandler = (e: MouseEvent) => {
|
||||||
|
// if (!isDragging.value || !imageRef.value) return
|
||||||
|
// const rect = imageRef.value.getBoundingClientRect()
|
||||||
|
// mapObjectPivotPoints.value[draggedPointIndex.value] = {
|
||||||
|
// x: e.clientX - rect.left,
|
||||||
|
// y: e.clientY - rect.top
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// const upHandler = () => {
|
||||||
|
// isDragging.value = false
|
||||||
|
// draggedPointIndex.value = -1
|
||||||
|
// window.removeEventListener('mousemove', moveHandler)
|
||||||
|
// window.removeEventListener('mouseup', upHandler)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// window.addEventListener('mousemove', moveHandler)
|
||||||
|
// window.addEventListener('mouseup', upHandler)
|
||||||
|
// }
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
assetManagerStore.setSelectedMapObject(null)
|
assetManagerStore.setSelectedMapObject(null)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pointer-events-none {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -29,7 +29,9 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
|
import { SocketEvent } from '@/application/enums'
|
||||||
import type { MapObject } from '@/application/types'
|
import type { MapObject } from '@/application/types'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useVirtualList } from '@vueuse/core'
|
import { useVirtualList } from '@vueuse/core'
|
||||||
@ -47,13 +49,13 @@ const elementToScroll = ref()
|
|||||||
const handleFileUpload = (e: Event) => {
|
const handleFileUpload = (e: Event) => {
|
||||||
const files = (e.target as HTMLInputElement).files
|
const files = (e.target as HTMLInputElement).files
|
||||||
if (!files) return
|
if (!files) return
|
||||||
gameStore.connection?.emit('gm:mapObject:upload', files, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_MAPOBJECT_UPLOAD, files, (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
if (config.development) console.error('Failed to upload object')
|
if (config.environment === 'development') console.error('Failed to upload map object')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => {
|
socketManager.emit(SocketEvent.GM_MAPOBJECT_LIST, {}, (response: MapObject[]) => {
|
||||||
assetManagerStore.setMapObjectList(response)
|
assetManagerStore.setMapObjectList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -92,7 +94,7 @@ function toTop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
gameStore.connection?.emit('gm:mapObject:list', {}, (response: MapObject[]) => {
|
socketManager.emit(SocketEvent.GM_MAPOBJECT_LIST, {}, (response: MapObject[]) => {
|
||||||
assetManagerStore.setMapObjectList(response)
|
assetManagerStore.setMapObjectList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,91 +1,75 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full overflow-auto">
|
<div class="h-full overflow-auto">
|
||||||
<div class="relative flex flex-col">
|
<div class="relative flex flex-col">
|
||||||
<div class="flex flex-wrap gap-2 p-2.5 rounded-md default-border bg-gray">
|
<div class="flex flex-wrap gap-2 p-2.5 rounded-md default-border bg-gray mb-4">
|
||||||
<div class="w-full flex flex-col">
|
<div class="w-full flex flex-col">
|
||||||
<label class="mb-1.5 font-titles" for="name">Name</label>
|
<label class="mb-1.5 font-titles" for="name">Name</label>
|
||||||
<input v-model="spriteName" class="input-field" type="text" name="name" placeholder="New sprite" />
|
<input v-model="spriteName" class="input-field" type="text" name="name" placeholder="New sprite" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full flex gap-2 mt-2 pb-4 relative">
|
<div class="w-full flex gap-2 mt-2 pb-4 relative">
|
||||||
<button class="btn-cyan px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="saveSprite">Save</button>
|
<button class="btn-cyan px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="saveSprite">Save</button>
|
||||||
<button class="btn-red px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="deleteSprite">Delete</button>
|
<button class="btn-red px-4 py-2 flex-1 sm:flex-none sm:min-w-24" type="button" @click.prevent="deleteSprite">Delete</button>
|
||||||
<button class="btn bg-indigo-500 hover:bg-indigo-600 rounded text-white px-4 py-2 flex-1 sm:flex-none" type="button" @click.prevent="copySprite">
|
<button class="btn-indigo px-4 py-2 flex-1 sm:flex-none" type="button" @click.prevent="copySprite">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn-cyan px-4" type="button" @click.prevent="addNewImage">New action</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-for="action in spriteActions" :key="action.id">
|
||||||
<button class="btn-cyan py-2 my-4" type="button" @click.prevent="addNewImage">New action</button>
|
<div class="flex flex-wrap gap-3 mb-3">
|
||||||
<Accordion v-for="action in spriteActions" :key="action.id">
|
<div v-for="(image, index) in action.sprites" :key="index" class="h-20 w-20 p-4 bg-gray-300 bg-opacity-50 rounded text-center relative group">
|
||||||
<template #header>
|
<img :src="image.url" class="max-w-full max-h-full object-contain pointer-events-none" alt="Uploaded image" @load="updateImageDimensions($event, index)" />
|
||||||
<div class="flex items-center">
|
<div v-if="imageDimensions[index]" class="absolute bottom-1 right-1 bg-black/50 text-white text-xs px-1 py-0.5 rounded transition-opacity font-default">{{ imageDimensions[index].width }}x{{ imageDimensions[index].height }}</div>
|
||||||
{{ action.action }}
|
|
||||||
<div class="ml-auto space-x-2">
|
|
||||||
<button class="btn-cyan px-4 py-1.5 min-w-24" type="button" @click.stop.prevent="openPreviewModal(action)">View</button>
|
|
||||||
<button class="btn-red px-4 py-1.5 min-w-24" type="button" @click.stop.prevent="() => spriteActions.splice(spriteActions.indexOf(action), 1)">Delete</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
<template #content>
|
<div class="flex items-center mb-3">
|
||||||
<form class="flex gap-2.5 flex-wrap" @submit.prevent="saveSprite">
|
<div class="mr-3 space-x-2">
|
||||||
<div class="form-field-full">
|
<button class="btn-cyan px-4 py-1.5 min-w-24 text-left" type="button" @click.stop.prevent="openEditorModal(action)">
|
||||||
<label for="action">Action</label>
|
Editor
|
||||||
<input v-model="action.action" class="input-field" type="text" name="action" placeholder="Action" />
|
<div class="flex">
|
||||||
</div>
|
<small class="text-xs font-default">{{ action.action }}</small>
|
||||||
<div class="form-field-half">
|
</div>
|
||||||
<label for="origin-x">Origin X</label>
|
</button>
|
||||||
<input v-model.number="action.originX" class="input-field" type="number" step="any" name="origin-x" placeholder="Origin X" />
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-half">
|
</div>
|
||||||
<label for="origin-y">Origin Y</label>
|
<SpriteEditor
|
||||||
<input v-model.number="action.originY" class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
|
v-for="[actionId, editorData] in Array.from(openEditors.entries())"
|
||||||
</div>
|
:key="actionId"
|
||||||
<div class="form-field-full">
|
:sprite="selectedSprite!"
|
||||||
<label for="frame-speed">Frame rate</label>
|
:sprites="editorData.action.sprites"
|
||||||
<input v-model.number="action.frameRate" class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
|
:frame-rate="editorData.action.frameRate"
|
||||||
</div>
|
:is-modal-open="editorData.isOpen"
|
||||||
<div class="form-field-full">
|
:temp-offset-index="getTempOffsetIndex(editorData.action)"
|
||||||
<SpriteActionsInput v-model="action.sprites" @tempOffsetChange="(index, offset) => handleTempOffsetChange(action, index, offset)" />
|
:temp-offset="getTempOffset(editorData.action)"
|
||||||
</div>
|
@update:frame-rate="(value) => updateFrameRate(editorData.action, value)"
|
||||||
</form>
|
@update:is-modal-open="(value) => handleEditorModalClose(editorData.action, value)"
|
||||||
</template>
|
@update:temp-offset="(index, offset) => handleTempOffsetChange(editorData.action, index, offset)"
|
||||||
</Accordion>
|
|
||||||
<SpritePreview
|
|
||||||
v-if="selectedAction"
|
|
||||||
:sprites="selectedAction.sprites"
|
|
||||||
:frame-rate="selectedAction.frameRate"
|
|
||||||
:is-modal-open="isModalOpen"
|
|
||||||
:temp-offset-index="tempOffsetData.index"
|
|
||||||
:temp-offset="tempOffsetData.offset"
|
|
||||||
@update:frame-rate="updateFrameRate"
|
|
||||||
@update:is-modal-open="isModalOpen = $event"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Sprite, SpriteAction, UUID } from '@/application/types'
|
import { SocketEvent } from '@/application/enums'
|
||||||
import { uuidv4 } from '@/application/utilities'
|
import type { Sprite, SpriteAction } from '@/application/types'
|
||||||
import SpriteActionsInput from '@/components/gameMaster/assetManager/partials/sprite/partials/SpriteImagesInput.vue'
|
import { downloadCache, uuidv4 } from '@/application/utilities'
|
||||||
import SpritePreview from '@/components/gameMaster/assetManager/partials/sprite/partials/SpritePreview.vue'
|
import SpriteEditor from '@/components/gameMaster/assetManager/partials/sprite/partials/SpriteEditor.vue'
|
||||||
import Accordion from '@/components/utilities/Accordion.vue'
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
|
import { SpriteStorage } from '@/storage/storages'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const assetManagerStore = useAssetManagerStore()
|
const assetManagerStore = useAssetManagerStore()
|
||||||
|
|
||||||
const selectedSprite = computed(() => assetManagerStore.selectedSprite)
|
const selectedSprite = computed(() => assetManagerStore.selectedSprite)
|
||||||
|
const tempOffsetData = ref<Map<string, { index: number | undefined; offset: { x: number; y: number } | undefined }>>(new Map())
|
||||||
const spriteName = ref('')
|
const spriteName = ref('')
|
||||||
const spriteActions = ref<SpriteAction[]>([])
|
const spriteActions = ref<SpriteAction[]>([])
|
||||||
const isModalOpen = ref(false)
|
|
||||||
const selectedAction = ref<SpriteAction | null>(null)
|
const openEditors = ref(new Map<string, { action: SpriteAction; isOpen: boolean }>())
|
||||||
|
|
||||||
if (!selectedSprite.value) {
|
if (!selectedSprite.value) {
|
||||||
console.error('No sprite selected')
|
console.error('No sprite selected')
|
||||||
@ -96,28 +80,32 @@ if (selectedSprite.value) {
|
|||||||
spriteActions.value = sortSpriteActions(selectedSprite.value.spriteActions)
|
spriteActions.value = sortSpriteActions(selectedSprite.value.spriteActions)
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteSprite() {
|
async function deleteSprite() {
|
||||||
gameStore.connection?.emit('gm:sprite:delete', { id: selectedSprite.value?.id }, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_SPRITE_DELETE, { id: selectedSprite.value?.id }, async (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to delete sprite')
|
console.error('Failed to delete sprite')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
refreshSpriteList()
|
|
||||||
|
await downloadCache('sprites', new SpriteStorage())
|
||||||
|
await refreshSpriteList()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function copySprite() {
|
async function copySprite() {
|
||||||
gameStore.connection?.emit('gm:sprite:copy', { id: selectedSprite.value?.id }, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_SPRITE_COPY, { id: selectedSprite.value?.id }, async (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to copy sprite')
|
console.error('Failed to copy sprite')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
refreshSpriteList(false)
|
|
||||||
|
await downloadCache('sprites', new SpriteStorage())
|
||||||
|
await refreshSpriteList(false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshSpriteList(unsetSelectedSprite = true) {
|
async function refreshSpriteList(unsetSelectedSprite = true) {
|
||||||
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||||
assetManagerStore.setSpriteList(response)
|
assetManagerStore.setSpriteList(response)
|
||||||
|
|
||||||
if (unsetSelectedSprite) {
|
if (unsetSelectedSprite) {
|
||||||
@ -126,7 +114,7 @@ function refreshSpriteList(unsetSelectedSprite = true) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveSprite() {
|
async function saveSprite() {
|
||||||
if (!selectedSprite.value) {
|
if (!selectedSprite.value) {
|
||||||
console.error('No sprite selected')
|
console.error('No sprite selected')
|
||||||
return
|
return
|
||||||
@ -136,25 +124,27 @@ function saveSprite() {
|
|||||||
id: selectedSprite.value.id,
|
id: selectedSprite.value.id,
|
||||||
name: spriteName.value,
|
name: spriteName.value,
|
||||||
spriteActions:
|
spriteActions:
|
||||||
spriteActions.value?.map((action) => {
|
spriteActions.value?.map((action) => {
|
||||||
return {
|
return {
|
||||||
action: action.action,
|
action: action.action,
|
||||||
sprites: action.sprites,
|
sprites: action.sprites,
|
||||||
originX: action.originX,
|
originX: action.originX,
|
||||||
originY: action.originY,
|
originY: action.originY,
|
||||||
frameRate: action.frameRate,
|
frameRate: action.frameRate,
|
||||||
frameWidth: action.frameWidth,
|
frameWidth: action.frameWidth,
|
||||||
frameHeight: action.frameHeight
|
frameHeight: action.frameHeight
|
||||||
}
|
}
|
||||||
}) ?? []
|
}) ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:sprite:update', updatedSprite, (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_SPRITE_UPDATE, updatedSprite, async (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to save sprite')
|
console.error('Failed to save sprite')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
refreshSpriteList(false)
|
|
||||||
|
await downloadCache('sprites', new SpriteStorage())
|
||||||
|
await refreshSpriteList(false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,39 +175,69 @@ function sortSpriteActions(actions: SpriteAction[]): SpriteAction[] {
|
|||||||
return [...actions].sort((a, b) => a.action.localeCompare(b.action))
|
return [...actions].sort((a, b) => a.action.localeCompare(b.action))
|
||||||
}
|
}
|
||||||
|
|
||||||
function openPreviewModal(action: SpriteAction) {
|
function openEditorModal(action: SpriteAction) {
|
||||||
selectedAction.value = action
|
const newOpenEditors = new Map(openEditors.value)
|
||||||
isModalOpen.value = true
|
newOpenEditors.set(action.id, { action, isOpen: true })
|
||||||
|
openEditors.value = newOpenEditors
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFrameRate(value: number) {
|
function updateFrameRate(action: SpriteAction, value: number) {
|
||||||
if (selectedAction.value) {
|
console.log('update frame rate', action)
|
||||||
selectedAction.value.frameRate = value
|
action.frameRate = value
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const tempOffsetData = ref<{ index: number | undefined; offset: { x: number; y: number } | undefined }>({
|
function handleEditorModalClose(action: SpriteAction, isOpen: boolean) {
|
||||||
index: undefined,
|
if (isOpen) return
|
||||||
offset: undefined
|
const newOpenEditors = new Map(openEditors.value)
|
||||||
})
|
newOpenEditors.delete(action.id)
|
||||||
|
openEditors.value = newOpenEditors
|
||||||
|
}
|
||||||
|
|
||||||
function handleTempOffsetChange(action: SpriteAction, index: number, offset: { x: number; y: number }) {
|
function handleTempOffsetChange(action: SpriteAction, index: number, offset: { x: number; y: number }) {
|
||||||
if (selectedAction.value === action) {
|
// Update the temporary offset data for this action
|
||||||
tempOffsetData.value = { index, offset }
|
const newTempOffsetData = new Map(tempOffsetData.value)
|
||||||
|
newTempOffsetData.set(action.id, { index, offset })
|
||||||
|
tempOffsetData.value = newTempOffsetData
|
||||||
|
|
||||||
|
// Also update the actual sprite data so changes persist
|
||||||
|
if (action.sprites && action.sprites[index]) {
|
||||||
|
action.sprites[index].offset = { ...offset };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTempOffsetIndex(action: SpriteAction): number | undefined {
|
||||||
|
return tempOffsetData.value.get(action.id)?.index
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTempOffset(action: SpriteAction): { x: number; y: number } | undefined {
|
||||||
|
return tempOffsetData.value.get(action.id)?.offset
|
||||||
|
}
|
||||||
|
|
||||||
watch(selectedSprite, (sprite: Sprite | null) => {
|
watch(selectedSprite, (sprite: Sprite | null) => {
|
||||||
if (!sprite) return
|
if (!sprite) return
|
||||||
spriteName.value = sprite.name
|
spriteName.value = sprite.name
|
||||||
spriteActions.value = sortSpriteActions(sprite.spriteActions)
|
spriteActions.value = sortSpriteActions(sprite.spriteActions)
|
||||||
|
openEditors.value = new Map()
|
||||||
|
tempOffsetData.value = new Map() // Reset temp offset data when sprite changes
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(isModalOpen, (newValue) => {
|
interface SpriteImage {
|
||||||
if (!newValue) {
|
url: string
|
||||||
selectedAction.value = null
|
offset: {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
|
const imageDimensions = ref<{ [key: number]: { width: number; height: number } }>({})
|
||||||
|
|
||||||
|
const updateImageDimensions = (event: Event, index: number) => {
|
||||||
|
const img = event.target as HTMLImageElement
|
||||||
|
imageDimensions.value[index] = {
|
||||||
|
width: img.naturalWidth,
|
||||||
|
height: img.naturalHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!selectedSprite.value) return
|
if (!selectedSprite.value) return
|
||||||
|
@ -25,7 +25,9 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
|
import { SocketEvent } from '@/application/enums'
|
||||||
import type { Sprite } from '@/application/types'
|
import type { Sprite } from '@/application/types'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useVirtualList } from '@vueuse/core'
|
import { useVirtualList } from '@vueuse/core'
|
||||||
@ -40,13 +42,13 @@ const hasScrolled = ref(false)
|
|||||||
const elementToScroll = ref()
|
const elementToScroll = ref()
|
||||||
|
|
||||||
function newButtonClickHandler() {
|
function newButtonClickHandler() {
|
||||||
gameStore.connection?.emit('gm:sprite:create', {}, (response: boolean) => {
|
socketManager.emit(SocketEvent.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
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||||
assetManagerStore.setSpriteList(response)
|
assetManagerStore.setSpriteList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -85,7 +87,7 @@ function toTop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
gameStore.connection?.emit('gm:sprite:list', {}, (response: Sprite[]) => {
|
socketManager.emit(SocketEvent.GM_SPRITE_LIST, {}, (response: Sprite[]) => {
|
||||||
assetManagerStore.setSpriteList(response)
|
assetManagerStore.setSpriteList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -0,0 +1,366 @@
|
|||||||
|
<template>
|
||||||
|
<Modal :is-modal-open="isModalOpen" :modal-width="700" :modal-height="330" :can-full-screen="true" bg-style="none" @modal:close="closeModal">
|
||||||
|
<template #modalHeader>
|
||||||
|
<h3 class="m-0 font-medium shrink-0 text-white">Sprite editor</h3>
|
||||||
|
</template>
|
||||||
|
<template #modalBody>
|
||||||
|
<div class="m-4 flex gap-4 h-full">
|
||||||
|
<!-- Settings -->
|
||||||
|
<div class="w-80 h-full flex flex-col overflow-y-auto">
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<label class="block mb-1 text-white text-sm">Frame Rate: {{ frameRate }} FPS</label>
|
||||||
|
<div class="text-xs font-default text-gray-400 mb-1">Duration: {{ totalDuration }}s</div>
|
||||||
|
<input type="range" v-model.number="localFrameRate" min="0" max="60" step="1" class="w-full accent-cyan-500" @input="updateFrameRate" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<label class="block mb-1 text-white text-sm">Frame: {{ currentFrame + 1 }} of {{ sprites.length }}</label>
|
||||||
|
<input type="range" v-model.number="currentFrame" :min="0" :max="sprites.length - 1" step="1" class="w-full accent-cyan-500" @input="stopAnimation" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<label class="block mb-1 text-white text-sm">Zoom: {{ zoomLevel }}%</label>
|
||||||
|
<input type="range" v-model.number="zoomLevel" min="10" max="600" step="10" class="w-full accent-cyan-500" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6 space-y-2">
|
||||||
|
<button @click="toggleAnimation" class="px-3 py-1 bg-cyan-600 hover:bg-cyan-700 text-white rounded transition-colors w-full">
|
||||||
|
{{ isAnimating ? 'Pause' : 'Play' }}
|
||||||
|
</button>
|
||||||
|
<button @click="toggleReferenceSprites" class="px-3 py-1 bg-cyan-600 hover:bg-cyan-700 text-white rounded transition-colors w-full">
|
||||||
|
{{ showReferenceSprites ? 'Hide References' : 'Show References' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6">
|
||||||
|
<form class="flex gap-2.5 flex-wrap" @submit.prevent="">
|
||||||
|
<div class="relative flex py-5 items-center">
|
||||||
|
<div class="flex-grow border-t border-gray-400"></div>
|
||||||
|
<span class="flex-shrink mx-4 text-gray-400">Sprite action</span>
|
||||||
|
<div class="flex-grow border-solid border-gray-200"></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="action">Name</label>
|
||||||
|
<input class="input-field" type="text" name="action" placeholder="Action" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field-half">
|
||||||
|
<label for="origin-x">Origin X</label>
|
||||||
|
<input class="input-field" type="number" step="any" name="origin-x" placeholder="Origin X" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field-half">
|
||||||
|
<label for="origin-y">Origin Y</label>
|
||||||
|
<input class="input-field" type="number" step="any" name="origin-y" placeholder="Origin Y" />
|
||||||
|
</div>
|
||||||
|
<div class="relative flex py-5 items-center">
|
||||||
|
<div class="flex-grow border-t border-gray-400"></div>
|
||||||
|
<span class="flex-shrink mx-4 text-gray-400">Sprite action image</span>
|
||||||
|
<div class="flex-grow border-t border-gray-400"></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-field-half">
|
||||||
|
<label for="offset-x">Offset X</label>
|
||||||
|
<input class="input-field" type="number" step="1" v-model.number="offsetXModel" :disabled="isAnimating" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field-half">
|
||||||
|
<label for="offset-y">Offset Y</label>
|
||||||
|
<input class="input-field" type="number" step="1" v-model.number="offsetYModel" :disabled="isAnimating" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field-full">
|
||||||
|
<label for="frame-speed">Frame rate</label>
|
||||||
|
<input class="input-field" type="number" step="any" name="frame-speed" placeholder="Frame rate" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Sprite thumbnails -->
|
||||||
|
<div class="flex-1 flex flex-col h-full">
|
||||||
|
<div class="bg-gray-800 border-solid border-white/10 rounded flex-grow mb-2 relative overflow-hidden" @mousedown="startDrag" @mousemove="onDrag" @mouseup="stopDrag" @mouseleave="stopDrag">
|
||||||
|
<!-- Background reference sprites (semi-transparent) -->
|
||||||
|
<img
|
||||||
|
v-for="(sprite, index) in spritesWithTempOffset"
|
||||||
|
:key="`bg-${index}`"
|
||||||
|
:src="sprite.url"
|
||||||
|
alt="Reference sprite"
|
||||||
|
v-show="index !== currentFrame && showReferenceSprites"
|
||||||
|
:style="{
|
||||||
|
position: 'absolute',
|
||||||
|
left: `${(sprite.offset?.x || 0) * (zoomLevel / 100)}px`,
|
||||||
|
bottom: `${(sprite.offset?.y || 0) * (zoomLevel / 100)}px`,
|
||||||
|
opacity: 0.3,
|
||||||
|
transform: `scale(${zoomLevel / 100})`,
|
||||||
|
transformOrigin: 'bottom left',
|
||||||
|
pointerEvents: 'none'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<!-- Current sprite (draggable) -->
|
||||||
|
<img
|
||||||
|
v-for="(sprite, index) in spritesWithTempOffset"
|
||||||
|
:key="index"
|
||||||
|
:src="sprite.url"
|
||||||
|
alt="Sprite"
|
||||||
|
:class="{ 'cursor-move': currentFrame === index }"
|
||||||
|
:style="{
|
||||||
|
position: 'absolute',
|
||||||
|
left: `${(sprite.offset?.x || 0) * (zoomLevel / 100)}px`,
|
||||||
|
bottom: `${(sprite.offset?.y || 0) * (zoomLevel / 100)}px`,
|
||||||
|
display: currentFrame === index ? 'block' : 'none',
|
||||||
|
transform: `scale(${zoomLevel / 100})`,
|
||||||
|
transformOrigin: 'bottom left',
|
||||||
|
userSelect: 'none',
|
||||||
|
pointerEvents: currentFrame === index ? 'auto' : 'none'
|
||||||
|
}"
|
||||||
|
@dragstart.prevent
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-800 p-2 overflow-x-auto border-solid border-white/10 rounded mb-8 h-24 min-h-16">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<div
|
||||||
|
v-for="(sprite, index) in sprites"
|
||||||
|
:key="`thumb-${index}`"
|
||||||
|
class="relative cursor-pointer border-solid transition-all duration-200 rounded flex-shrink-0 p-3 px-12"
|
||||||
|
:class="currentFrame === index ? 'border-cyan-600 bg-cyan-500/10' : 'border-transparent hover:border-white/30'"
|
||||||
|
@click="selectFrame(index)"
|
||||||
|
>
|
||||||
|
<img :src="sprite.url" alt="Sprite thumbnail" class="h-16 w-auto object-contain rounded" />
|
||||||
|
<div class="absolute top-0 right-0 bg-gray-400 text-white text-xs font-default px-1 rounded-bl">
|
||||||
|
{{ index + 1 }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Sprite, SpriteImage } from '@/application/types'
|
||||||
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
sprite: Sprite
|
||||||
|
sprites: SpriteImage[]
|
||||||
|
frameRate: number
|
||||||
|
isModalOpen?: boolean
|
||||||
|
tempOffsetIndex?: number
|
||||||
|
tempOffset?: { x: number; y: number }
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:frameRate', value: number): void
|
||||||
|
(e: 'update:isModalOpen', value: boolean): void
|
||||||
|
(e: 'update:tempOffset', index: number, offset: { x: number; y: number }): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const currentFrame = ref(0)
|
||||||
|
const localFrameRate = ref(props.frameRate)
|
||||||
|
const zoomLevel = ref(100)
|
||||||
|
const isAnimating = ref(false)
|
||||||
|
const isDragging = ref(false)
|
||||||
|
const startDragPos = ref({ x: 0, y: 0 })
|
||||||
|
const currentOffset = ref({ x: 0, y: 0 })
|
||||||
|
let animationInterval: number | null = null
|
||||||
|
|
||||||
|
const totalDuration = computed(() => {
|
||||||
|
if (props.frameRate <= 0) return 0
|
||||||
|
return (props.sprites.length / props.frameRate).toFixed(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
const spritesWithTempOffset = computed(() => {
|
||||||
|
return props.sprites.map((sprite, index) => {
|
||||||
|
if (index === props.tempOffsetIndex && props.tempOffset) {
|
||||||
|
return { ...sprite, offset: props.tempOffset }
|
||||||
|
}
|
||||||
|
return sprite
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentSprite = computed(() => {
|
||||||
|
if (currentFrame.value >= 0 && currentFrame.value < spritesWithTempOffset.value.length) {
|
||||||
|
return spritesWithTempOffset.value[currentFrame.value]
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create computed properties with getters and setters for two-way binding
|
||||||
|
const offsetXModel = computed({
|
||||||
|
get: () => currentSprite.value?.offset?.x || 0,
|
||||||
|
set: (value) => {
|
||||||
|
if (isAnimating.value) return
|
||||||
|
const newOffset = {
|
||||||
|
x: value,
|
||||||
|
y: currentSprite.value?.offset?.y || 0
|
||||||
|
}
|
||||||
|
emit('update:tempOffset', currentFrame.value, newOffset)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const offsetYModel = computed({
|
||||||
|
get: () => currentSprite.value?.offset?.y || 0,
|
||||||
|
set: (value) => {
|
||||||
|
if (isAnimating.value) return
|
||||||
|
const newOffset = {
|
||||||
|
x: currentSprite.value?.offset?.x || 0,
|
||||||
|
y: value
|
||||||
|
}
|
||||||
|
emit('update:tempOffset', currentFrame.value, newOffset)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Toggle for showing reference sprites
|
||||||
|
const showReferenceSprites = ref(true)
|
||||||
|
|
||||||
|
function updateAnimation() {
|
||||||
|
stopAnimation()
|
||||||
|
if (props.frameRate <= 0 || props.sprites.length === 0) {
|
||||||
|
currentFrame.value = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAnimating.value) {
|
||||||
|
animationInterval = window.setInterval(() => {
|
||||||
|
currentFrame.value = (currentFrame.value + 1) % props.sprites.length
|
||||||
|
}, 1000 / props.frameRate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAnimation() {
|
||||||
|
isAnimating.value = !isAnimating.value
|
||||||
|
if (isAnimating.value) {
|
||||||
|
updateAnimation()
|
||||||
|
} else {
|
||||||
|
stopAnimation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopAnimation() {
|
||||||
|
if (animationInterval) {
|
||||||
|
clearInterval(animationInterval)
|
||||||
|
animationInterval = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectFrame(index: number) {
|
||||||
|
currentFrame.value = index
|
||||||
|
stopAnimation()
|
||||||
|
isAnimating.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFrameRate() {
|
||||||
|
emit('update:frameRate', localFrameRate.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
emit('update:isModalOpen', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function startDrag(event: MouseEvent) {
|
||||||
|
if (isAnimating.value) return
|
||||||
|
|
||||||
|
const previewContainer = event.currentTarget as HTMLElement
|
||||||
|
const rect = previewContainer.getBoundingClientRect()
|
||||||
|
|
||||||
|
// Store initial mouse position
|
||||||
|
startDragPos.value = {
|
||||||
|
x: event.clientX,
|
||||||
|
y: event.clientY
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store current offset
|
||||||
|
if (currentSprite.value && currentSprite.value.offset) {
|
||||||
|
currentOffset.value = {
|
||||||
|
x: currentSprite.value.offset.x,
|
||||||
|
y: currentSprite.value.offset.y
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currentOffset.value = { x: 0, y: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
isDragging.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrag(event: MouseEvent) {
|
||||||
|
if (!isDragging.value) return
|
||||||
|
|
||||||
|
// Calculate the difference from the start position
|
||||||
|
const deltaX = event.clientX - startDragPos.value.x
|
||||||
|
const deltaY = startDragPos.value.y - event.clientY // Inverted for bottom positioning
|
||||||
|
|
||||||
|
// Apply the zoom factor to the delta
|
||||||
|
// This ensures that the movement in screen pixels is converted to the correct
|
||||||
|
// number of pixels at the sprite's natural size, regardless of zoom level
|
||||||
|
const zoomFactor = 100 / zoomLevel.value
|
||||||
|
const scaledDeltaX = deltaX * zoomFactor
|
||||||
|
const scaledDeltaY = deltaY * zoomFactor
|
||||||
|
|
||||||
|
// Calculate new offset
|
||||||
|
// These offsets are in the sprite's natural coordinate space (as if zoom was 100%)
|
||||||
|
const newOffset = {
|
||||||
|
x: currentOffset.value.x + scaledDeltaX,
|
||||||
|
y: currentOffset.value.y + scaledDeltaY
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit the new offset
|
||||||
|
emit('update:tempOffset', currentFrame.value, newOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopDrag() {
|
||||||
|
if (isDragging.value && currentSprite.value?.offset) {
|
||||||
|
// Ensure the final offset is applied when dragging stops
|
||||||
|
emit('update:tempOffset', currentFrame.value, {
|
||||||
|
x: currentSprite.value.offset.x,
|
||||||
|
y: currentSprite.value.offset.y
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isDragging.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleReferenceSprites() {
|
||||||
|
showReferenceSprites.value = !showReferenceSprites.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateOffset(event: Event, axis: 'x' | 'y') {
|
||||||
|
if (isAnimating.value) return
|
||||||
|
|
||||||
|
const input = event.target as HTMLInputElement
|
||||||
|
const value = parseInt(input.value) || 0
|
||||||
|
|
||||||
|
if (currentSprite.value && currentSprite.value.offset) {
|
||||||
|
const newOffset = { ...currentSprite.value.offset }
|
||||||
|
newOffset[axis] = value
|
||||||
|
emit('update:tempOffset', currentFrame.value, newOffset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.frameRate,
|
||||||
|
(newValue) => {
|
||||||
|
localFrameRate.value = newValue
|
||||||
|
updateAnimation()
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(() => props.sprites, updateAnimation, { immediate: true })
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => isAnimating.value,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
updateAnimation()
|
||||||
|
} else {
|
||||||
|
stopAnimation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
isAnimating.value = props.frameRate > 0
|
||||||
|
if (isAnimating.value) {
|
||||||
|
updateAnimation()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopAnimation()
|
||||||
|
})
|
||||||
|
</script>
|
@ -1,185 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex flex-wrap gap-3">
|
|
||||||
<div v-for="(image, index) in modelValue" :key="index" class="h-20 w-20 p-4 bg-gray-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)" />
|
|
||||||
<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">{{ image.dimensions.width }}x{{ image.dimensions.height }}</div>
|
|
||||||
<div class="absolute top-1 left-1 flex-row space-y-1">
|
|
||||||
<button @click.stop="deleteImage(index)" class="bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" aria-label="Delete image">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button @click.stop="openOffsetModal(index)" class="bg-blue-500 text-white rounded-full w-6 h-6 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" aria-label="Scope image">
|
|
||||||
<svg width="50px" height="50px" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M8.29289 3.70711L1 11V15H5L12.2929 7.70711L8.29289 3.70711Z" fill="white" />
|
|
||||||
<path d="M9.70711 2.29289L13.7071 6.29289L15.1716 4.82843C15.702 4.29799 16 3.57857 16 2.82843C16 1.26633 14.7337 0 13.1716 0C12.4214 0 11.702 0.297995 11.1716 0.828428L9.70711 2.29289Z" fill="white" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Modal :is-modal-open="selectedImageIndex === index" :modal-width="300" :modal-height="210" :is-resizable="false" :bg-style="'none'" @modal:close="closeOffsetModal">
|
|
||||||
<template #modalHeader>
|
|
||||||
<h3 class="m-0 font-medium shrink-0 text-white">Action offset ({{ selectedImageIndex }})</h3>
|
|
||||||
</template>
|
|
||||||
<template #modalBody>
|
|
||||||
<div class="m-4">
|
|
||||||
<form method="post" @submit.prevent="saveOffset(index)" class="inline">
|
|
||||||
<div class="gap-2.5 flex flex-wrap">
|
|
||||||
<div class="form-field-half">
|
|
||||||
<label for="offsetX">Offset X</label>
|
|
||||||
<input class="input-field max-w-64" v-model="tempOffset.x" name="offsetX" id="offsetX" type="number" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field-half">
|
|
||||||
<label for="offsetY">Offset Y</label>
|
|
||||||
<input class="input-field max-w-64" v-model="tempOffset.y" name="offsetY" id="offsetY" type="number" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button class="btn-cyan px-4 py-1.5 min-w-24" type="submit">Save</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Modal>
|
|
||||||
</div>
|
|
||||||
<div class="h-20 w-20 p-4 bg-gray-200 bg-opacity-50 rounded justify-center items-center flex hover:cursor-pointer" @click="triggerFileInput" @drop.prevent="onDrop" @dragover.prevent>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 invert" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<input type="file" ref="fileInput" @change="onFileChange" multiple accept="image/png" class="hidden" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
|
||||||
import { ref, watch } from 'vue'
|
|
||||||
|
|
||||||
interface SpriteImage {
|
|
||||||
url: string
|
|
||||||
offset: {
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
modelValue: SpriteImage[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
modelValue: () => []
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:modelValue', value: SpriteImage[]): void
|
|
||||||
(e: 'close'): void
|
|
||||||
(e: 'tempOffsetChange', index: number, offset: { x: number; y: number }): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const fileInput = ref<HTMLInputElement | null>(null)
|
|
||||||
const draggedIndex = ref<number | null>(null)
|
|
||||||
const selectedImageIndex = ref<number | null>(null)
|
|
||||||
const tempOffset = ref({ x: 0, y: 0 })
|
|
||||||
|
|
||||||
const triggerFileInput = () => {
|
|
||||||
fileInput.value?.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
const onFileChange = (event: Event) => {
|
|
||||||
const target = event.target as HTMLInputElement
|
|
||||||
if (target.files) {
|
|
||||||
handleFiles(target.files)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onDrop = (event: DragEvent) => {
|
|
||||||
if (event.dataTransfer?.files) {
|
|
||||||
handleFiles(event.dataTransfer.files)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFiles = (files: FileList) => {
|
|
||||||
Array.from(files).forEach((file) => {
|
|
||||||
if (!file.type.startsWith('image/')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.onload = (e) => {
|
|
||||||
if (typeof e.target?.result === 'string') {
|
|
||||||
const newImage: SpriteImage = {
|
|
||||||
url: e.target.result,
|
|
||||||
offset: { x: 0, y: 0 }
|
|
||||||
}
|
|
||||||
updateImages([...props.modelValue, newImage])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reader.readAsDataURL(file)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateImages = (newImages: SpriteImage[]) => {
|
|
||||||
emit('update:modelValue', newImages)
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteImage = (index: number) => {
|
|
||||||
const newImages = [...props.modelValue]
|
|
||||||
newImages.splice(index, 1)
|
|
||||||
updateImages(newImages)
|
|
||||||
}
|
|
||||||
|
|
||||||
const dragStart = (event: DragEvent, index: number) => {
|
|
||||||
draggedIndex.value = index
|
|
||||||
if (event.dataTransfer) {
|
|
||||||
event.dataTransfer.effectAllowed = 'move'
|
|
||||||
event.dataTransfer.dropEffect = 'move'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const drop = (event: DragEvent, dropIndex: number) => {
|
|
||||||
event.preventDefault()
|
|
||||||
if (draggedIndex.value !== null && draggedIndex.value !== dropIndex) {
|
|
||||||
const newImages = [...props.modelValue]
|
|
||||||
const [reorderedItem] = newImages.splice(draggedIndex.value, 1)
|
|
||||||
newImages.splice(dropIndex, 0, reorderedItem)
|
|
||||||
updateImages(newImages)
|
|
||||||
}
|
|
||||||
draggedIndex.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const openOffsetModal = (index: number) => {
|
|
||||||
selectedImageIndex.value = index
|
|
||||||
tempOffset.value = { ...props.modelValue[index].offset }
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeOffsetModal = () => {
|
|
||||||
selectedImageIndex.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveOffset = (index: number) => {
|
|
||||||
const newImages = [...props.modelValue]
|
|
||||||
newImages[index] = {
|
|
||||||
...newImages[index],
|
|
||||||
offset: { ...tempOffset.value }
|
|
||||||
}
|
|
||||||
updateImages(newImages)
|
|
||||||
closeOffsetModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
const onOffsetChange = () => {
|
|
||||||
if (selectedImageIndex.value !== null) {
|
|
||||||
emit('tempOffsetChange', selectedImageIndex.value, tempOffset.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(tempOffset, onOffsetChange, { deep: true })
|
|
||||||
|
|
||||||
const imageDimensions = ref<{ [key: number]: { width: number; height: number } }>({})
|
|
||||||
|
|
||||||
const updateImageDimensions = (event: Event, index: number) => {
|
|
||||||
const img = event.target as HTMLImageElement
|
|
||||||
imageDimensions.value[index] = {
|
|
||||||
width: img.naturalWidth,
|
|
||||||
height: img.naturalHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -1,146 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Modal :is-modal-open="isModalOpen" :modal-width="700" :modal-height="330" :bg-style="'none'" @modal:close="closeModal">
|
|
||||||
<template #modalHeader>
|
|
||||||
<h3 class="m-0 font-medium shrink-0 text-white">View sprite</h3>
|
|
||||||
</template>
|
|
||||||
<template #modalBody>
|
|
||||||
<div class="m-4 flex gap-8">
|
|
||||||
<div class="relative">
|
|
||||||
<div
|
|
||||||
class="sprite-container bg-gray-800"
|
|
||||||
:style="{
|
|
||||||
width: `${maxWidth}px`,
|
|
||||||
height: `${maxHeight}px`,
|
|
||||||
position: 'relative',
|
|
||||||
overflow: 'hidden'
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
v-for="(sprite, index) in spritesWithTempOffset"
|
|
||||||
:key="index"
|
|
||||||
:src="sprite.url"
|
|
||||||
alt="Sprite"
|
|
||||||
:style="{
|
|
||||||
position: 'absolute',
|
|
||||||
left: `${sprite.offset?.x || 0}px`,
|
|
||||||
bottom: `${sprite.offset?.y || 0}px`,
|
|
||||||
display: currentFrame === index ? 'block' : 'none',
|
|
||||||
transform: `scale(${zoomLevel / 100})`,
|
|
||||||
transformOrigin: 'bottom left'
|
|
||||||
}"
|
|
||||||
@load="updateContainerSize"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col justify-center gap-8 flex-1">
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<label class="block mb-2 text-white">Frame Rate: {{ frameRate }} FPS</label>
|
|
||||||
<input type="range" v-model.number="localFrameRate" min="0" max="60" step="1" class="w-full accent-cyan-500" @input="updateFrameRate" />
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<label class="block mb-2 text-white">Frame: {{ currentFrame + 1 }} of {{ sprites.length }}</label>
|
|
||||||
<input type="range" v-model.number="currentFrame" :min="0" :max="sprites.length - 1" step="1" class="w-full accent-cyan-500" @input="stopAnimation" />
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<label class="block mb-2 text-white">Zoom: {{ zoomLevel }}%</label>
|
|
||||||
<input type="range" v-model.number="zoomLevel" min="10" max="200" step="10" class="w-full accent-cyan-500" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Modal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { SpriteImage } from '@/application/types'
|
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
|
||||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
sprites: SpriteImage[]
|
|
||||||
frameRate: number
|
|
||||||
isModalOpen?: boolean
|
|
||||||
tempOffsetIndex?: number
|
|
||||||
tempOffset?: { x: number; y: number }
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:frameRate', value: number): void
|
|
||||||
(e: 'update:isModalOpen', value: boolean): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const currentFrame = ref(0)
|
|
||||||
const maxWidth = ref(250)
|
|
||||||
const maxHeight = ref(250)
|
|
||||||
const localFrameRate = ref(props.frameRate)
|
|
||||||
const zoomLevel = ref(100)
|
|
||||||
let animationInterval: number | null = null
|
|
||||||
|
|
||||||
const spritesWithTempOffset = computed(() => {
|
|
||||||
return props.sprites.map((sprite, index) => {
|
|
||||||
if (index === props.tempOffsetIndex && props.tempOffset) {
|
|
||||||
return { ...sprite, offset: props.tempOffset }
|
|
||||||
}
|
|
||||||
return sprite
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
function updateContainerSize(event: Event) {
|
|
||||||
const img = event.target as HTMLImageElement
|
|
||||||
maxWidth.value = Math.max(maxWidth.value, img.naturalWidth)
|
|
||||||
maxHeight.value = Math.max(maxHeight.value, img.naturalHeight)
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateAnimation() {
|
|
||||||
stopAnimation()
|
|
||||||
if (props.frameRate <= 0 || props.sprites.length === 0) {
|
|
||||||
currentFrame.value = 0
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
animationInterval = window.setInterval(() => {
|
|
||||||
currentFrame.value = (currentFrame.value + 1) % props.sprites.length
|
|
||||||
}, 1000 / props.frameRate)
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopAnimation() {
|
|
||||||
if (animationInterval) {
|
|
||||||
clearInterval(animationInterval)
|
|
||||||
animationInterval = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateFrameRate() {
|
|
||||||
emit('update:frameRate', localFrameRate.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeModal() {
|
|
||||||
emit('update:isModalOpen', false)
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.frameRate,
|
|
||||||
(newValue) => {
|
|
||||||
localFrameRate.value = newValue
|
|
||||||
updateAnimation()
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(() => props.sprites, updateAnimation, { immediate: true })
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
updateAnimation()
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
stopAnimation()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.sprite-container {
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -24,16 +24,16 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
|
import { SocketEvent } from '@/application/enums'
|
||||||
import type { Tile } from '@/application/types'
|
import type { Tile } from '@/application/types'
|
||||||
|
import { downloadCache } from '@/application/utilities'
|
||||||
import ChipsInput from '@/components/forms/ChipsInput.vue'
|
import ChipsInput from '@/components/forms/ChipsInput.vue'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { TileStorage } from '@/storage/storages'
|
import { TileStorage } from '@/storage/storages'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue'
|
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const assetManagerStore = useAssetManagerStore()
|
const assetManagerStore = useAssetManagerStore()
|
||||||
const tileStorage = new TileStorage()
|
|
||||||
|
|
||||||
const selectedTile = computed(() => assetManagerStore.selectedTile)
|
const selectedTile = computed(() => assetManagerStore.selectedTile)
|
||||||
|
|
||||||
@ -56,18 +56,19 @@ watch(selectedTile, (tile: Tile | null) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function deleteTile() {
|
async function deleteTile() {
|
||||||
gameStore.connection?.emit('gm:tile:delete', { id: selectedTile.value?.id }, async (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_TILE_DELETE, { id: selectedTile.value?.id }, async (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to delete tile')
|
console.error('Failed to delete tile')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await tileStorage.delete(selectedTile.value!.id)
|
|
||||||
refreshTileList()
|
await downloadCache('tiles', new TileStorage())
|
||||||
|
await refreshTileList()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshTileList(unsetSelectedTile = true) {
|
async function refreshTileList(unsetSelectedTile = true) {
|
||||||
gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => {
|
socketManager.emit(SocketEvent.GM_TILE_LIST, {}, (response: Tile[]) => {
|
||||||
assetManagerStore.setTileList(response)
|
assetManagerStore.setTileList(response)
|
||||||
|
|
||||||
if (unsetSelectedTile) {
|
if (unsetSelectedTile) {
|
||||||
@ -76,25 +77,27 @@ function refreshTileList(unsetSelectedTile = true) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveTile() {
|
async function saveTile() {
|
||||||
if (!selectedTile.value) {
|
if (!selectedTile.value) {
|
||||||
console.error('No tile selected')
|
console.error('No tile selected')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit(
|
socketManager.emit(
|
||||||
'gm:tile:update',
|
'gm:tile:update',
|
||||||
{
|
{
|
||||||
id: selectedTile.value.id,
|
id: selectedTile.value.id,
|
||||||
name: tileName.value,
|
name: tileName.value,
|
||||||
tags: tileTags.value
|
tags: tileTags.value
|
||||||
},
|
},
|
||||||
(response: boolean) => {
|
async (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Failed to save tile')
|
console.error('Failed to save tile')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
refreshTileList(false)
|
|
||||||
|
await downloadCache('tiles', new TileStorage())
|
||||||
|
await refreshTileList(false)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,9 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
|
import { SocketEvent } from '@/application/enums'
|
||||||
import type { Tile } from '@/application/types'
|
import type { Tile } from '@/application/types'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
import { useAssetManagerStore } from '@/stores/assetManagerStore'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useVirtualList } from '@vueuse/core'
|
import { useVirtualList } from '@vueuse/core'
|
||||||
@ -47,13 +49,13 @@ const elementToScroll = ref()
|
|||||||
const handleFileUpload = (e: Event) => {
|
const handleFileUpload = (e: Event) => {
|
||||||
const files = (e.target as HTMLInputElement).files
|
const files = (e.target as HTMLInputElement).files
|
||||||
if (!files) return
|
if (!files) return
|
||||||
gameStore.connection?.emit('gm:tile:upload', files, (response: boolean) => {
|
socketManager.emit(SocketEvent.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
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => {
|
socketManager.emit(SocketEvent.GM_TILE_LIST, {}, (response: Tile[]) => {
|
||||||
assetManagerStore.setTileList(response)
|
assetManagerStore.setTileList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -92,7 +94,7 @@ function toTop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
gameStore.connection?.emit('gm:tile:list', {}, (response: Tile[]) => {
|
socketManager.emit(SocketEvent.GM_TILE_LIST, {}, (response: Tile[]) => {
|
||||||
assetManagerStore.setTileList(response)
|
assetManagerStore.setTileList(response)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,46 +1,245 @@
|
|||||||
<template>
|
<template>
|
||||||
<MapTiles ref="mapTiles" @tileMap:create="tileMap = $event" />
|
<MapTiles ref="mapTiles" @createCommand="addCommand" v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer />
|
||||||
<PlacedMapObjects ref="mapObjects" v-if="tileMap" :tileMap="tileMap as Phaser.Tilemaps.Tilemap" />
|
<PlacedMapObjects ref="mapObjects" @update="updateMapObjects" @updateAndCommit="updateAndCommit" @pauseObjectTracking="pause" @resumeObjectTracking="resume" v-if="tileMap && tileMapLayer" :tileMap :tileMapLayer />
|
||||||
<MapEventTiles ref="eventTiles" v-if="tileMap" :tileMap="tileMap as Phaser.Tilemaps.Tilemap" />
|
<MapEventTiles ref="eventTiles" @createCommand="addCommand" v-if="tileMap" :tileMap />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { MapEventTile, Map as MapT, PlacedMapObject as PlacedMapObjectT } from '@/application/types'
|
||||||
import MapEventTiles from '@/components/gameMaster/mapEditor/mapPartials/MapEventTiles.vue'
|
import MapEventTiles from '@/components/gameMaster/mapEditor/mapPartials/MapEventTiles.vue'
|
||||||
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 { cloneArray, createTileLayer, createTileMap, placeTiles } from '@/services/mapService'
|
||||||
|
import { TileStorage } from '@/storage/storages'
|
||||||
|
import { useRefHistory } from '@vueuse/core'
|
||||||
import { useScene } from 'phavuer'
|
import { useScene } from 'phavuer'
|
||||||
import { onMounted, onUnmounted, shallowRef, useTemplateRef } from 'vue'
|
import { onBeforeUnmount, onMounted, onUnmounted, ref, shallowRef, useTemplateRef, watch } from 'vue'
|
||||||
|
|
||||||
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
|
const tileMap = shallowRef<Phaser.Tilemaps.Tilemap>()
|
||||||
const mapEditor = useMapEditorComposable()
|
const tileMapLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
|
||||||
|
|
||||||
|
const mapEditor = useMapEditorComposable()
|
||||||
const scene = useScene()
|
const scene = useScene()
|
||||||
|
|
||||||
const mapTiles = useTemplateRef('mapTiles')
|
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) {
|
//Record of commands
|
||||||
|
let commandStack: (EditorCommand | number)[] = []
|
||||||
|
let commandIndex = ref(0)
|
||||||
|
|
||||||
|
let originTiles: string[][] = []
|
||||||
|
let originEventTiles: MapEventTile[] = []
|
||||||
|
let originObjects = ref<PlacedMapObjectT[]>(mapEditor.currentMap.value?.placedMapObjects ?? [])
|
||||||
|
|
||||||
|
const { undo, redo, commit, pause, resume, canUndo, canRedo } = useRefHistory(originObjects, { deep: true, capacity: 9 })
|
||||||
|
|
||||||
|
//Command Pattern basic interface, extended to store what elements have been changed by each edit
|
||||||
|
export interface EditorCommand {
|
||||||
|
apply: (elements: any[]) => any[]
|
||||||
|
type: 'tile' | 'map_object' | 'event_tile'
|
||||||
|
operation: 'draw' | 'erase' | 'clear'
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyCommands(tiles: any[], ...commands: EditorCommand[]): any[] {
|
||||||
|
let tileVersion = cloneArray(tiles)
|
||||||
|
for (let command of commands) {
|
||||||
|
tileVersion = command.apply(tileVersion)
|
||||||
|
}
|
||||||
|
return tileVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => mapEditor.shouldClearTiles.value,
|
||||||
|
(shouldClear) => {
|
||||||
|
if (shouldClear && mapEditor.currentMap.value) {
|
||||||
|
mapTiles.value!.clearTiles()
|
||||||
|
eventTiles.value!.clearTiles()
|
||||||
|
mapEditor.currentMap.value.placedMapObjects = []
|
||||||
|
mapEditor.currentMap.value.mapEventTiles = []
|
||||||
|
updateAndCommit(mapEditor.currentMap.value)
|
||||||
|
mapEditor.resetClearTilesFlag()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function update(commands: (EditorCommand | number)[]) {
|
||||||
|
if (!mapEditor.currentMap.value) return
|
||||||
|
|
||||||
|
if (commandStack.length >= 9) {
|
||||||
|
if (typeof commandStack[0] !== 'number') {
|
||||||
|
const base = commandStack.shift() as EditorCommand
|
||||||
|
if (base.operation !== 'clear') {
|
||||||
|
switch (base.type) {
|
||||||
|
case 'tile':
|
||||||
|
originTiles = base.apply(originTiles) as string[][]
|
||||||
|
break
|
||||||
|
case 'event_tile':
|
||||||
|
originEventTiles = base.apply(originEventTiles) as MapEventTile[]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
commandStack.shift()
|
||||||
|
}
|
||||||
|
} else if (typeof commandStack[0] === 'number') {
|
||||||
|
commandStack.shift()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let tileCommands = commands.filter((item) => typeof item !== 'number' && item.type === 'tile') as EditorCommand[]
|
||||||
|
let eventTileCommands = commands.filter((item) => typeof item !== 'number' && item.type === 'event_tile') as EditorCommand[]
|
||||||
|
|
||||||
|
let modifiedTiles = applyCommands(originTiles, ...tileCommands)
|
||||||
|
placeTiles(tileMap.value!, tileMapLayer.value!, modifiedTiles)
|
||||||
|
|
||||||
|
let eventTiles = applyCommands(originEventTiles, ...eventTileCommands)
|
||||||
|
|
||||||
|
mapEditor.currentMap.value.tiles = modifiedTiles
|
||||||
|
mapEditor.currentMap.value.mapEventTiles = eventTiles
|
||||||
|
mapEditor.currentMap.value.placedMapObjects = originObjects.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMapObjects(map: MapT) {
|
||||||
|
originObjects.value = map.placedMapObjects
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAndCommit(map?: MapT) {
|
||||||
|
commandStack = commandStack.slice(0, commandIndex.value)
|
||||||
|
if (map) updateMapObjects(map)
|
||||||
|
commit()
|
||||||
|
commandStack.push(0)
|
||||||
|
commandIndex.value = commandStack.length
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCommand(command: EditorCommand) {
|
||||||
|
commandStack = commandStack.slice(0, commandIndex.value)
|
||||||
|
commandStack.push(command)
|
||||||
|
commandIndex.value = commandStack.length
|
||||||
|
}
|
||||||
|
|
||||||
|
function undoEdit() {
|
||||||
|
if (commandIndex.value > 0) {
|
||||||
|
if (typeof commandStack[--commandIndex.value] === 'number' && canUndo) {
|
||||||
|
undo()
|
||||||
|
}
|
||||||
|
update(commandStack.slice(0, commandIndex.value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function redoEdit() {
|
||||||
|
if (commandIndex.value <= 9 && commandIndex.value < commandStack.length) {
|
||||||
|
if (typeof commandStack[commandIndex.value++] === 'number' && canRedo) {
|
||||||
|
redo()
|
||||||
|
}
|
||||||
|
update(commandStack.slice(0, commandIndex.value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
if (!pointer.isDown) return
|
if (!pointer.isDown) return
|
||||||
|
|
||||||
// Check if shift is pressed or if we're in move mode, this means we are moving the camera
|
// Check if shift is not pressed, this means we are moving the camera
|
||||||
if (pointer.event.shiftKey || mapEditor.tool.value === 'move') return
|
if (pointer.event.shiftKey) return
|
||||||
|
|
||||||
// Check if draw mode is tile
|
// Check if draw mode is tile
|
||||||
switch (mapEditor.drawMode.value) {
|
switch (mapEditor.drawMode.value) {
|
||||||
case 'tile':
|
case 'tile':
|
||||||
mapTiles.value.handlePointer(pointer)
|
mapTiles.value.handlePointer(pointer)
|
||||||
break
|
break
|
||||||
case 'object':
|
case 'map_object':
|
||||||
mapObjects.value.handlePointer(pointer)
|
mapObjects.value.handlePointer(pointer)
|
||||||
break
|
break
|
||||||
case 'event':
|
case 'teleport':
|
||||||
|
eventTiles.value.handlePointer(pointer)
|
||||||
|
break
|
||||||
|
case 'blocking tile':
|
||||||
eventTiles.value.handlePointer(pointer)
|
eventTiles.value.handlePointer(pointer)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleKeyDown(event: KeyboardEvent) {
|
||||||
|
//CTRL+Y
|
||||||
|
if (event.key === 'y' && event.ctrlKey) {
|
||||||
|
redoEdit()
|
||||||
|
}
|
||||||
|
|
||||||
|
//CTRL+Z
|
||||||
|
if (event.key === 'z' && event.ctrlKey) {
|
||||||
|
undoEdit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerMove(pointer: Phaser.Input.Pointer) {
|
||||||
|
if (mapEditor.inputMode.value === 'hold' && pointer.isDown) {
|
||||||
|
handlePointerDown(pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerUp(pointer: Phaser.Input.Pointer) {
|
||||||
|
switch (mapEditor.drawMode.value) {
|
||||||
|
case 'tile':
|
||||||
|
mapTiles.value!.finalizeCommand()
|
||||||
|
break
|
||||||
|
case 'map_object':
|
||||||
|
if (mapEditor.tool.value === 'pencil' || mapEditor.tool.value === 'eraser') {
|
||||||
|
resume()
|
||||||
|
updateAndCommit()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'teleport':
|
||||||
|
eventTiles.value!.finalizeCommand()
|
||||||
|
break
|
||||||
|
case 'blocking tile':
|
||||||
|
eventTiles.value!.finalizeCommand()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
let mapValue = mapEditor.currentMap.value
|
||||||
|
if (!mapValue) return
|
||||||
|
|
||||||
|
//Clone
|
||||||
|
originTiles = cloneArray(mapValue.tiles)
|
||||||
|
originEventTiles = cloneArray(mapValue.mapEventTiles)
|
||||||
|
|
||||||
|
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(() => {
|
||||||
|
if (tileMap.value) {
|
||||||
|
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()
|
||||||
|
})
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
scene.children.queueDepthSort()
|
||||||
|
}, 0.2)
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
removeEventListener('keydown', handleKeyDown)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -3,22 +3,71 @@
|
|||||||
</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 { type EditorCommand } from '@/components/gameMaster/mapEditor/Map.vue'
|
||||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
import { Image, useScene } from 'phavuer'
|
import { cloneArray, getTile, tileToWorldX, tileToWorldY } from '@/services/mapService'
|
||||||
import { shallowRef } from 'vue'
|
import { Image } from 'phavuer'
|
||||||
|
|
||||||
const mapEditor = useMapEditorComposable()
|
const mapEditor = useMapEditorComposable()
|
||||||
|
|
||||||
defineExpose({ handlePointer })
|
defineExpose({ handlePointer, finalizeCommand, clearTiles })
|
||||||
|
|
||||||
|
const emit = defineEmits(['createCommand'])
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
tileMap: Phaser.Tilemaps.Tilemap
|
tileMap: Phaser.Tilemaps.Tilemap
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const tileLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
|
// *** COMMAND STATE ***
|
||||||
|
|
||||||
|
let currentCommand: EventTileCommand | null = null
|
||||||
|
|
||||||
|
class EventTileCommand implements EditorCommand {
|
||||||
|
public operation: 'draw' | 'erase' | 'clear' = 'draw'
|
||||||
|
public type: 'event_tile' = 'event_tile'
|
||||||
|
public affectedTiles: MapEventTile[] = []
|
||||||
|
|
||||||
|
apply(elements: MapEventTile[]) {
|
||||||
|
let tileVersion = cloneArray(elements) as MapEventTile[]
|
||||||
|
if (this.operation === 'draw') {
|
||||||
|
tileVersion = tileVersion.concat(this.affectedTiles)
|
||||||
|
} else if (this.operation === 'erase') {
|
||||||
|
tileVersion = tileVersion.filter((v) => !this.affectedTiles.includes(v))
|
||||||
|
} else if (this.operation === 'clear') {
|
||||||
|
tileVersion = []
|
||||||
|
}
|
||||||
|
return tileVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(operation: 'draw' | 'erase' | 'clear') {
|
||||||
|
this.operation = operation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCommandUpdate(tile?: MapEventTile | null, operation: 'draw' | 'erase' | 'clear' = 'draw') {
|
||||||
|
if (!tile) return
|
||||||
|
|
||||||
|
if (!currentCommand) {
|
||||||
|
currentCommand = new EventTileCommand(operation)
|
||||||
|
}
|
||||||
|
|
||||||
|
//If position is already in, do not proceed
|
||||||
|
for (const priorTile of currentCommand.affectedTiles) {
|
||||||
|
if (priorTile.positionX === tile.positionX && priorTile.positionY == tile.positionY) return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentCommand.affectedTiles.push(tile)
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalizeCommand() {
|
||||||
|
if (!currentCommand) return
|
||||||
|
emit('createCommand', currentCommand)
|
||||||
|
currentCommand = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// *** HANDLERS ***
|
||||||
|
|
||||||
function getImageProps(tile: MapEventTile) {
|
function getImageProps(tile: MapEventTile) {
|
||||||
return {
|
return {
|
||||||
@ -30,10 +79,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
|
||||||
@ -41,19 +88,17 @@ function pencil(pointer: Phaser.Input.Pointer, map: MapT) {
|
|||||||
if (existingEventTile) return
|
if (existingEventTile) return
|
||||||
|
|
||||||
// If teleport, check if there is a selected map
|
// If teleport, check if there is a selected map
|
||||||
if (mapEditor.drawMode.value === 'teleport' && !mapEditor.teleportSettings.value.toMapId) return
|
if (mapEditor.drawMode.value === 'teleport' && !mapEditor.teleportSettings.value.toMap) return
|
||||||
|
|
||||||
const newEventTile = {
|
const newEventTile = {
|
||||||
id: uuidv4(),
|
id: uuidv4() as UUID,
|
||||||
mapId: 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,
|
||||||
teleport:
|
teleport:
|
||||||
mapEditor.drawMode.value === 'teleport'
|
mapEditor.drawMode.value === 'teleport'
|
||||||
? {
|
? {
|
||||||
toMap: mapEditor.teleportSettings.value.toMapId,
|
toMap: mapEditor.teleportSettings.value.toMap,
|
||||||
toPositionX: mapEditor.teleportSettings.value.toPositionX,
|
toPositionX: mapEditor.teleportSettings.value.toPositionX,
|
||||||
toPositionY: mapEditor.teleportSettings.value.toPositionY,
|
toPositionY: mapEditor.teleportSettings.value.toPositionY,
|
||||||
toRotation: mapEditor.teleportSettings.value.toRotation
|
toRotation: mapEditor.teleportSettings.value.toRotation
|
||||||
@ -61,19 +106,28 @@ function pencil(pointer: Phaser.Input.Pointer, map: MapT) {
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
map!.mapEventTiles = map!.mapEventTiles.concat(newEventTile as MapEventTile)
|
createCommandUpdate(newEventTile as MapEventTile, 'draw')
|
||||||
|
|
||||||
|
map.mapEventTiles.push(newEventTile as MapEventTile)
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
}
|
||||||
|
|
||||||
|
createCommandUpdate(existingEventTile, 'erase')
|
||||||
|
|
||||||
// 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 +136,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':
|
||||||
@ -97,4 +147,10 @@ function handlePointer(pointer: Phaser.Input.Pointer) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearTiles() {
|
||||||
|
if (mapEditor.currentMap.value?.mapEventTiles.length === 0) return
|
||||||
|
createCommandUpdate(null, 'clear')
|
||||||
|
finalizeCommand()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,109 +1,102 @@
|
|||||||
<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 EditorCommand } from '@/components/gameMaster/mapEditor/Map.vue'
|
||||||
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 { cloneArray, 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, clearTiles })
|
||||||
const tileLayer = shallowRef<Phaser.Tilemaps.TilemapLayer>()
|
|
||||||
|
|
||||||
defineExpose({ handlePointer })
|
const emit = defineEmits(['createCommand'])
|
||||||
|
|
||||||
function createTileMap() {
|
const props = defineProps<{
|
||||||
const mapData = new Phaser.Tilemaps.MapData({
|
tileMap: Phaser.Tilemaps.Tilemap
|
||||||
width: mapEditor.currentMap.value?.width,
|
tileMapLayer: Phaser.Tilemaps.TilemapLayer
|
||||||
height: mapEditor.currentMap.value?.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, mapData)
|
// *** COMMAND STATE ***
|
||||||
emit('tileMap:create', newTileMap)
|
|
||||||
return newTileMap
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createTileLayer(currentTileMap: Phaser.Tilemaps.Tilemap) {
|
let currentCommand: TileCommand | null = null
|
||||||
const tiles = await tileStorage.getAll()
|
|
||||||
const tilesetImages = []
|
|
||||||
|
|
||||||
for (const tile of tiles) {
|
class TileCommand implements EditorCommand {
|
||||||
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 }))
|
public operation: 'draw' | 'erase' | 'clear' = 'draw'
|
||||||
|
public type: 'tile' = 'tile'
|
||||||
|
public tileName: string = 'blank_tile'
|
||||||
|
public affectedTiles: number[][] = []
|
||||||
|
|
||||||
|
apply(elements: string[][]) {
|
||||||
|
let tileVersion
|
||||||
|
if (this.operation === 'clear') {
|
||||||
|
tileVersion = createTileArray(props.tileMapLayer.width, props.tileMapLayer.height, 'blank_tile')
|
||||||
|
} else {
|
||||||
|
tileVersion = cloneArray(elements) as string[][]
|
||||||
|
for (const position of this.affectedTiles) {
|
||||||
|
tileVersion[position[1]][position[0]] = this.tileName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tileVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add blank tile
|
constructor(operation: 'draw' | 'erase' | 'clear', tileName: string) {
|
||||||
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 }))
|
this.operation = operation
|
||||||
|
this.tileName = tileName
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function pencil(pointer: Phaser.Input.Pointer) {
|
function createCommandUpdate(x: number, y: number, tileName: string, operation: 'draw' | 'erase' | 'clear') {
|
||||||
|
if (!currentCommand) {
|
||||||
|
currentCommand = new TileCommand(operation, tileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
//If position is already in, do not proceed
|
||||||
|
for (const vec of currentCommand.affectedTiles) {
|
||||||
|
if (vec[0] === x && vec[1] === y) return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentCommand.affectedTiles.push([x, y])
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalizeCommand() {
|
||||||
|
if (!currentCommand) return
|
||||||
|
emit('createCommand', currentCommand)
|
||||||
|
currentCommand = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// *** HANDLERS ***
|
||||||
|
|
||||||
|
function draw(pointer: Phaser.Input.Pointer, tileName: string) {
|
||||||
let map = mapEditor.currentMap.value
|
let map = mapEditor.currentMap.value
|
||||||
if (!map) return
|
if (!map) return
|
||||||
|
|
||||||
// Check if there is a selected tile
|
|
||||||
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, tileName)
|
||||||
|
|
||||||
|
createCommandUpdate(tile.x, tile.y, tileName, tileName === 'blank_tile' ? 'erase' : 'draw')
|
||||||
|
|
||||||
// Adjust mapEditorStore.map.tiles
|
// Adjust mapEditorStore.map.tiles
|
||||||
map.tiles[tile.y][tile.x] = mapEditor.selectedTile.value
|
map.tiles[tile.y][tile.x] = tileName
|
||||||
}
|
|
||||||
|
|
||||||
function eraser(pointer: Phaser.Input.Pointer) {
|
|
||||||
let map = mapEditor.currentMap.value
|
|
||||||
if (!map) return
|
|
||||||
|
|
||||||
if (!tileMap.value || !tileLayer.value) return
|
|
||||||
|
|
||||||
// Check if there is a tile
|
|
||||||
const tile = getTile(tileLayer.value, pointer.worldX, pointer.worldY)
|
|
||||||
if (!tile) return
|
|
||||||
|
|
||||||
// Place tile
|
|
||||||
placeTile(tileMap.value, tileLayer.value, tile.x, tile.y, 'blank_tile')
|
|
||||||
|
|
||||||
// Adjust mapEditorStore.map.tiles
|
|
||||||
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 +104,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
|
||||||
@ -151,10 +128,10 @@ function handlePointer(pointer: Phaser.Input.Pointer) {
|
|||||||
// Check if draw mode is tile
|
// Check if draw mode is tile
|
||||||
switch (mapEditor.tool.value) {
|
switch (mapEditor.tool.value) {
|
||||||
case 'pencil':
|
case 'pencil':
|
||||||
pencil(pointer)
|
draw(pointer, mapEditor.selectedTile.value!)
|
||||||
break
|
break
|
||||||
case 'eraser':
|
case 'eraser':
|
||||||
eraser(pointer)
|
draw(pointer, 'blank_tile')
|
||||||
break
|
break
|
||||||
case 'paint':
|
case 'paint':
|
||||||
paint(pointer)
|
paint(pointer)
|
||||||
@ -162,33 +139,19 @@ function handlePointer(pointer: Phaser.Input.Pointer) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// *** LIFECYCLE ***
|
||||||
|
|
||||||
|
function clearTiles() {
|
||||||
|
const tileArray = createTileArray(props.tileMap.width, props.tileMap.height, 'blank_tile')
|
||||||
|
placeTiles(props.tileMap, props.tileMapLayer, tileArray)
|
||||||
|
createCommandUpdate(0, 0, 'blank_tile', 'clear')
|
||||||
|
finalizeCommand()
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!mapEditor.currentMap.value) return
|
if (!mapEditor.currentMap.value) return
|
||||||
|
const mapState = mapEditor.currentMap.value
|
||||||
|
|
||||||
tileMap.value = createTileMap()
|
placeTiles(props.tileMap, props.tileMapLayer, mapState.tiles)
|
||||||
tileLayer.value = await createTileLayer(tileMap.value)
|
|
||||||
|
|
||||||
// First fill the entire map with blank tiles using current map dimensions
|
|
||||||
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
|
|
||||||
const mapTiles = mapEditor.currentMap.value.tiles
|
|
||||||
for (let y = 0; y < mapEditor.currentMap.value.height; y++) {
|
|
||||||
for (let x = 0; x < mapEditor.currentMap.value.width; x++) {
|
|
||||||
if (mapTiles[y] && mapTiles[y][x] !== undefined) {
|
|
||||||
blankTiles[y][x] = mapTiles[y][x]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setLayerTiles(tileMap.value, tileLayer.value, blankTiles)
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (tileMap.value) {
|
|
||||||
tileMap.value.destroyLayer('tiles')
|
|
||||||
tileMap.value.removeAllLayers()
|
|
||||||
tileMap.value.destroy()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
</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,173 @@
|
|||||||
<template>
|
<template>
|
||||||
<SelectedPlacedMapObjectComponent v-if="selectedPlacedMapObject" :placedMapObject="selectedPlacedMapObject" @move="moveMapObject" @rotate="rotatePlacedMapObject" @delete="deletePlacedMapObject" />
|
<PlacedMapObject
|
||||||
<PlacedMapObject v-for="placedMapObject in mapEditor.currentMap.value?.placedMapObjects" :tileMap="tileMap" :placedMapObject :selectedPlacedMapObject :movingPlacedMapObject @pointerup="clickPlacedMapObject(placedMapObject)" />
|
v-if="mapEditor.tool.value === 'pencil' && mapEditor.drawMode.value === 'map_object' && mapEditor.isPlacedMapObjectPreviewEnabled.value && mapEditor.selectedMapObject.value && previewPlacedMapObject"
|
||||||
|
:tileMap
|
||||||
|
:tileMapLayer
|
||||||
|
:key="previewPlacedMapObject?.id"
|
||||||
|
:placedMapObject="previewPlacedMapObject as PlacedMapObjectT"
|
||||||
|
/>
|
||||||
|
<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 :tileMapLayer :placedMapObject @pointerdown="clickPlacedMapObject(placedMapObject)" :key="`${placedMapObject.id}-${placedMapObjectKey}`" />
|
||||||
</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, onMounted, onUnmounted, ref, watch } 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 placedMapObjectKey = computed(() => mapEditor.refreshMapObject.value)
|
||||||
|
|
||||||
const props = defineProps<{
|
const emit = defineEmits<{ (e: 'update', map: MapT): void; (e: 'updateAndCommit', map: MapT): void; (e: 'pauseObjectTracking'): void; (e: 'resumeObjectTracking'): void }>()
|
||||||
tileMap: Phaser.Tilemaps.Tilemap
|
|
||||||
}>()
|
|
||||||
|
|
||||||
defineExpose({ handlePointer })
|
defineExpose({ handlePointer })
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
tileMap: Tilemap
|
||||||
|
tileMapLayer: TilemapLayer
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const previewPosition = ref({ x: 0, y: 0 })
|
||||||
|
const previewPlacedMapObject = computed(() => ({
|
||||||
|
id: mapEditor.selectedMapObject.value!.id,
|
||||||
|
mapObject: mapEditor.selectedMapObject.value!.id,
|
||||||
|
isRotated: false,
|
||||||
|
positionX: previewPosition.value.x,
|
||||||
|
positionY: previewPosition.value.y
|
||||||
|
}))
|
||||||
|
|
||||||
|
function updatePreviewPosition(pointer: Phaser.Input.Pointer) {
|
||||||
|
const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
|
||||||
|
if (!tile || (previewPosition.value.x === tile.x && previewPosition.value.y === tile.y)) return
|
||||||
|
|
||||||
|
previewPosition.value = { x: tile.x, y: tile.y }
|
||||||
|
}
|
||||||
|
|
||||||
function pencil(pointer: Phaser.Input.Pointer, map: MapT) {
|
function pencil(pointer: Phaser.Input.Pointer, map: MapT) {
|
||||||
|
emit('pauseObjectTracking')
|
||||||
|
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, mapEditor.currentMap.value!)
|
||||||
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.id,
|
||||||
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)
|
mapEditor.selectedPlacedObject.value = newPlacedMapObject
|
||||||
|
map.placedMapObjects.push(newPlacedMapObject)
|
||||||
|
|
||||||
|
emit('update', map)
|
||||||
}
|
}
|
||||||
|
|
||||||
function eraser(pointer: Phaser.Input.Pointer, map: MapT) {
|
function eraser(pointer: Phaser.Input.Pointer, map: MapT) {
|
||||||
|
emit('pauseObjectTracking')
|
||||||
|
|
||||||
// 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)
|
||||||
|
|
||||||
|
emit('update', map)
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
emit('pauseObjectTracking')
|
||||||
|
|
||||||
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() {
|
|
||||||
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
|
|
||||||
movingPlacedMapObject.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
scene.input.on(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
||||||
|
|
||||||
|
function handlePointerUp(pointer: Phaser.Input.Pointer) {
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_MOVE, handlePointerMove)
|
||||||
|
|
||||||
|
const tile = getTile(props.tileMap, pointer.worldX, pointer.worldY)
|
||||||
|
if (!tile) return
|
||||||
|
|
||||||
|
map.placedMapObjects.map((placed) => {
|
||||||
|
if (placed.id === id) {
|
||||||
|
placed.positionX = tile.x
|
||||||
|
placed.positionY = tile.y
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
mapEditor.movingPlacedObject.value = null
|
||||||
|
|
||||||
|
scene.input.off(Phaser.Input.Events.POINTER_UP, handlePointerUp)
|
||||||
|
emit('resumeObjectTracking')
|
||||||
|
emit('updateAndCommit', map)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function rotatePlacedMapObject(id: string, map: MapT) {
|
function rotatePlacedMapObject(id: string, map: MapT) {
|
||||||
map.placedMapObjects = map.placedMapObjects.map((placedMapObject) => {
|
map.placedMapObjects.map((placed) => {
|
||||||
if (placedMapObject.id === id) {
|
if (placed.id === id) {
|
||||||
return {
|
placed.isRotated = !placed.isRotated
|
||||||
...placedMapObject,
|
|
||||||
isRotated: !placedMapObject.isRotated
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return placedMapObject
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
emit('updateAndCommit', map)
|
||||||
}
|
}
|
||||||
|
|
||||||
function deletePlacedMapObject(id: string, map: MapT) {
|
function deletePlacedMapObject(id: string, map: MapT) {
|
||||||
map.placedMapObjects = map.placedMapObjects.filter((object) => object.id !== id)
|
map.placedMapObjects = map.placedMapObjects.filter((object) => object.id !== id)
|
||||||
selectedPlacedMapObject.value = null
|
mapEditor.selectedPlacedObject.value = null
|
||||||
|
emit('updateAndCommit', map)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 +175,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)
|
||||||
@ -139,43 +192,11 @@ function handlePointer(pointer: Phaser.Input.Pointer) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// watch mapEditorStore.mapObjectList and update originX and originY of objects in mapObjects
|
onMounted(() => {
|
||||||
watch(
|
scene.input.on(Phaser.Input.Events.POINTER_MOVE, updatePreviewPosition)
|
||||||
() => mapEditor.currentMap.value,
|
})
|
||||||
() => {
|
|
||||||
const map = mapEditor.currentMap.value
|
|
||||||
if (!map) return
|
|
||||||
|
|
||||||
const updatedMapObjects = map.placedMapObjects.map((mapObject) => {
|
onUnmounted(() => {
|
||||||
const updatedMapObject = map.placedMapObjects.find((obj) => obj.id === mapObject.mapObject.id)
|
scene.input.off(Phaser.Input.Events.POINTER_MOVE, updatePreviewPosition)
|
||||||
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>
|
||||||
@ -35,9 +35,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { SocketEvent } from '@/application/enums'
|
||||||
import type { Map } from '@/application/types'
|
import type { Map } from '@/application/types'
|
||||||
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 { socketManager } from '@/managers/SocketManager'
|
||||||
import { MapStorage } from '@/storage/storages'
|
import { MapStorage } from '@/storage/storages'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { ref, useTemplateRef } from 'vue'
|
import { ref, useTemplateRef } from 'vue'
|
||||||
@ -56,12 +58,8 @@ const pvp = ref(false)
|
|||||||
defineExpose({ open: () => modalRef.value?.open() })
|
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) => {
|
socketManager.emit(SocketEvent.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 +70,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,17 +1,15 @@
|
|||||||
<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>
|
<div class="flex items-center">
|
||||||
|
<button class="btn-cyan w-7 h-7 font-normal flex items-center justify-center" @click="createMapModal?.open">+</button>
|
||||||
|
<h3 class="text-lg text-white ml-2">Maps</h3>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #modalBody>
|
<template #modalBody>
|
||||||
<div class="my-4 mx-auto h-full">
|
<div class="mx-auto h-full">
|
||||||
<div class="text-center mb-4 px-2 flex gap-2.5">
|
<div class="overflow-y-auto h-[calc(100%)]">
|
||||||
<button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="fetchMaps">Refresh</button>
|
|
||||||
<button class="btn-cyan py-1.5 min-w-[calc(50%_-_5px)]" @click="createMapModal?.open">New</button>
|
|
||||||
</div>
|
|
||||||
<div class="overflow-y-auto h-[calc(100%-20px)]">
|
|
||||||
<div class="relative p-2.5 cursor-pointer flex gap-y-2.5 gap-x-5 flex-wrap" v-for="(map, index) in mapList" :key="map.id">
|
<div class="relative p-2.5 cursor-pointer flex gap-y-2.5 gap-x-5 flex-wrap" v-for="(map, index) in mapList" :key="map.id">
|
||||||
<div class="absolute left-0 top-0 w-full h-px bg-gray-500" v-if="index === 0"></div>
|
|
||||||
<div class="flex gap-3 items-center w-full" @click="() => loadMap(map.id)">
|
<div class="flex gap-3 items-center w-full" @click="() => loadMap(map.id)">
|
||||||
<span>{{ map.name }}</span>
|
<span>{{ map.name }}</span>
|
||||||
<span class="ml-auto gap-1 flex">
|
<span class="ml-auto gap-1 flex">
|
||||||
@ -24,18 +22,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<CreateMap ref="createMapModal" @create="fetchMaps" />
|
<CreateMap ref="createMapModal" @create="fetchMaps" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Map, UUID } from '@/application/types'
|
import { SocketEvent } from '@/application/enums'
|
||||||
|
import type { Map } from '@/application/types'
|
||||||
import CreateMap from '@/components/gameMaster/mapEditor/partials/CreateMap.vue'
|
import CreateMap from '@/components/gameMaster/mapEditor/partials/CreateMap.vue'
|
||||||
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 { socketManager } from '@/managers/SocketManager'
|
||||||
import { MapStorage } from '@/storage/storages'
|
import { MapStorage } from '@/storage/storages'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useMapEditorStore } from '@/stores/mapEditorStore'
|
|
||||||
import { onMounted, ref, useTemplateRef } from 'vue'
|
import { onMounted, ref, useTemplateRef } from 'vue'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
@ -60,15 +58,15 @@ async function fetchMaps() {
|
|||||||
mapList.value = await mapStorage.getAll()
|
mapList.value = await mapStorage.getAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadMap(id: UUID) {
|
function loadMap(id: string) {
|
||||||
gameStore.connection?.emit('gm:map:request', { mapId: id }, (response: Map) => {
|
socketManager.emit(SocketEvent.GM_MAP_REQUEST, { mapId: id }, (response: Map) => {
|
||||||
mapEditor.loadMap(response)
|
mapEditor.loadMap(response)
|
||||||
})
|
})
|
||||||
modalRef.value?.close()
|
modalRef.value?.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteMap(id: UUID) {
|
async function deleteMap(id: string) {
|
||||||
gameStore.connection?.emit('gm:map:delete', { mapId: id }, async (response: boolean) => {
|
socketManager.emit(SocketEvent.GM_MAP_DELETE, { mapId: id }, async (response: boolean) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
gameStore.addNotification({
|
gameStore.addNotification({
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
|
@ -1,44 +1,46 @@
|
|||||||
<template>
|
<template>
|
||||||
<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 z-20" v-if="isOpen">
|
<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" v-if="mapEditor.tool.value === 'pencil' && mapEditor.drawMode.value === 'map_object'">
|
||||||
<div class="relative z-10 p-2.5 border-solid border-0 border-b border-gray-500">
|
<div class="flex flex-col gap-2.5 p-2.5">
|
||||||
<h3 class="text-lg text-white">Map objects</h3>
|
<div class="flex justify-between items-center">
|
||||||
</div>
|
<div class="flex-grow">
|
||||||
<div class="overflow-hidden grow relative">
|
<div class="relative flex">
|
||||||
<div class="absolute w-full h-full top-0 left-0">
|
<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" />
|
||||||
<div class="relative z-10 h-full">
|
<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>
|
|
||||||
<label class="mb-1.5 font-titles hidden" for="search">Search...</label>
|
|
||||||
<input @mousedown.stop class="input-field" type="text" name="search" placeholder="Search" v-model="searchQuery" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col h-[calc(100%_-_170px)] p-4 pb-24">
|
|
||||||
<div class="mb-4 flex flex-wrap gap-2">
|
|
||||||
<button v-for="tag in uniqueTags" :key="tag" @click="toggleTag(tag)" class="btn-cyan" :class="{ 'opacity-50': !selectedTags.includes(tag) }">
|
|
||||||
{{ tag }}
|
|
||||||
</button>
|
|
||||||
</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 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>
|
||||||
|
<img src="/assets/icons/mapEditor/dropdown-chevron.svg" class="w-12 h-12 ml-2 cursor-pointer hover:opacity-80 -rotate-90" alt="Close" @click="mapEditor.setTool('move')" />
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<select class="input-field w-full" name="lists" v-model="mapEditor.drawMode.value" @change="(event: any) => mapEditor.setDrawMode(event.target.value)">
|
||||||
|
<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 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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -52,13 +54,6 @@ import { MapObjectStorage } from '@/storage/storages'
|
|||||||
import { liveQuery } from 'dexie'
|
import { liveQuery } from 'dexie'
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
open: () => (isOpen.value = true),
|
|
||||||
close: () => (isOpen.value = false),
|
|
||||||
toggle: () => (isOpen.value = !isOpen.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
const isOpen = ref(false)
|
|
||||||
const mapObjectStorage = new MapObjectStorage()
|
const mapObjectStorage = new MapObjectStorage()
|
||||||
const mapEditor = useMapEditorComposable()
|
const mapEditor = useMapEditorComposable()
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
@ -70,14 +65,6 @@ const uniqueTags = computed(() => {
|
|||||||
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)
|
||||||
@ -86,6 +73,14 @@ 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(() => {
|
||||||
@ -94,14 +89,13 @@ onMounted(() => {
|
|||||||
mapObjectList.value = result
|
mapObjectList.value = result
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
console.error('Failed to fetch objects:', error)
|
console.error('Failed to fetch tiles:', error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
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,115 @@
|
|||||||
<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 { SocketEvent } from '@/application/enums'
|
||||||
|
import type { MapObject, Map as MapT, PlacedMapObject } from '@/application/types'
|
||||||
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
|
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
|
||||||
|
|
||||||
|
socketManager.emit(
|
||||||
|
SocketEvent.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 v-if="showTeleportModal" ref="modalRef" @modal:close="() => mapEditor.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>
|
||||||
@ -28,7 +28,7 @@
|
|||||||
<label for="toMap">Map to teleport to</label>
|
<label for="toMap">Map to teleport to</label>
|
||||||
<select v-model="toMap" class="input-field" name="toMap" id="toMap">
|
<select v-model="toMap" class="input-field" name="toMap" id="toMap">
|
||||||
<option :value="null">Select map</option>
|
<option :value="null">Select map</option>
|
||||||
<option v-for="map in mapList" :key="map.id" :value="map">{{ map.name }}</option>
|
<option v-for="map in mapList" :key="map.id" :value="map.id">{{ map.name }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -41,48 +41,48 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Map } from '@/application/types'
|
import type { Map } from '@/application/types'
|
||||||
import Modal from '@/components/utilities/Modal.vue'
|
import Modal from '@/components/utilities/Modal.vue'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||||
import { useMapEditorStore } from '@/stores/mapEditorStore'
|
import { MapStorage } from '@/storage/storages'
|
||||||
import { computed, onMounted, ref, useTemplateRef, watch } from 'vue'
|
import { computed, onMounted, ref, useTemplateRef, watch } from 'vue'
|
||||||
|
|
||||||
const showTeleportModal = computed(() => mapEditorStore.tool === 'pencil' && mapEditorStore.drawMode === 'teleport')
|
const showTeleportModal = computed(() => mapEditor.tool.value === 'pencil' && mapEditor.drawMode.value === 'teleport')
|
||||||
const mapEditorStore = useMapEditorStore()
|
const mapStorage = new MapStorage()
|
||||||
const gameStore = useGameStore()
|
const mapEditor = useMapEditorComposable()
|
||||||
const mapList = ref<Map[]>([])
|
|
||||||
const modalRef = useTemplateRef('modalRef')
|
const modalRef = useTemplateRef('modalRef')
|
||||||
|
const mapList = ref<Map[]>([])
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
open: () => modalRef.value?.open()
|
open: () => modalRef.value?.open()
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(fetchMaps)
|
|
||||||
|
|
||||||
function fetchMaps() {
|
|
||||||
gameStore.connection?.emit('gm:map:list', {}, (response: Map[]) => {
|
|
||||||
mapList.value = response
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const { toPositionX, toPositionY, toRotation, toMap } = useRefTeleportSettings()
|
const { toPositionX, toPositionY, toRotation, toMap } = useRefTeleportSettings()
|
||||||
|
|
||||||
function useRefTeleportSettings() {
|
function useRefTeleportSettings() {
|
||||||
const settings = mapEditorStore.teleportSettings
|
const settings = mapEditor.teleportSettings.value
|
||||||
return {
|
return {
|
||||||
toPositionX: ref(settings.toPositionX),
|
toPositionX: ref(settings.toPositionX),
|
||||||
toPositionY: ref(settings.toPositionY),
|
toPositionY: ref(settings.toPositionY),
|
||||||
toRotation: ref(settings.toRotation),
|
toRotation: ref(settings.toRotation),
|
||||||
toMap: ref(settings.toMapId)
|
toMap: ref(settings.toMap)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch([toPositionX, toPositionY, toRotation, toMap], updateTeleportSettings)
|
watch([toPositionX, toPositionY, toRotation, toMap], updateTeleportSettings)
|
||||||
|
|
||||||
function updateTeleportSettings() {
|
function updateTeleportSettings() {
|
||||||
mapEditorStore.setTeleportSettings({
|
mapEditor.setTeleportSettings({
|
||||||
toPositionX: toPositionX.value,
|
toPositionX: toPositionX.value,
|
||||||
toPositionY: toPositionY.value,
|
toPositionY: toPositionY.value,
|
||||||
toRotation: toRotation.value,
|
toRotation: toRotation.value,
|
||||||
toMapId: toMap.value
|
toMap: toMap.value
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchMaps() {
|
||||||
|
mapList.value = await mapStorage.getAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetchMaps()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,86 +1,87 @@
|
|||||||
<template>
|
<template>
|
||||||
<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 z-20" v-if="isOpen">
|
<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" v-if="(mapEditor.tool.value === 'pencil' && mapEditor.drawMode.value === 'tile') || mapEditor.tool.value === 'paint'">
|
||||||
<div class="relative z-10 p-2.5 border-solid border-0 border-b border-gray-500">
|
<div class="flex flex-col gap-2.5 p-2.5">
|
||||||
<h3 class="text-lg text-white">Tiles</h3>
|
<div class="flex justify-between items-center">
|
||||||
</div>
|
<div class="flex-grow">
|
||||||
<div class="overflow-hidden grow relative">
|
<div class="relative flex">
|
||||||
<div class="absolute top-0 left-0 h-full w-full">
|
<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" />
|
||||||
<div class="relative z-10 h-full">
|
<label class="mb-1.5 font-titles hidden" for="search">Search</label>
|
||||||
<div class="h-full" 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 class="w-full flex gap-1.5 flex-row">
|
|
||||||
<div>
|
|
||||||
<label class="mb-1.5 font-titles hidden" for="search">Search...</label>
|
|
||||||
<input @mousedown.stop class="input-field" type="text" name="search" placeholder="Search" v-model="searchQuery" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col h-[calc(100%_-_170px)] p-4 pb-24">
|
|
||||||
<div class="mb-4 flex flex-wrap gap-2">
|
|
||||||
<button v-for="tag in uniqueTags" :key="tag" @click="toggleTag(tag)" class="btn-cyan" :class="{ 'opacity-50': !selectedTags.includes(tag) }">
|
|
||||||
{{ tag }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="h-full flex-grow overflow-y-auto">
|
|
||||||
<div class="grid grid-cols-4 gap-2 justify-items-center">
|
|
||||||
<div v-for="group in groupedTiles" :key="group.parent.id" class="flex flex-col items-center justify-center relative">
|
|
||||||
<img
|
|
||||||
class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300"
|
|
||||||
:src="`${config.server_endpoint}/textures/tiles/${group.parent.id}.png`"
|
|
||||||
:alt="group.parent.name"
|
|
||||||
@click="openGroup(group)"
|
|
||||||
@load="() => tileProcessor.processTile(group.parent)"
|
|
||||||
:class="{
|
|
||||||
'border-cyan shadow-lg': isActiveTile(group.parent),
|
|
||||||
'border-transparent hover:border-gray-300': !isActiveTile(group.parent)
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
<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 v-else class="h-full overflow-auto">
|
</div>
|
||||||
<div class="p-4">
|
<img src="/assets/icons/mapEditor/dropdown-chevron.svg" class="w-12 h-12 ml-2 cursor-pointer hover:opacity-80 -rotate-90" alt="Close" @click="mapEditor.setTool('move')" />
|
||||||
<button @click="closeGroup" class="btn-cyan mb-4">Back to All Tiles</button>
|
</div>
|
||||||
<h4 class="text-lg mb-4">{{ selectedGroup.parent.name }} Group</h4>
|
<div class="flex">
|
||||||
<div class="grid grid-cols-4 gap-2 justify-items-center">
|
<select class="input-field w-full" name="lists" v-model="mapEditor.drawMode.value" @change="(event: any) => mapEditor.setDrawMode(event.target.value)">
|
||||||
<div class="flex flex-col items-center justify-center">
|
<option value="tile">Tiles</option>
|
||||||
<img
|
<option value="map_object">Objects</option>
|
||||||
class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300"
|
</select>
|
||||||
:src="`${config.server_endpoint}/textures/tiles/${selectedGroup.parent.id}.png`"
|
</div>
|
||||||
:alt="selectedGroup.parent.name"
|
</div>
|
||||||
@click="selectTile(selectedGroup.parent.id)"
|
<div class="h-full overflow-auto relative border-0 border-t border-solid border-gray-500 p-2.5">
|
||||||
:class="{
|
<div class="h-full" v-if="!selectedGroup">
|
||||||
'border-cyan shadow-lg': isActiveTile(selectedGroup.parent),
|
<div class="grid grid-cols-4 gap-2 justify-items-center">
|
||||||
'border-transparent hover:border-gray-300': !isActiveTile(selectedGroup.parent)
|
<div v-for="group in groupedTiles" :key="group.parent.id" class="flex flex-col items-center justify-center relative">
|
||||||
}"
|
<img
|
||||||
/>
|
class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300"
|
||||||
<span class="text-xs mt-1">{{ getTileCategory(selectedGroup.parent) }}</span>
|
:src="`${config.server_endpoint}/textures/tiles/${group.parent.id}.png`"
|
||||||
</div>
|
:alt="group.parent.name"
|
||||||
<div v-for="childTile in selectedGroup.children" :key="childTile.id" class="flex flex-col items-center justify-center">
|
@click="openGroup(group)"
|
||||||
<img
|
@load="() => tileProcessor.processTile(group.parent)"
|
||||||
class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300"
|
:class="{
|
||||||
:src="`${config.server_endpoint}/textures/tiles/${childTile.id}.png`"
|
'border-cyan shadow-lg': isActiveTile(group.parent),
|
||||||
:alt="childTile.name"
|
'border-transparent hover:border-gray-300': !isActiveTile(group.parent)
|
||||||
@click="selectTile(childTile.id)"
|
}"
|
||||||
:class="{
|
/>
|
||||||
'border-cyan shadow-lg': isActiveTile(childTile),
|
<span class="text-xs mt-1">{{ getTileCategory(group.parent) }}</span>
|
||||||
'border-transparent hover:border-gray-300': !isActiveTile(childTile)
|
<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>
|
||||||
<span class="text-xs mt-1">{{ getTileCategory(childTile) }}</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="h-full overflow-auto">
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<button @click="closeGroup" class="hover:text-white">Back to all tiles</button>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-4 gap-2 justify-items-center">
|
||||||
|
<div class="flex flex-col items-center justify-center">
|
||||||
|
<img
|
||||||
|
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`"
|
||||||
|
:alt="selectedGroup.parent.name"
|
||||||
|
@click="selectTile(selectedGroup.parent.id)"
|
||||||
|
:class="{
|
||||||
|
'border-cyan shadow-lg': isActiveTile(selectedGroup.parent),
|
||||||
|
'border-transparent hover:border-gray-300': !isActiveTile(selectedGroup.parent)
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<span class="text-xs mt-1">{{ getTileCategory(selectedGroup.parent) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-for="childTile in selectedGroup.children" :key="childTile.id" class="flex flex-col items-center justify-center">
|
||||||
|
<img
|
||||||
|
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`"
|
||||||
|
:alt="childTile.name"
|
||||||
|
@click="selectTile(childTile.id)"
|
||||||
|
:class="{
|
||||||
|
'border-cyan shadow-lg': isActiveTile(childTile),
|
||||||
|
'border-transparent hover:border-gray-300': !isActiveTile(childTile)
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<span class="text-xs mt-1">{{ getTileCategory(childTile) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -92,7 +93,6 @@ import { useTileProcessingComposable } from '@/composables/useTileProcessingComp
|
|||||||
import { TileStorage } from '@/storage/storages'
|
import { TileStorage } from '@/storage/storages'
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
|
||||||
const isOpen = ref(false)
|
|
||||||
const tileStorage = new TileStorage()
|
const tileStorage = new TileStorage()
|
||||||
const mapEditor = useMapEditorComposable()
|
const mapEditor = useMapEditorComposable()
|
||||||
const tileProcessor = useTileProcessingComposable()
|
const tileProcessor = useTileProcessingComposable()
|
||||||
@ -102,12 +102,6 @@ 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[]>([])
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
open: () => (isOpen.value = true),
|
|
||||||
close: () => (isOpen.value = false),
|
|
||||||
toggle: () => (isOpen.value = !isOpen.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
const uniqueTags = computed(() => {
|
const uniqueTags = computed(() => {
|
||||||
const allTags = tiles.value.flatMap((tile) => tile.tags || [])
|
const allTags = tiles.value.flatMap((tile) => tile.tags || [])
|
||||||
return Array.from(new Set(allTags))
|
return Array.from(new Set(allTags))
|
||||||
|
@ -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 z-20">
|
<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>
|
||||||
@ -68,53 +68,59 @@
|
|||||||
|
|
||||||
<div class="w-px bg-cyan"></div>
|
<div class="w-px bg-cyan"></div>
|
||||||
|
|
||||||
<button class="flex justify-center items-center min-w-10 p-0 relative" @click="handleClick('settings')"><img class="invert w-5 h-5" src="/assets/icons/mapEditor/gear.svg" alt="Map settings" /> <span class="h-5 ml-2.5">(Z)</span></button>
|
<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/gear.svg" alt="Map settings" /> <span class="h-5 ml-2.5">(Z)</span></button>
|
||||||
|
|
||||||
<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="() => mapEditor.toggleActive()">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="toggleContinuousDrawing" v-model="isContinuousDrawingEnabled" 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>
|
||||||
|
<div class="m-4 flex items-center space-x-2">
|
||||||
|
<input id="show-placed-map-object-preview" @change="mapEditor.togglePlacedMapObjectPreview()" v-model="isShowPlacedMapObjectPreviewEnabled" type="checkbox" class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" />
|
||||||
|
<label for="show-placed-map-object-preview" class="text-sm font-medium text-gray-200 cursor-pointer"> Show placed map object preview </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 { computed, 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', 'open-teleport'])
|
||||||
|
|
||||||
// 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 isContinuousDrawingEnabled = ref<Boolean>(false)
|
||||||
|
const isShowPlacedMapObjectPreviewEnabled = ref<Boolean>(mapEditor.isPlacedMapObjectPreviewEnabled.value)
|
||||||
let tileListShown = ref(false)
|
const listOpen = computed(() => (mapEditor.tool.value === 'pencil' && (mapEditor.drawMode.value === 'tile' || mapEditor.drawMode.value === 'map_object')) || mapEditor.tool.value === 'paint')
|
||||||
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' || mapEditor.tool.value === 'eraser') {
|
|
||||||
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,24 +137,21 @@ function setEraserMode() {
|
|||||||
selectEraserOpen.value = false
|
selectEraserOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleContinuousDrawing() {
|
||||||
|
mapEditor.setInputMode(isContinuousDrawingEnabled.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') {
|
mapEditor.setTool(tool)
|
||||||
emit('open-settings')
|
if (tool === 'paint') mapEditor.setDrawMode('tile')
|
||||||
emit('close-lists')
|
|
||||||
} else if (tool === 'move') {
|
|
||||||
emit('close-lists')
|
|
||||||
mapEditor.setTool(tool)
|
|
||||||
} else {
|
|
||||||
mapEditor.setTool(tool)
|
|
||||||
}
|
|
||||||
|
|
||||||
selectPencilOpen.value = tool === 'pencil' ? !selectPencilOpen.value : false
|
selectPencilOpen.value = tool === 'pencil' ? !selectPencilOpen.value : false
|
||||||
selectEraserOpen.value = tool === 'eraser' ? !selectEraserOpen.value : false
|
selectEraserOpen.value = tool === 'eraser' ? !selectEraserOpen.value : false
|
||||||
|
if (mapEditor.drawMode.value === 'teleport') emit('open-teleport')
|
||||||
}
|
}
|
||||||
|
|
||||||
function cycleToolMode(tool: 'pencil' | 'eraser') {
|
function cycleToolMode(tool: 'pencil' | 'eraser') {
|
||||||
@ -164,15 +167,18 @@ function initKeyShortcuts(event: KeyboardEvent) {
|
|||||||
// Check if map is set
|
// Check if map is set
|
||||||
if (!mapEditor.currentMap.value) return
|
if (!mapEditor.currentMap.value) return
|
||||||
|
|
||||||
// prevent if focused on composables
|
// prevent if focused on inputs
|
||||||
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,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { login } from '@/services/authentication'
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
|
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'
|
||||||
@ -39,15 +40,6 @@ const password = ref('')
|
|||||||
const formError = ref('')
|
const formError = ref('')
|
||||||
const showPassword = ref(false)
|
const showPassword = ref(false)
|
||||||
|
|
||||||
// automatic login because of development
|
|
||||||
onMounted(async () => {
|
|
||||||
const token = useCookies().get('token')
|
|
||||||
if (token) {
|
|
||||||
gameStore.setToken(token)
|
|
||||||
gameStore.initConnection()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
// check if username and password are valid
|
// check if username and password are valid
|
||||||
if (username.value === '' || password.value === '') {
|
if (username.value === '' || password.value === '') {
|
||||||
@ -62,7 +54,7 @@ async function submit() {
|
|||||||
formError.value = response.error
|
formError.value = response.error
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
gameStore.setToken(response.token)
|
socketManager.setToken(response.token)
|
||||||
gameStore.initConnection()
|
gameStore.initConnection()
|
||||||
return true // Indicate success
|
return true // Indicate success
|
||||||
}
|
}
|
||||||
|
@ -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'
|
||||||
@ -34,15 +34,6 @@ const password = ref('')
|
|||||||
const newPasswordError = ref('')
|
const newPasswordError = ref('')
|
||||||
const showPassword = ref(false)
|
const showPassword = ref(false)
|
||||||
|
|
||||||
// automatic login because of development
|
|
||||||
onMounted(async () => {
|
|
||||||
const token = useCookies().get('token')
|
|
||||||
if (token) {
|
|
||||||
gameStore.setToken(token)
|
|
||||||
gameStore.initConnection()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
async function newPasswordFunc() {
|
async function newPasswordFunc() {
|
||||||
// check if username and password are valid
|
// check if username and password are valid
|
||||||
if (password.value === '') {
|
if (password.value === '') {
|
||||||
|
@ -26,7 +26,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { login, register } from '@/services/authentication'
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
|
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'
|
||||||
@ -40,15 +41,6 @@ const email = ref('')
|
|||||||
const formError = ref('')
|
const formError = ref('')
|
||||||
const showPassword = ref(false)
|
const showPassword = ref(false)
|
||||||
|
|
||||||
// automatic login because of development
|
|
||||||
onMounted(async () => {
|
|
||||||
const token = useCookies().get('token')
|
|
||||||
if (token) {
|
|
||||||
gameStore.setToken(token)
|
|
||||||
gameStore.initConnection()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
// check if username and password are valid
|
// check if username and password are valid
|
||||||
if (username.value === '' || email.value === '' || password.value === '') {
|
if (username.value === '' || email.value === '' || password.value === '') {
|
||||||
@ -76,7 +68,7 @@ async function submit() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.setToken(loginResponse.token)
|
socketManager.setToken(loginResponse.token)
|
||||||
gameStore.initConnection()
|
gameStore.initConnection()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
@ -10,78 +10,106 @@
|
|||||||
<div class="filler"></div>
|
<div class="filler"></div>
|
||||||
<div class="w-2/3 max-w-[860px]" v-if="!isLoading">
|
<div class="w-2/3 max-w-[860px]" v-if="!isLoading">
|
||||||
<div class="mb-5 flex flex-col gap-1">
|
<div class="mb-5 flex flex-col gap-1">
|
||||||
<h1 class="text-white font-bold">SELECT CHARACTER TO PLAY</h1>
|
<h1 class="text-white font-bold">{{ isCreatingCharacter ? 'CREATE CHARACTER' : 'SELECT CHARACTER TO PLAY' }}</h1>
|
||||||
<p class="m-0">Maximum of 4 characters can be created per player</p>
|
<p class="m-0" v-if="!isCreatingCharacter">Maximum of 4 characters can be created per player</p>
|
||||||
|
<p class="m-0" v-if="isCreatingCharacter">Customize your new character</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-full max-lg:flex-col lg:h-[400px] default-border rounded-md bg-gray">
|
<div class="flex w-full max-lg:flex-col lg:h-[400px] default-border rounded-md bg-gray">
|
||||||
<div class="lg:min-w-[285px] max-lg:min-h-[383px] lg:w-1/3 h-full bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center border-0 max-lg:border-b lg:border-r border-solid border-gray-500 max-lg:rounded-t-md lg:rounded-l-md relative">
|
<div class="lg:min-w-[285px] max-lg:min-h-[383px] lg:w-1/3 h-full bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center border-0 max-lg:border-b lg:border-r border-solid border-gray-500 max-lg:rounded-t-md lg:rounded-l-md relative">
|
||||||
<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: isCreatingCharacter }" v-if="characters.length < characterCreationSettings.maxCharacters">
|
||||||
<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="startCharacterCreation">
|
||||||
<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" alt="Add character" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="py-6 px-8 h-[calc(100%_-_48px)] flex flex-col items-center gap-6 justify-center" v-if="selectedCharacterId">
|
<div class="py-6 px-8 h-[calc(100%_-_48px)] flex flex-col items-center gap-6 justify-center">
|
||||||
<input class="input-field w-[158px]" type="text" name="name" :placeholder="characters.find((c) => c.id == selectedCharacterId)?.name" />
|
<template v-if="selectedCharacterId && !isCreatingCharacter">
|
||||||
<div class="flex flex-col gap-4 items-center">
|
<input class="input-field w-[158px]" type="text" name="name" :placeholder="characters.find((c) => c.id == selectedCharacterId)?.name" v-model="newNickname" />
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-4 items-center">
|
||||||
<div class="bg-[url('/assets/ui-elements/character-select-ui-shape.svg')] w-[190px] h-52 bg-no-repeat bg-center flex items-center justify-between">
|
<div class="flex flex-col gap-3">
|
||||||
|
<div class="bg-[url('/assets/ui-elements/character-select-ui-shape.svg')] w-[190px] h-52 bg-no-repeat bg-center flex items-center justify-between">
|
||||||
|
<button class="ml-6 w-4 h-8 p-0">
|
||||||
|
<img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 m-auto" alt="Arrow left" />
|
||||||
|
</button>
|
||||||
|
<img class="w-24 object-contain mb-3.5 max-h-[70%]" alt="Player avatar" :src="config.server_endpoint + '/avatar/s/' + characters.find((c) => c.id === selectedCharacterId)?.characterType + '/' + (selectedHairId ?? 'default')" />
|
||||||
|
<button class="mr-6 w-4 h-8 p-0">
|
||||||
|
<img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 -scale-x-100" alt="Arrow right" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-if="isCreatingCharacter">
|
||||||
|
<div class="flex flex-col gap-4 items-center">
|
||||||
|
<input class="input-field" v-model="newCharacterName" name="name" id="name" placeholder="Enter a nickname..." />
|
||||||
|
<div class="bg-[url('/assets/ui-elements/character-select-ui-shape.svg')] w-[190px] h-52 bg-no-repeat bg-center flex items-center justify-center">
|
||||||
<button class="ml-6 w-4 h-8 p-0">
|
<button class="ml-6 w-4 h-8 p-0">
|
||||||
<img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 m-auto" alt="Arrow left" />
|
<img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 m-auto" alt="Arrow left" />
|
||||||
</button>
|
</button>
|
||||||
<img class="w-24 object-contain mb-3.5 max-h-[70%]" alt="Player avatar" :src="config.server_endpoint + '/avatar/s/' + characters.find((c) => c.id === selectedCharacterId)?.characterType + '/' + (selectedHairId ?? 'default')" />
|
<img class="w-24 object-contain mb-3.5 max-h-[70%]" alt="Player avatar" :src="config.server_endpoint + '/avatar/s/' + defaultCharacterTypeId + '/' + (selectedHairId ?? 'default')" />
|
||||||
<button class="mr-6 w-4 h-8 p-0">
|
<button class="mr-6 w-4 h-8 p-0">
|
||||||
<img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 -scale-x-100" alt="Arrow right" />
|
<img src="/assets/icons/triangle-icon.svg" class="w-3 h-3.5 -scale-x-100" alt="Arrow right" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex justify-between w-[190px]">
|
||||||
|
<button class="btn-empty flex gap-2" :class="{ selected: selectedGender === 'MALE' }" @click="selectedGender = 'MALE'">
|
||||||
|
<img src="/assets/icons/male-icon.svg" class="w-4 h-4 m-auto" alt="Male symbol" />
|
||||||
|
<span class="text-white">Male</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn-empty flex gap-2" :class="{ selected: selectedGender === 'FEMALE' }" @click="selectedGender = 'FEMALE'">
|
||||||
|
<img src="/assets/icons/male-icon.svg" class="w-4 h-4 m-auto" alt="Female symbol" />
|
||||||
|
<span class="text-white">Female</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- TODO: update gender on (selected) character -->
|
</template>
|
||||||
<!-- <div class="flex justify-between w-[190px]">-->
|
|
||||||
<!-- <button class="btn-empty flex gap-2" :class="{ selected: characters.find((c) => c.id == selectedCharacterId)?.characterType?.gender === 'MALE' }">-->
|
|
||||||
<!-- <img src="/assets/icons/male-icon.svg" class="w-4 h-4 m-auto" alt="Male symbol" />-->
|
|
||||||
<!-- <span class="text-white">Male</span>-->
|
|
||||||
<!-- </button>-->
|
|
||||||
<!-- <button class="btn-empty flex gap-2" :class="{ selected: characters.find((c) => c.id == selectedCharacterId)?.characterType?.gender === 'FEMALE' }">-->
|
|
||||||
<!-- <img src="/assets/icons/male-icon.svg" class="w-4 h-4 m-auto" alt="Male symbol" />-->
|
|
||||||
<!-- <span class="text-white">Female</span>-->
|
|
||||||
<!-- </button>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 lg:w-2/3 max-lg:min-h-[212px] h-full bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center max-lg:rounded-bl-md rounded-r-md">
|
<div class="flex-1 lg:w-2/3 max-lg:min-h-[212px] h-full bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center max-lg:rounded-bl-md rounded-r-md">
|
||||||
<div class="py-6 px-8 h-[calc(100%_-_48px)] flex flex-col items-center gap-10" v-if="selectedCharacterId">
|
<div class="py-6 px-8 h-[calc(100%_-_48px)] flex flex-col items-center gap-10" v-if="selectedCharacterId || isCreatingCharacter">
|
||||||
|
<div class="flex flex-col gap-3 w-full">
|
||||||
|
<span class="text-sm">Hair color</span>
|
||||||
|
<div class="flex gap-2 flex-wrap">
|
||||||
|
<div
|
||||||
|
class="hair-deselect relative flex justify-center items-center bg-gray default-border w-[18px] h-[18px] p-2 rounded-sm hover:bg-gray-500 hover:border-gray-400 focus-visible:outline-none focus-visible:border-white focus-visible:bg-cyan has-[:checked]:bg-cyan has-[:checked]:border-transparent"
|
||||||
|
>
|
||||||
|
<img src="/assets/icons/x-button-gray.svg" class="w-4 h-4" alt="Empty button" />
|
||||||
|
<input type="radio" name="hair-color" :value="null" v-model="selectedHairColor" class="h-full w-full absolute left-0 top-0 m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0 focus-visible:outline-white" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="color in uniqueHairColors"
|
||||||
|
class="relative flex justify-center items-center default-border w-[18px] h-[18px] p-2 rounded-sm hover:bg-gray-500 hover:border-gray-400 focus-visible:outline-none focus-visible:border-gray-300 focus-visible:bg-gray-500 has-[:checked]:bg-cyan has-[:checked]:border-transparent"
|
||||||
|
>
|
||||||
|
<div class="w-full h-full rounded-sm" :style="getHairColorStyle(color)"></div>
|
||||||
|
<input type="radio" name="hair-color" :value="color" v-model="selectedHairColor" class="h-full w-full absolute left-0 top-0 m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0 focus-visible:outline-white" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="flex flex-col gap-3 w-full">
|
<div class="flex flex-col gap-3 w-full">
|
||||||
<span class="text-sm">Hairstyle</span>
|
<span class="text-sm">Hairstyle</span>
|
||||||
<div class="flex gap-2 flex-wrap max-h-20 overflow-y-auto scrollbar">
|
<div class="flex gap-2 flex-wrap">
|
||||||
<div
|
<div
|
||||||
class="hair-deselect relative flex justify-center items-center bg-gray default-border w-[18px] h-[18px] p-2 rounded-sm hover:bg-gray-500 hover:border-gray-400 focus-visible:outline-none focus-visible:border-white focus-visible:bg-cyan has-[:checked]:bg-cyan has-[:checked]:border-transparent"
|
class="hair-deselect relative flex justify-center items-center bg-gray default-border w-[18px] h-[18px] p-2 rounded-sm hover:bg-gray-500 hover:border-gray-400 focus-visible:outline-none focus-visible:border-white focus-visible:bg-cyan has-[:checked]:bg-cyan has-[:checked]:border-transparent"
|
||||||
>
|
>
|
||||||
<img src="/assets/icons/x-button-gray.svg" class="w-4 h-4" alt="Empty button" />
|
<img src="/assets/icons/x-button-gray.svg" class="w-4 h-4" alt="Empty button" />
|
||||||
<input type="radio" name="hair" :value="null" v-model="selectedHairId" class="h-full w-full absolute left-0 top-0 m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0 focus-visible:outline-white" />
|
<input type="radio" name="hair" :value="null" v-model="selectedHairId" class="h-full w-full absolute left-0 top-0 m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0 focus-visible:outline-white" />
|
||||||
</div>
|
</div>
|
||||||
<!-- TODO #255: make radio button so we can set a value, do the same with swatches -->
|
|
||||||
<div
|
<div
|
||||||
v-for="hair in characterHairs"
|
v-for="hair in filteredHairs"
|
||||||
class="relative flex justify-center items-center bg-gray default-border w-[18px] h-[18px] p-2 rounded-sm hover:bg-gray-500 hover:border-gray-400 focus-visible:outline-none focus-visible:border-gray-300 focus-visible:bg-gray-500 has-[:checked]:bg-cyan has-[:checked]:border-transparent"
|
class="relative flex justify-center items-center bg-gray default-border w-[18px] h-[18px] p-2 rounded-sm hover:bg-gray-500 hover:border-gray-400 focus-visible:outline-none focus-visible:border-gray-300 focus-visible:bg-gray-500 has-[:checked]:bg-cyan has-[:checked]:border-transparent overflow-hidden"
|
||||||
>
|
>
|
||||||
<img class="h-4 object-contain" :src="config.server_endpoint + '/textures/sprites/' + hair.sprite + '/front.png'" alt="Hair sprite" />
|
<div class="absolute inset-0 flex items-center justify-center">
|
||||||
|
<img class="h-16 object-contain scale-[1] mt-8 origin-center" :src="config.server_endpoint + '/textures/sprites/' + hair.sprite + '/front.png'" alt="Hair sprite" />
|
||||||
|
</div>
|
||||||
<input type="radio" name="hair" :value="hair.id" v-model="selectedHairId" class="h-full w-full absolute left-0 top-0 m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0 focus-visible:outline-white" />
|
<input type="radio" name="hair" :value="hair.id" v-model="selectedHairId" class="h-full w-full absolute left-0 top-0 m-0 z-10 hover:cursor-pointer focus-visible:outline-offset-0 focus-visible:outline-white" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-3 w-full">
|
|
||||||
<span class="text-sm">Hair color</span>
|
|
||||||
<div class="flex gap-2 flex-wrap">
|
|
||||||
<!-- TODO: replace with hair colors -->
|
|
||||||
<input type="radio" name="hair-color" v-for="n in 10" class="bg-red w-6 h-6 m-0 rounded-sm hover:cursor-pointer checked:outline checked:outline-1 checked:outline-white" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -89,73 +117,102 @@
|
|||||||
<div v-else>
|
<div v-else>
|
||||||
<img class="w-20 invert-80" src="/assets/icons/loading-icon1.svg" alt="Loading" />
|
<img class="w-20 invert-80" src="/assets/icons/loading-icon1.svg" alt="Loading" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-2/3 button-wrapper flex self-center justify-center lg:justify-end gap-4 max-w-[860px]" v-if="!isLoading">
|
<div class="w-2/3 button-wrapper flex self-center justify-center lg:justify-end gap-4 max-w-[860px]" v-if="!isLoading">
|
||||||
<button class="btn-empty min-w-48" @click.stop="gameStore.disconnectSocket()">Back</button>
|
<template v-if="!isCreatingCharacter">
|
||||||
<button class="btn-cyan min-w-48 disabled:bg-cyan-800 disabled:cursor-not-allowed" :disabled="!selectedCharacterId" @click="loginWithCharacter()">Play now</button>
|
<button class="btn-empty min-w-48" @click.stop="gameStore.disconnectSocket()">Back</button>
|
||||||
|
<button class="btn-cyan min-w-48 disabled:bg-cyan-800 disabled:cursor-not-allowed" :disabled="!selectedCharacterId" @click="loginWithCharacter()">Play now</button>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<button class="btn-empty min-w-48" @click="cancelCharacterCreation">Cancel</button>
|
||||||
|
<button class="btn-cyan min-w-48" @click="createCharacter">Create</button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- CREATE CHARACTER MODAL -->
|
|
||||||
<Modal :isModalOpen="isCreateNewCharacterModalOpen" @modal:close="isCreateNewCharacterModalOpen = false" :modal-width="430" :modal-height="275">
|
|
||||||
<template #modalHeader>
|
|
||||||
<h3 class="m-0 font-medium text-white">Create your character</h3>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #modalBody>
|
|
||||||
<div class="p-4 h-[calc(100%_-_32px)]">
|
|
||||||
<form method="post" @submit.prevent="createCharacter" class="h-full flex flex-col justify-between">
|
|
||||||
<div class="form-field-full">
|
|
||||||
<label for="name" class="text-white">Nickname</label>
|
|
||||||
<input class="input-field" v-model="newCharacterName" name="name" id="name" placeholder="Enter a nickname..." />
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-flow-col justify-stretch gap-4">
|
|
||||||
<button type="button" class="btn-empty py-1.5 px-4 inline-block" @click.prevent="isCreateNewCharacterModalOpen = false">Cancel</button>
|
|
||||||
<button class="btn-cyan py-1.5 px-4 inline-block" type="submit">Create</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Modal>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
|
import { SocketEvent } from '@/application/enums'
|
||||||
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 { useSoundComposable } from '@/composables/useSoundComposable'
|
||||||
import { CharacterHairStorage } from '@/storage/storages'
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
|
import { CharacterHairStorage, CharacterTypeStorage } from '@/storage/storages'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
const characterCreationSettings = {
|
||||||
|
maxCharacters: 4,
|
||||||
|
minNameLength: 3,
|
||||||
|
maxNameLength: 20,
|
||||||
|
defaultGender: 'MALE' as const
|
||||||
|
}
|
||||||
|
|
||||||
|
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[]>([])
|
||||||
const selectedCharacterId = ref<string | null>(null)
|
const selectedCharacterId = ref<string | null>(null)
|
||||||
const isCreateNewCharacterModalOpen = ref<boolean>(false)
|
const newNickname = ref<string>('')
|
||||||
const newCharacterName = ref<string>('')
|
const newCharacterName = ref<string>('')
|
||||||
const characterHairs = ref<CharacterHair[]>([])
|
const characterHairs = ref<CharacterHair[]>([])
|
||||||
const selectedHairId = ref<string | null>(null)
|
const selectedHairId = ref<string | null>(null)
|
||||||
|
const defaultCharacterTypeId = ref<string>('')
|
||||||
|
const isCreatingCharacter = ref<boolean>(false)
|
||||||
|
const selectedGender = ref()
|
||||||
|
|
||||||
|
const selectedHairColor = ref<string | null>(null)
|
||||||
|
const uniqueHairColors = computed(() => {
|
||||||
|
return [...new Set(characterHairs.value.map((hair) => hair.color))]
|
||||||
|
})
|
||||||
|
|
||||||
|
const filteredHairs = computed(() => {
|
||||||
|
if (!selectedHairColor.value) return characterHairs.value
|
||||||
|
return characterHairs.value.filter((hair) => hair.color === selectedHairColor.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
function getHairColorStyle(color: string | null) {
|
||||||
|
return {
|
||||||
|
backgroundColor: color,
|
||||||
|
border: selectedHairColor.value === color ? '1px solid white' : '1px solid rgba(255, 255, 255, 0.2)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startCharacterCreation() {
|
||||||
|
isCreatingCharacter.value = true
|
||||||
|
selectedCharacterId.value = null
|
||||||
|
newCharacterName.value = ''
|
||||||
|
selectedHairId.value = null
|
||||||
|
selectedHairColor.value = null
|
||||||
|
selectedGender.value = characterCreationSettings.defaultGender
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelCharacterCreation() {
|
||||||
|
isCreatingCharacter.value = false
|
||||||
|
newCharacterName.value = ''
|
||||||
|
selectedHairId.value = null
|
||||||
|
selectedHairColor.value = null
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch characters
|
// Fetch characters
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
gameStore.connection?.emit('character:list')
|
socketManager.emit(SocketEvent.CHARACTER_LIST)
|
||||||
}, 750)
|
}, 750)
|
||||||
|
|
||||||
gameStore.connection?.on('character:list', (data: any) => {
|
socketManager.on(SocketEvent.CHARACTER_LIST, (data: any) => {
|
||||||
characters.value = data
|
characters.value = data
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
})
|
})
|
||||||
|
|
||||||
// Select character logics
|
|
||||||
function loginWithCharacter() {
|
function loginWithCharacter() {
|
||||||
if (!selectedCharacterId.value) return
|
if (!selectedCharacterId.value) return
|
||||||
|
|
||||||
gameStore.connection?.emit(
|
socketManager.emit(
|
||||||
'character:connect',
|
SocketEvent.CHARACTER_CONNECT,
|
||||||
{
|
{
|
||||||
characterId: selectedCharacterId.value,
|
characterId: selectedCharacterId.value,
|
||||||
characterHairId: selectedHairId.value
|
characterHairId: selectedHairId.value,
|
||||||
|
newNickname: newNickname.value
|
||||||
},
|
},
|
||||||
(response: { character: CharacterT; map: Map; characters: CharacterT[] }) => {
|
(response: { character: CharacterT; map: Map; characters: CharacterT[] }) => {
|
||||||
gameStore.setCharacter(response.character)
|
gameStore.setCharacter(response.character)
|
||||||
@ -165,27 +222,51 @@ function loginWithCharacter() {
|
|||||||
|
|
||||||
// Create character logics
|
// Create character logics
|
||||||
function createCharacter() {
|
function createCharacter() {
|
||||||
gameStore.connection?.on('character:create:success', (data: CharacterT) => {
|
if (newCharacterName.value.length < characterCreationSettings.minNameLength || newCharacterName.value.length > characterCreationSettings.maxNameLength) {
|
||||||
gameStore.setCharacter(data)
|
return
|
||||||
isCreateNewCharacterModalOpen.value = false
|
}
|
||||||
})
|
|
||||||
gameStore.connection?.emit('character:create', { name: newCharacterName.value })
|
socketManager.emit(
|
||||||
|
SocketEvent.CHARACTER_CREATE,
|
||||||
|
{
|
||||||
|
name: newCharacterName.value,
|
||||||
|
gender: selectedGender.value,
|
||||||
|
hairId: selectedHairId.value
|
||||||
|
},
|
||||||
|
(success: boolean) => {
|
||||||
|
if (success) {
|
||||||
|
cancelCharacterCreation()
|
||||||
|
socketManager.emit(SocketEvent.CHARACTER_LIST)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch changes for selected character and update hairs
|
// Watch changes for selected character and update hairs
|
||||||
watch(selectedCharacterId, (characterId) => {
|
watch(selectedCharacterId, (characterId) => {
|
||||||
if (!characterId) return
|
if (!characterId) return
|
||||||
// selectedHairId.value = characters.value.find((c) => c.id == characterId)?.characterHairId ?? null
|
newNickname.value = ''
|
||||||
|
isCreatingCharacter.value = false
|
||||||
|
selectedHairId.value = characters.value.find((c) => c.id == characterId)?.characterHair ?? null
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
await playSound('/assets/music/intro.mp3')
|
||||||
const characterHairStorage = new CharacterHairStorage()
|
const characterHairStorage = new CharacterHairStorage()
|
||||||
|
const characterTypeStorage = new CharacterTypeStorage()
|
||||||
characterHairs.value = await characterHairStorage.getAll()
|
characterHairs.value = await characterHairStorage.getAll()
|
||||||
|
|
||||||
|
// Get the first available character type for preview
|
||||||
|
const types = await characterTypeStorage.getAll()
|
||||||
|
const defaultType = types.find((type) => type.isSelectable)
|
||||||
|
if (defaultType) {
|
||||||
|
defaultCharacterTypeId.value = defaultType.id
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
gameStore.connection?.off('character:list')
|
socketManager.off(SocketEvent.CHARACTER_LIST)
|
||||||
gameStore.connection?.off('character:connect')
|
socketManager.off(SocketEvent.CHARACTER_CONNECT)
|
||||||
gameStore.connection?.off('character:create:success')
|
socketManager.off(SocketEvent.CHARACTER_CREATE)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -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>
|
||||||
|
@ -44,13 +44,4 @@ function switchToLogin() {
|
|||||||
currentForm.value = 'login'
|
currentForm.value = 'login'
|
||||||
doesUrlHaveToken.value = false
|
doesUrlHaveToken.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// automatic login because of development
|
|
||||||
onMounted(async () => {
|
|
||||||
const token = useCookies().get('token')
|
|
||||||
if (token) {
|
|
||||||
gameStore.setToken(token)
|
|
||||||
gameStore.initConnection()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -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" @open-teleport="teleportModal?.open" />
|
||||||
ref="toolbar"
|
|
||||||
@save="save"
|
|
||||||
@clear="clear"
|
|
||||||
@open-maps="mapModal?.open"
|
|
||||||
@open-settings="mapSettingsModal?.open"
|
|
||||||
@close-editor="mapEditor.toggleActive"
|
|
||||||
@close-lists="tileList?.close"
|
|
||||||
@closeLists="objectList?.close"
|
|
||||||
@open-tile-list="tileList?.open"
|
|
||||||
@open-map-object-list="objectList?.open"
|
|
||||||
/>
|
|
||||||
<MapList ref="mapModal" @open-create-map="mapSettingsModal?.open" />
|
<MapList ref="mapModal" @open-create-map="mapSettingsModal?.open" />
|
||||||
<TileList ref="tileList" />
|
<TileList />
|
||||||
<ObjectList ref="objectList" />
|
<MapObjectList />
|
||||||
<MapSettings ref="mapSettingsModal" />
|
<MapSettings ref="mapSettingsModal" />
|
||||||
<TeleportModal ref="teleportModal" />
|
<TeleportModal ref="teleportModal" />
|
||||||
</div>
|
</div>
|
||||||
@ -30,30 +19,27 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import config from '@/application/config'
|
import config from '@/application/config'
|
||||||
|
import { SocketEvent } from '@/application/enums'
|
||||||
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
import 'phaser'
|
import 'phaser'
|
||||||
import type { Map as MapT } from '@/application/types'
|
import type { Map as MapT } from '@/application/types'
|
||||||
|
import { downloadCache } from '@/application/utilities'
|
||||||
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 { Game, Scene } from 'phavuer'
|
import { Game, Scene } from 'phavuer'
|
||||||
import { ref, useTemplateRef, watch } from 'vue'
|
import { ref, toRaw, useTemplateRef } from 'vue'
|
||||||
|
|
||||||
const mapStorage = new MapStorage()
|
|
||||||
const mapEditor = useMapEditorComposable()
|
const mapEditor = useMapEditorComposable()
|
||||||
const gameStore = useGameStore()
|
|
||||||
|
|
||||||
const toolbar = useTemplateRef('toolbar')
|
|
||||||
const mapModal = useTemplateRef('mapModal')
|
const mapModal = useTemplateRef('mapModal')
|
||||||
const tileList = useTemplateRef('tileList')
|
|
||||||
const objectList = useTemplateRef('objectList')
|
|
||||||
const mapSettingsModal = useTemplateRef('mapSettingsModal')
|
const mapSettingsModal = useTemplateRef('mapSettingsModal')
|
||||||
const teleportModal = useTemplateRef('teleportModal')
|
const teleportModal = useTemplateRef('teleportModal')
|
||||||
|
|
||||||
@ -64,7 +50,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 +71,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) => {
|
||||||
@ -93,24 +82,18 @@ const preloadScene = async (scene: Phaser.Scene) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
async function save() {
|
||||||
const currentMap = mapEditor.currentMap.value
|
const currentMap = toRaw(mapEditor.currentMap.value)
|
||||||
if (!currentMap) return
|
if (!currentMap) return
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
mapId: currentMap.id,
|
...currentMap,
|
||||||
name: currentMap.name,
|
mapId: currentMap.id
|
||||||
width: currentMap.width,
|
|
||||||
height: currentMap.height,
|
|
||||||
tiles: currentMap.tiles,
|
|
||||||
pvp: currentMap.pvp,
|
|
||||||
mapEffects: currentMap.mapEffects?.map(({ id, effect, strength }) => ({ id, effect, strength })) ?? [],
|
|
||||||
mapEventTiles: currentMap.mapEventTiles?.map(({ id, type, positionX, positionY, teleport }) => ({ id, type, positionX, positionY, teleport })) ?? [],
|
|
||||||
placedMapObjects: currentMap.placedMapObjects?.map(({ id, mapObject, depth, isRotated, positionX, positionY }) => ({ id, mapObject, depth, isRotated, positionX, positionY })) ?? []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStore.connection?.emit('gm:map:update', data, (response: MapT) => {
|
socketManager.emit(SocketEvent.GM_MAP_UPDATE, data, async (response: MapT) => {
|
||||||
mapStorage.update(response.id, response)
|
if (!response.id) return
|
||||||
|
await downloadCache('maps', new MapStorage())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,6 +101,6 @@ function clear() {
|
|||||||
if (!mapEditor.currentMap.value) return
|
if (!mapEditor.currentMap.value) return
|
||||||
|
|
||||||
// Clear placed objects, event tiles and tiles
|
// Clear placed objects, event tiles and tiles
|
||||||
mapEditor.clearMap()
|
mapEditor.triggerClearTiles()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="mb-4 flex flex-col gap-3">
|
|
||||||
<div @click="toggle" class="p-3 bg-gray-300 bg-opacity-50 rounded hover:bg-gray-400 text-white font-default cursor-pointer">
|
|
||||||
<slot name="header" />
|
|
||||||
</div>
|
|
||||||
<transition enter-active-class="transition-all duration-300 ease-in-out" leave-active-class="transition-all duration-300 ease-in-out" enter-from-class="opacity-0 max-h-0" enter-to-class="opacity-100 max-h-96" leave-from-class="opacity-100 max-h-96" leave-to-class="opacity-0 max-h-0">
|
|
||||||
<div v-if="isOpen" class="overflow-hidden">
|
|
||||||
<slot name="content" />
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
const isOpen = ref(false)
|
|
||||||
|
|
||||||
const toggle = () => {
|
|
||||||
isOpen.value = !isOpen.value
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -1,23 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div style="display: none">
|
|
||||||
<img v-for="(url, index) in imageUrls" :key="index" :src="url" alt="" @load="handleImageLoad(index)" @error="handleImageError(index)" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
// Internal array of images to preload
|
|
||||||
const imageUrls = ref<string[]>(['/assets/ui-elements/button-ui-box-textured.svg', '/assets/ui-elements/button-ui-frame-empty.svg', '/assets/ui-elements/button-ui-box-textured-small.svg'])
|
|
||||||
|
|
||||||
const loadedImages = ref<Set<number>>(new Set())
|
|
||||||
|
|
||||||
const handleImageLoad = (index: number) => {
|
|
||||||
loadedImages.value.add(index)
|
|
||||||
console.log(`Image ${index} loaded:`, imageUrls.value[index])
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleImageError = (index: number) => {
|
|
||||||
console.log(`Image ${index} failed to load:`, imageUrls.value[index])
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -1,10 +1,14 @@
|
|||||||
<template></template>
|
<template></template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { CharacterHairStorage, CharacterTypeStorage, MapObjectStorage, MapStorage, SpriteStorage, TileStorage } from '@/storage/storages'
|
import { socketManager } from '@/managers/SocketManager'
|
||||||
|
import { login } from '@/services/authenticationService'
|
||||||
|
import { CharacterHairStorage, CharacterTypeStorage, MapObjectStorage, MapStorage, SoundStorage, SpriteStorage, TileStorage } from '@/storage/storages'
|
||||||
import { TextureStorage } from '@/storage/textureStorage'
|
import { TextureStorage } from '@/storage/textureStorage'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { onMounted, onUnmounted } from 'vue'
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
const mapStorage = new MapStorage()
|
const mapStorage = new MapStorage()
|
||||||
const tileStorage = new TileStorage()
|
const tileStorage = new TileStorage()
|
||||||
const mapObjectStorage = new MapObjectStorage()
|
const mapObjectStorage = new MapObjectStorage()
|
||||||
@ -12,6 +16,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,9 +37,21 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentString.includes('11')) {
|
||||||
|
if (socketManager.token) return
|
||||||
|
const response = await login('root', 'password')
|
||||||
|
|
||||||
|
if (response.success === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
socketManager.setToken(response.token)
|
||||||
|
gameStore.initConnection()
|
||||||
|
}
|
||||||
|
|
||||||
// Reset string after a certain amount of time
|
// Reset string after a certain amount of time
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
currentString = ''
|
currentString = ''
|
||||||
|
@ -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)
|
||||||
|