Compare commits
396 Commits
feature/ne
...
feature/#3
Author | SHA1 | Date | |
---|---|---|---|
4042808d4e | |||
a6d6d894a9 | |||
0c61fe77de | |||
bfb2bcb939 | |||
af5a97f66d | |||
79fa54b1bb | |||
dbb4cae154 | |||
9a8220e4e0 | |||
bc0db8b32b | |||
ad611ef593 | |||
d819a84a37 | |||
15dc331a43 | |||
920baaebde | |||
b569888682 | |||
94eab073e6 | |||
d843b954ab | |||
337446497b | |||
d8805dd775 | |||
4c040c21d6 | |||
d0af83ec60 | |||
2de34d2034 | |||
132121c082 | |||
201f628bfa | |||
af99d66595 | |||
56f30093f6 | |||
8f26a40a0e | |||
110fd4e608 | |||
c1edf31ca0 | |||
90c0ed3141 | |||
bcf0d2832d | |||
8bf67ab168 | |||
f83e2bf8c8 | |||
8b0bf6534e | |||
5e243e5201 | |||
c82db9813e | |||
579749f4e0 | |||
ddc26a021b | |||
2d6b1ff1e0 | |||
16720777c9 | |||
41e7832cbe | |||
e6412d8a65 | |||
faa8e5def9 | |||
beed1d6903 | |||
2ebcc24390 | |||
2e3ff803f6 | |||
dd1cc795de | |||
59243e0e17 | |||
87ffc98cce | |||
0c450b24ed | |||
9459639497 | |||
5f2c7a09b1 | |||
13e8c1b4dd | |||
b27a2e8779 | |||
b3c9e3ca3d | |||
31a91c3f9f | |||
5d4de60f90 | |||
4070bcf048 | |||
04203cb9c1 | |||
592d1df9bf | |||
9413fdbb2f | |||
34caac562c | |||
52dafb8643 | |||
390187f353 | |||
cbd111a05b | |||
5ef11f3157 | |||
c56c2796c4 | |||
c228af7bb6 | |||
f45a51c230 | |||
790a62c600 | |||
82a854e647 | |||
3bcb16fa9c | |||
f79ebedc62 | |||
44b0368276 | |||
b8b985470f | |||
39e00c6feb | |||
6de0bb200d | |||
2a00e206eb | |||
8f9b19ba8b | |||
d997a33b86 | |||
9749b02ccf | |||
f83d5eabee | |||
a9cedba4e0 | |||
49dcd92a9e | |||
d010159989 | |||
275dd95c69 | |||
e3c3d4d420 | |||
87e7f14469 | |||
723aa59142 | |||
c369719564 | |||
2d8c421ac6 | |||
1137c95ff3 | |||
4b56da0fa0 | |||
c21e78c2ec | |||
fcf96a25ae | |||
cf9deebc94 | |||
ca307d4de3 | |||
4c4e8ffe02 | |||
369522fda3 | |||
dc7e20842a | |||
75c9d5f349 | |||
b35794d6d3 | |||
6ba4c1b843 | |||
6a52546a08 | |||
8133bd02df | |||
e720a1098e | |||
48d1d920be | |||
7542fd70ed | |||
9f866fea72 | |||
ec6f3031b8 | |||
838610d041 | |||
fb3a59aa59 | |||
ccb64fc048 | |||
db52bcfff3 | |||
12735756d7 | |||
6383320e8c | |||
557b8aaabb | |||
c09e9ea841 | |||
c2d41a63a7 | |||
122a178feb | |||
909dbf4280 | |||
8add054f63 | |||
04d55f994e | |||
b83c340385 | |||
d5984f1c3f | |||
7071d934b4 | |||
15b212160d | |||
2a2841cf16 | |||
a545018639 | |||
90f3056e08 | |||
7730fd81bd | |||
b195f1399f | |||
3c06f7db97 | |||
6c7864b4d4 | |||
0c9a41c286 | |||
dffdd0542f | |||
d2abf8fda8 | |||
fdbc101f96 | |||
7ff1de4018 | |||
f258c65403 | |||
bab13646ed | |||
adc3eba237 | |||
2097a51f07 | |||
50daf01a01 | |||
14474f7665 | |||
f14d9baaa1 | |||
d2b6d8dcb3 | |||
027fdd7dac | |||
2b40741ca7 | |||
aee18956f3 | |||
cf54ab842a | |||
d25100c810 | |||
cd1daf9345 | |||
0ecd951710 | |||
ff9dcb91b0 | |||
841ec0f3df | |||
90d7252784 | |||
554497ecbc | |||
efeae337ab | |||
ad47b37279 | |||
5e11b67774 | |||
7daefb74eb | |||
4adcf8d61d | |||
e53e154d16 | |||
d65ceba66a | |||
db426bb03e | |||
af26ca5e89 | |||
e4b9bb4d61 | |||
d7f60d7bfc | |||
cfdfa98379 | |||
63889a537a | |||
99bb1555a0 | |||
ac1396304f | |||
09ee9bf01d | |||
09b458eeef | |||
9d95562679 | |||
a9de031673 | |||
8e81ce716b | |||
2c1db56cc4 | |||
4fba3678d6 | |||
d29ca10ba9 | |||
67f83c3447 | |||
8f82bad3fa | |||
d665ac989c | |||
e389534e30 | |||
7d3946e274 | |||
0f46e3b6d2 | |||
6ca82733eb | |||
eb61f45535 | |||
a181fc7fe3 | |||
507d4226ac | |||
5dd9d1e7af | |||
15f9e9861e | |||
7fd334d414 | |||
c7d4b5f2c3 | |||
5747166822 | |||
c010373e5b | |||
57ad9d4889 | |||
f268ac9e5b | |||
fb6e2aa742 | |||
8befce7ffb | |||
e530f69311 | |||
144a513cb6 | |||
2a6321b06b | |||
ba90982e35 | |||
014c08b17a | |||
bdbda6456c | |||
85537840ab | |||
2b7082ac92 | |||
abc58bfa38 | |||
027325f2bf | |||
517e92b07b | |||
6bede8c44e | |||
9e652868ca | |||
35f0dcca64 | |||
9618e07bc6 | |||
791830fd6f | |||
37acf1782b | |||
14aa696197 | |||
cfac1d508b | |||
82cfe5902f | |||
284ca6f64e | |||
967cb1893d | |||
18db005bc1 | |||
c8473fc206 | |||
b5e84c133a | |||
3f75e4acd8 | |||
765d0986bf | |||
95c3a1af61 | |||
45a9d8cfdb | |||
e0a48a089a | |||
69f9944dc7 | |||
9cdfcbcc56 | |||
a614ee6241 | |||
7a51323682 | |||
807bc2066e | |||
fab0b08425 | |||
b074270c75 | |||
30845b80e9 | |||
bde0f74f19 | |||
bc685c63ef | |||
7a922261e3 | |||
3936676f2c | |||
9744083dea | |||
176f31d84a | |||
faad00b2a5 | |||
a61e05592d | |||
5202251ac7 | |||
5e2781b265 | |||
ebd6d96e54 | |||
41005735f9 | |||
78f1c6e6a0 | |||
2d48f83802 | |||
7dccb73698 | |||
7171112881 | |||
9a601b7e2e | |||
5c68b02fff | |||
b86d9dd4ce | |||
93baa10acf | |||
419cf319be | |||
1cd7f28402 | |||
0657dbcb1b | |||
5cf7423a5c | |||
4d88917526 | |||
8f07cf5093 | |||
367d536c52 | |||
3f8c911e9d | |||
689e443b3d | |||
4fead371d7 | |||
b9bcfc719f | |||
9de7af961e | |||
4067ec2585 | |||
fb18841c91 | |||
7b1dcf7ce3 | |||
7546116878 | |||
03fef60621 | |||
574777da80 | |||
2b84bfcad2 | |||
f829cfb883 | |||
c2db9b5469 | |||
6e30a8530a | |||
41f82897a8 | |||
37b97b0aac | |||
c1d9cc3a11 | |||
b54b825422 | |||
0142850983 | |||
2d09715dc4 | |||
ef807982a5 | |||
ae0841889b | |||
bdd2f93175 | |||
10f6dc3802 | |||
700bd57e67 | |||
145143cdc5 | |||
201853a3ec | |||
40c87f0ee3 | |||
736ddddc54 | |||
63758e67b3 | |||
b51aa29bd8 | |||
f9bfbdf735 | |||
2abce7a7e7 | |||
6ec9f8a7bc | |||
8191a039c9 | |||
540425ca44 | |||
1c2e642fe3 | |||
8355c83dc8 | |||
5fcb336835 | |||
90bdf43b64 | |||
e9dfcf7870 | |||
d0c08c25fd | |||
7bb7af9476 | |||
e4186a1bf5 | |||
8c664d7774 | |||
744df2e2dc | |||
b4f9b11143 | |||
18b07d2f46 | |||
9d0f810ab3 | |||
cf3f17dfef | |||
6be1134c8c | |||
6dad7bc9dd | |||
231f19a30f | |||
9c105d6df6 | |||
179ceb0ca0 | |||
680661f07c | |||
c54d2a2da8 | |||
85f0fca2ae | |||
420e63b724 | |||
5d9b4fd19a | |||
b3d68ef562 | |||
baae737d6b | |||
03f8b327c5 | |||
b9a1ce5ab5 | |||
1b650bd733 | |||
b867250580 | |||
2c7a1e27be | |||
0e455f8ffc | |||
8005bc1318 | |||
11e978121f | |||
727ca99b73 | |||
97080d7380 | |||
1a3a53a229 | |||
a926de8466 | |||
5eabb39ec8 | |||
03313cb092 | |||
d58cfa668d | |||
e3e40dd083 | |||
facdd2d1b4 | |||
7d6bd39f29 | |||
608932300f | |||
b5c1c92b04 | |||
c68b129da8 | |||
963c593a1f | |||
a299e22f88 | |||
2007bfd7c5 | |||
4cae045d0d | |||
1fa8b8f06e | |||
4095184b27 | |||
857d56a878 | |||
8087f754b0 | |||
1479d96162 | |||
606e220a9f | |||
6988565484 | |||
fbc4a3dcdb | |||
924d5bdd13 | |||
25a2fd24f3 | |||
64f5ac45dd | |||
937ce939d1 | |||
7d89364104 | |||
f7b8c235d8 | |||
89d83efca4 | |||
ab97e27f27 | |||
ee3e1b55cb | |||
5e109e2a39 | |||
a8e50c993a | |||
ba8af589a7 | |||
301340327a | |||
1e4c58c79e | |||
f87cd063ee | |||
9593298389 | |||
ad4651844d | |||
3748c459f8 | |||
50ea3ecdab | |||
8910390f7b | |||
d820490b2b | |||
2c96caee4f | |||
84939a7d32 | |||
1e3fc2b0f8 | |||
7c8b5f3e82 | |||
570d315bf5 | |||
7871b34c60 | |||
85d64f23eb | |||
bdb6dd0d54 | |||
faf887163a | |||
dd5baa530d | |||
d9947e29cf | |||
1888521762 | |||
48fef2313b | |||
0a99d2c430 |
@ -1,5 +1,6 @@
|
||||
VITE_NAME=Sylvan Quest
|
||||
VITE_DEVELOPMENT=true
|
||||
VITE_NAME=Noxious
|
||||
VITE_DOMAIN=localhost
|
||||
VITE_ENVIRONMENT=development
|
||||
VITE_SERVER_ENDPOINT=http://localhost:4000
|
||||
VITE_TILE_SIZE_X=64
|
||||
VITE_TILE_SIZE_Y=32
|
||||
VITE_TILE_SIZE_WIDTH=64
|
||||
VITE_TILE_SIZE_HEIGHT=32
|
@ -1,13 +0,0 @@
|
||||
/* eslint-env node */
|
||||
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['plugin:vue/vue3-essential', 'eslint:recommended', '@vue/eslint-config-typescript', '@vue/eslint-config-prettier/skip-formatting'],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest'
|
||||
},
|
||||
rules: {
|
||||
'vue/multi-word-component-names': 'off'
|
||||
}
|
||||
}
|
@ -4,5 +4,8 @@
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"printWidth": 300,
|
||||
"trailingComma": "none"
|
||||
"trailingComma": "none",
|
||||
"plugins": ["@ianvs/prettier-plugin-sort-imports"],
|
||||
"importOrderParserPlugins": ["typescript", "jsx", "decorators-legacy", "classProperties"],
|
||||
"importOrderCaseSensitive": false
|
||||
}
|
1
.vscode/extensions.json
vendored
@ -1,7 +1,6 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"Vue.volar",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
||||
|
70
Caddyfile
Normal file
@ -0,0 +1,70 @@
|
||||
{
|
||||
# Global options
|
||||
admin off # Disable admin API
|
||||
|
||||
# Global logging configuration
|
||||
log {
|
||||
output file /var/log/caddy/access.log
|
||||
format json
|
||||
level INFO
|
||||
}
|
||||
}
|
||||
|
||||
noxious.gg {
|
||||
# Root directory for your Vue app
|
||||
root * ./dist
|
||||
|
||||
# Enable compression with optimal settings
|
||||
encode zstd gzip
|
||||
|
||||
# Handle SPA routing
|
||||
try_files {path} /index.html
|
||||
|
||||
# Serve static files with optimizations
|
||||
file_server
|
||||
|
||||
# Enhanced security headers
|
||||
header {
|
||||
# Existing headers with improvements
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
X-XSS-Protection "1; mode=block"
|
||||
X-Content-Type-Options "nosniff"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
|
||||
# Additional security headers
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"
|
||||
|
||||
# Remove server information
|
||||
-Server
|
||||
}
|
||||
|
||||
# Improved cache configuration for static assets
|
||||
@static {
|
||||
file
|
||||
path *.js *.css *.png *.jpg *.jpeg *.gif *.ico *.svg *.woff *.woff2 *.ttf *.eot
|
||||
}
|
||||
header @static {
|
||||
Cache-Control "public, max-age=31536000, immutable"
|
||||
Vary Accept-Encoding
|
||||
}
|
||||
|
||||
# Cache control for HTML files
|
||||
@html {
|
||||
file
|
||||
path *.html
|
||||
}
|
||||
header @html {
|
||||
Cache-Control "no-cache, must-revalidate"
|
||||
}
|
||||
|
||||
# Handle errors
|
||||
handle_errors {
|
||||
respond "{http.error.status_code} {http.error.status_text}" {http.error.status_code}
|
||||
}
|
||||
}
|
||||
|
||||
# Improved redirect configuration
|
||||
www.noxious.gg {
|
||||
redir https://noxious.gg{uri} permanent
|
||||
}
|
32
Dockerfile
@ -1,32 +0,0 @@
|
||||
# Build stage
|
||||
FROM node:22.4.1-alpine as builder
|
||||
WORKDIR /usr/src/app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
|
||||
# Set environment variables
|
||||
ARG VITE_NAME=${VITE_NAME}
|
||||
ENV VITE_NAME=${VITE_NAME}
|
||||
|
||||
ARG VITE_DEVELOPMENT=${VITE_DEVELOPMENT}
|
||||
ENV VITE_DEVELOPMENT=${VITE_DEVELOPMENT}
|
||||
|
||||
ARG VITE_SERVER_ENDPOINT=${VITE_SERVER_ENDPOINT}
|
||||
ENV VITE_SERVER_ENDPOINT=${VITE_SERVER_ENDPOINT}
|
||||
|
||||
ARG VITE_TILE_SIZE_X=${VITE_TILE_SIZE_X}
|
||||
ENV VITE_TILE_SIZE_X=${VITE_TILE_SIZE_X}
|
||||
|
||||
ARG VITE_TILE_SIZE_Y=${VITE_TILE_SIZE_Y}
|
||||
ENV VITE_TILE_SIZE_Y=${VITE_TILE_SIZE_Y}
|
||||
|
||||
# Build the application
|
||||
RUN npm run build-ntc
|
||||
|
||||
# Production stage
|
||||
FROM nginx:1.26.1-alpine
|
||||
COPY --from=builder /usr/src/app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"dockerfilePath" :"./Dockerfile"
|
||||
}
|
@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>Sylvan Quest - Play</title>
|
||||
<title>Noxious - Play</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
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;
|
||||
}
|
||||
}
|
4844
package-lock.json
generated
29
package.json
@ -11,39 +11,35 @@
|
||||
"test:unit": "vitest",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build --force",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^10.5.0",
|
||||
"@vueuse/integrations": "^10.5.0",
|
||||
"axios": "^1.7.7",
|
||||
"dexie": "^4.0.8",
|
||||
"phaser": "^3.86.0",
|
||||
"pinia": "^2.1.6",
|
||||
"socket.io-client": "^4.8.0",
|
||||
"axios": "^1.7.9",
|
||||
"dexie": "^4.0.11",
|
||||
"phaser": "^3.88.2",
|
||||
"phavuer": "^0.16.5",
|
||||
"phaser3-rex-plugins": "^1.80.13",
|
||||
"pinia": "^2.3.1",
|
||||
"sharp": "^0.33.5",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"universal-cookie": "^6.1.3",
|
||||
"vue": "^3.5.12",
|
||||
"zod": "^3.22.2"
|
||||
"vite-plugin-image-optimizer": "^1.1.8",
|
||||
"vue": "^3.5.13",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.10.3",
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.4.0",
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/node": "^20.14.11",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"@vue/eslint-config-prettier": "^9.0.0",
|
||||
"@vue/eslint-config-typescript": "^13.0.0",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"easystarjs": "^0.4.4",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-vue": "^9.27.0",
|
||||
"jsdom": "^24.1.1",
|
||||
"npm-run-all2": "^6.2.3",
|
||||
"phaser3-rex-plugins": "^1.80.8",
|
||||
"phavuer": "^0.16.1",
|
||||
"postcss": "^8.4.47",
|
||||
"prettier": "^3.3.3",
|
||||
"sass": "^1.79.4",
|
||||
@ -51,7 +47,6 @@
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^5.4.9",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-vue-devtools": "^7.5.2",
|
||||
"vitest": "^2.0.3",
|
||||
"vue-tsc": "^1.6.5"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 162 KiB |
@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 16L15 12M15 12L11 8M15 12H3M4.51555 17C6.13007 19.412 8.87958 21 12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3C8.87958 3 6.13007 4.58803 4.51555 7" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 453 B |
Before Width: | Height: | Size: 597 KiB After Width: | Height: | Size: 597 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 600 KiB After Width: | Height: | Size: 600 KiB |
Before Width: | Height: | Size: 600 KiB After Width: | Height: | Size: 600 KiB |
Before Width: | Height: | Size: 599 KiB After Width: | Height: | Size: 599 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 325 B After Width: | Height: | Size: 325 B |
4
public/assets/icons/mapEditor/dropdown-chevron.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17 9.5L12 14.5L7 9.5" stroke="#fff" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 325 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 847 B After Width: | Height: | Size: 847 B |
Before Width: | Height: | Size: 745 B After Width: | Height: | Size: 745 B |
3
public/assets/icons/mapEditor/search.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.3333 17.5L11.0833 12.25C10.6667 12.5833 10.1875 12.8472 9.64583 13.0417C9.10417 13.2361 8.52778 13.3333 7.91667 13.3333C6.40278 13.3333 5.12153 12.809 4.07292 11.7604C3.02431 10.7118 2.5 9.43056 2.5 7.91667C2.5 6.40278 3.02431 5.12153 4.07292 4.07292C5.12153 3.02431 6.40278 2.5 7.91667 2.5C9.43056 2.5 10.7118 3.02431 11.7604 4.07292C12.809 5.12153 13.3333 6.40278 13.3333 7.91667C13.3333 8.52778 13.2361 9.10417 13.0417 9.64583C12.8472 10.1875 12.5833 10.6667 12.25 11.0833L17.5 16.3333L16.3333 17.5ZM7.91667 11.6667C8.95833 11.6667 9.84375 11.3021 10.5729 10.5729C11.3021 9.84375 11.6667 8.95833 11.6667 7.91667C11.6667 6.875 11.3021 5.98958 10.5729 5.26042C9.84375 4.53125 8.95833 4.16667 7.91667 4.16667C6.875 4.16667 5.98958 4.53125 5.26042 5.26042C4.53125 5.98958 4.16667 6.875 4.16667 7.91667C4.16667 8.95833 4.53125 9.84375 5.26042 10.5729C5.98958 11.3021 6.875 11.6667 7.91667 11.6667Z" fill="#808080"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
59
public/assets/icons/mapEditor/settings.svg
Normal file
@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M490.667,405.333h-56.811C424.619,374.592,396.373,352,362.667,352s-61.931,22.592-71.189,53.333H21.333
|
||||
C9.557,405.333,0,414.891,0,426.667S9.557,448,21.333,448h270.144c9.237,30.741,37.483,53.333,71.189,53.333
|
||||
s61.931-22.592,71.189-53.333h56.811c11.797,0,21.333-9.557,21.333-21.333S502.464,405.333,490.667,405.333z M362.667,458.667
|
||||
c-17.643,0-32-14.357-32-32s14.357-32,32-32s32,14.357,32,32S380.309,458.667,362.667,458.667z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M490.667,64h-56.811c-9.259-30.741-37.483-53.333-71.189-53.333S300.736,33.259,291.477,64H21.333
|
||||
C9.557,64,0,73.557,0,85.333s9.557,21.333,21.333,21.333h270.144C300.736,137.408,328.96,160,362.667,160
|
||||
s61.931-22.592,71.189-53.333h56.811c11.797,0,21.333-9.557,21.333-21.333S502.464,64,490.667,64z M362.667,117.333
|
||||
c-17.643,0-32-14.357-32-32c0-17.643,14.357-32,32-32s32,14.357,32,32C394.667,102.976,380.309,117.333,362.667,117.333z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M490.667,234.667H220.523c-9.259-30.741-37.483-53.333-71.189-53.333s-61.931,22.592-71.189,53.333H21.333
|
||||
C9.557,234.667,0,244.224,0,256c0,11.776,9.557,21.333,21.333,21.333h56.811c9.259,30.741,37.483,53.333,71.189,53.333
|
||||
s61.931-22.592,71.189-53.333h270.144c11.797,0,21.333-9.557,21.333-21.333C512,244.224,502.464,234.667,490.667,234.667z
|
||||
M149.333,288c-17.643,0-32-14.357-32-32s14.357-32,32-32c17.643,0,32,14.357,32,32S166.976,288,149.333,288z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
@ -1,3 +1,3 @@
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.91481 9.08504C7.71955 9.2803 7.40297 9.2803 7.20771 9.08504L5.55709 7.43442C5.36183 7.23916 5.04524 7.23916 4.84998 7.43442L2.99578 9.28862C2.80052 9.48389 2.48393 9.48389 2.28867 9.28863L1.00416 8.00412C0.808899 7.80885 0.808899 7.49227 1.00416 7.29701L2.85837 5.4428C3.05363 5.24754 3.05363 4.93096 2.85837 4.7357L0.914865 2.7922C0.719603 2.59693 0.719603 2.28035 0.914865 2.08509L2.07053 0.929423C2.26579 0.734161 2.58238 0.734162 2.77764 0.929424L4.72114 2.87293C4.9164 3.06819 5.23298 3.06819 5.42825 2.87293L7.297 1.00417C7.49226 0.808906 7.80885 0.808906 8.00411 1.00417L9.28862 2.28868C9.48388 2.48394 9.48388 2.80052 9.28862 2.99578L7.41986 4.86454C7.2246 5.0598 7.2246 5.37639 7.41986 5.57165L9.07048 7.22227C9.26574 7.41753 9.26574 7.73411 9.07048 7.92937L7.91481 9.08504Z" fill="white"/>
|
||||
</svg>
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.91481 9.08504C7.71955 9.2803 7.40297 9.2803 7.20771 9.08504L5.55709 7.43442C5.36183 7.23916 5.04524 7.23916 4.84998 7.43442L2.99578 9.28862C2.80052 9.48389 2.48393 9.48389 2.28867 9.28863L1.00416 8.00412C0.808899 7.80885 0.808899 7.49227 1.00416 7.29701L2.85837 5.4428C3.05363 5.24754 3.05363 4.93096 2.85837 4.7357L0.914865 2.7922C0.719603 2.59693 0.719603 2.28035 0.914865 2.08509L2.07053 0.929423C2.26579 0.734161 2.58238 0.734162 2.77764 0.929424L4.72114 2.87293C4.9164 3.06819 5.23298 3.06819 5.42825 2.87293L7.297 1.00417C7.49226 0.808906 7.80885 0.808906 8.00411 1.00417L9.28862 2.28868C9.48388 2.48394 9.48388 2.80052 9.28862 2.99578L7.41986 4.86454C7.2246 5.0598 7.2246 5.37639 7.41986 5.57165L9.07048 7.22227C9.26574 7.41753 9.26574 7.73411 9.07048 7.92937L7.91481 9.08504Z" fill="white"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 915 B After Width: | Height: | Size: 918 B |
Before Width: | Height: | Size: 768 B After Width: | Height: | Size: 768 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 375 B After Width: | Height: | Size: 375 B |
Before Width: | Height: | Size: 652 B After Width: | Height: | Size: 652 B |
Before Width: | Height: | Size: 346 B After Width: | Height: | Size: 346 B |
@ -1 +0,0 @@
|
||||
<svg width="48" xmlns="http://www.w3.org/2000/svg" height="48" id="screenshot-e9346f42-72c8-800c-8004-507b356b7f18" viewBox="0 0 48 48" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-e9346f42-72c8-800c-8004-507b356b7f18" width="1em" height="1em" rx="0" ry="0" style="fill: rgb(0, 0, 0);"><g id="shape-e9346f42-72c8-800c-8004-507b356b7f1b"><defs style="fill: rgb(0, 0, 0);"/></g><g id="shape-e9346f42-72c8-800c-8004-507b356b7f1c"><defs><mask width="1.2" height="1.2" x="-0.1" id="render-3-ipSEnterKey0" data-old-y="-0.1" data-old-width="1.2" data-old-x="-0.1" y="-0.1" data-old-height="1.2"><g class="svg-mask-wrapper" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"><path fill="#fff" stroke="#fff" d="M44 44V4H24v16H4v24z"/><path stroke="#000" d="m21 28l-4 4l4 4"/><path stroke="#000" d="M34 23v9H17"/></g></g></mask></defs><g class="fills" id="fills-e9346f42-72c8-800c-8004-507b356b7f1c"><path d="M0.000,0.000L48.000,0.000L48.000,48.000L0.000,48.000ZZ" mask="url(#render-3-ipSEnterKey0)" style="fill: #696969; fill-opacity: 1;"/></g></g></g></svg>
|
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 2.1 KiB |
3
public/assets/icons/triangle-icon.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="13" height="16" viewBox="0 0 13 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.00044 7.42339L12.0005 1.07252C12.3193 0.888385 12.7271 0.997653 12.9111 1.31652C12.9697 1.41785 13.0005 1.53285 13.0005 1.64985L13.0005 14.3516C13.0005 14.7198 12.702 15.0182 12.3338 15.0182C12.2167 15.0182 12.1018 14.9874 12.0005 14.9289L1.00044 8.57805C0.681573 8.39399 0.572326 7.98625 0.756419 7.66739C0.814932 7.56605 0.899092 7.48185 1.00044 7.42339Z" fill="#4D4D4D"/>
|
||||
</svg>
|
After Width: | Height: | Size: 490 B |
3
public/assets/icons/x-button-gray.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.5859 10.0001L0.792969 2.20718L2.20718 0.792969L10.0001 8.58582L17.793 0.792969L19.2072 2.20718L11.4143 10.0001L19.2072 17.7929L17.793 19.2072L10.0001 11.4143L2.20718 19.2072L0.792969 17.7929L8.5859 10.0001Z" fill="#999999"/>
|
||||
</svg>
|
After Width: | Height: | Size: 340 B |
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 946 KiB |
Before Width: | Height: | Size: 109 B After Width: | Height: | Size: 109 B |
Before Width: | Height: | Size: 696 B After Width: | Height: | Size: 696 B |
Before Width: | Height: | Size: 708 B After Width: | Height: | Size: 708 B |
BIN
public/assets/music/intro.mp3
Normal file
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 274 B |
@ -1 +0,0 @@
|
||||
<svg width="170" xmlns="http://www.w3.org/2000/svg" height="275" id="screenshot-c7008730-586b-8052-8004-79a2f5807f5c" viewBox="0 0 170 275" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-c7008730-586b-8052-8004-79a2f5807f5c"><defs><linearGradient id="fill-color-gradient-render-1-0" x1="0.4238862507773219" y1="0.26897966364321624" x2="0.7513711107948178" y2="0.9146156628521559" gradientTransform=""><stop offset="0" stop-color="#ffffff" stop-opacity="0.4"/><stop offset="1" stop-color="#ffffff" stop-opacity="0"/></linearGradient><pattern patternUnits="userSpaceOnUse" x="0" y="0" width="170" height="275" patternTransform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)" id="fill-0-render-1"><g><rect width="170" height="275" style="fill: url("#fill-color-gradient-render-1-0");"/></g></pattern></defs><g class="fills" id="fills-c7008730-586b-8052-8004-79a2f5807f5c"><path d="M170.000,20.000L170.000,255.000C170.000,266.038,161.038,275.000,150.000,275.000L20.000,275.000C8.962,275.000,0.000,266.038,0.000,255.000L0.000,20.000C0.000,8.962,8.962,0.000,20.000,0.000L150.000,0.000C150.000,0.000,150.000,0.000,150.000,0.000C150.000,11.038,158.962,20.000,170.000,20.000Z" fill="url(#fill-0-render-1)"/></g><g id="strokes-c7008730-586b-8052-8004-79a2f5807f5c" class="strokes"><g class="inner-stroke-shape"><defs><clipPath id="inner-stroke-render-1-c7008730-586b-8052-8004-79a2f5807f5c-0"><use href="#stroke-shape-render-1-c7008730-586b-8052-8004-79a2f5807f5c-0"/></clipPath><path d="M170.000,20.000L170.000,255.000C170.000,266.038,161.038,275.000,150.000,275.000L20.000,275.000C8.962,275.000,0.000,266.038,0.000,255.000L0.000,20.000C0.000,8.962,8.962,0.000,20.000,0.000L150.000,0.000C150.000,0.000,150.000,0.000,150.000,0.000C150.000,11.038,158.962,20.000,170.000,20.000Z" id="stroke-shape-render-1-c7008730-586b-8052-8004-79a2f5807f5c-0" style="fill: none; stroke-width: 4; stroke: rgb(255, 255, 255); stroke-opacity: 1;"/></defs><use href="#stroke-shape-render-1-c7008730-586b-8052-8004-79a2f5807f5c-0" clip-path="url('#inner-stroke-render-1-c7008730-586b-8052-8004-79a2f5807f5c-0')"/></g></g></g></svg>
|
Before Width: | Height: | Size: 2.1 KiB |
@ -1 +0,0 @@
|
||||
<svg width="290" xmlns="http://www.w3.org/2000/svg" height="87" id="screenshot-e9942e24-155b-8096-8004-7eaff9882cd6" viewBox="0 0 290 87" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-e9942e24-155b-8096-8004-7eaff9882cd6"><g class="fills" id="fills-e9942e24-155b-8096-8004-7eaff9882cd6"><path d="M3.485,8.722C7.089,3.457,13.144,0.000,20.000,0.000L270.000,0.000C281.038,0.000,290.000,8.962,290.000,20.000L290.000,67.000C290.000,78.038,281.038,87.000,270.000,87.000L20.000,87.000C13.166,87.000,7.128,83.565,3.520,78.329C21.157,76.589,35.000,61.648,35.000,43.500C35.000,25.390,21.216,10.475,3.485,8.722ZM0.000,67.000L0.000,20.000"/></g><g id="strokes-e9942e24-155b-8096-8004-7eaff9882cd6" class="strokes"><g class="inner-stroke-shape"><defs><clipPath id="inner-stroke-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0"><use href="#stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0"/></clipPath><path d="M3.485,8.722C7.089,3.457,13.144,0.000,20.000,0.000L270.000,0.000C281.038,0.000,290.000,8.962,290.000,20.000L290.000,67.000C290.000,78.038,281.038,87.000,270.000,87.000L20.000,87.000C13.166,87.000,7.128,83.565,3.520,78.329C21.157,76.589,35.000,61.648,35.000,43.500C35.000,25.390,21.216,10.475,3.485,8.722ZM0.000,67.000L0.000,20.000" id="stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0" style="fill: none; stroke-width: 6; stroke: rgb(77 77 77); stroke-opacity: 1;"/></defs><use href="#stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0" clip-path="url('#inner-stroke-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0')"/></g></g></g></svg>
|
Before Width: | Height: | Size: 1.6 KiB |
@ -1 +0,0 @@
|
||||
<svg width="290" xmlns="http://www.w3.org/2000/svg" height="87" id="screenshot-e9942e24-155b-8096-8004-7eaff9882cd6" viewBox="0 0 290 87" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-e9942e24-155b-8096-8004-7eaff9882cd6"><g class="fills" id="fills-e9942e24-155b-8096-8004-7eaff9882cd6"><path d="M3.485,8.722C7.089,3.457,13.144,0.000,20.000,0.000L270.000,0.000C281.038,0.000,290.000,8.962,290.000,20.000L290.000,67.000C290.000,78.038,281.038,87.000,270.000,87.000L20.000,87.000C13.166,87.000,7.128,83.565,3.520,78.329C21.157,76.589,35.000,61.648,35.000,43.500C35.000,25.390,21.216,10.475,3.485,8.722ZM0.000,67.000L0.000,20.000" style="fill: rgb(127, 127, 127); fill-opacity: 0.7;"/></g><g id="strokes-e9942e24-155b-8096-8004-7eaff9882cd6" class="strokes"><g class="inner-stroke-shape"><defs><clipPath id="inner-stroke-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0"><use href="#stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0"/></clipPath><path d="M3.485,8.722C7.089,3.457,13.144,0.000,20.000,0.000L270.000,0.000C281.038,0.000,290.000,8.962,290.000,20.000L290.000,67.000C290.000,78.038,281.038,87.000,270.000,87.000L20.000,87.000C13.166,87.000,7.128,83.565,3.520,78.329C21.157,76.589,35.000,61.648,35.000,43.500C35.000,25.390,21.216,10.475,3.485,8.722ZM0.000,67.000L0.000,20.000" id="stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0" style="fill: none; stroke-width: 6; stroke: rgb(255, 255, 255); stroke-opacity: 1;"/></defs><use href="#stroke-shape-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0" clip-path="url('#inner-stroke-render-1-e9942e24-155b-8096-8004-7eaff9882cd6-0')"/></g></g></g></svg>
|
Before Width: | Height: | Size: 1.6 KiB |
@ -1 +0,0 @@
|
||||
<svg width="1508.086" xmlns="http://www.w3.org/2000/svg" height="1511.251" id="screenshot-0d120e2a-8725-8061-8004-79728483f7ea" viewBox="-201.784 -208.012 1508.086 1511.251" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-0d120e2a-8725-8061-8004-79728483f7ea" width="800px" height="800px" rx="0" ry="0" style="opacity: 0.3; fill: rgb(0, 0, 0);"><g id="shape-0d120e2a-8725-8061-8004-79728484b3fe"><g class="fills" id="fills-0d120e2a-8725-8061-8004-79728484b3fe"><path d="M1190.359,745.690L1043.367,575.945L1133.504,630.603C1099.180,585.722,1047.978,532.622,975.722,469.519L780.783,401.898L896.468,404.538C851.234,371.382,797.068,339.090,738.130,311.073L601.350,337.338L689.349,289.039C627.425,263.088,562.143,241.922,497.713,229.160C430.172,215.674,363.453,211.534,303.512,221.641L314.753,151.382C271.664,177.012,239.130,209.992,214.226,251.017L204.390,177.710C166.181,212.950,148.095,250.172,143.131,301.343C69.092,307.974,-2.300,327.925,-73.861,347.005L-63.628,384.898C361.675,238.903,753.109,407.667,987.467,615.054L960.305,643.506C749.259,458.743,490.332,358.712,193.406,380.541C209.110,415.226,228.858,447.126,251.288,474.785L371.998,430.671L277.417,504.449C294.635,521.771,312.591,536.670,331.111,548.765L396.081,470.998L358.366,564.253C377.625,573.924,397.183,580.217,416.674,582.837C534.164,599.232,652.310,618.566,782.785,703.535L773.618,601.955L831.163,737.792C852.261,754.489,876.370,771.191,897.168,782.701L861.169,684.190L960.409,811.519C976.589,817.512,992.991,822.477,1008.953,826.486C1066.083,840.287,1120.015,842.594,1157.256,819.002C1175.975,807.393,1189.205,786.963,1190.791,762.853C1191.080,756.933,1190.907,750.762,1190.359,745.690ZZ" style="fill: rgb(13 109 105);"/></g></g></g></svg>
|
Before Width: | Height: | Size: 1.7 KiB |
BIN
public/assets/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
BIN
public/assets/tlogo.png
Normal file
After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 470 KiB After Width: | Height: | Size: 470 KiB |
Before Width: | Height: | Size: 471 KiB After Width: | Height: | Size: 471 KiB |
Before Width: | Height: | Size: 598 KiB After Width: | Height: | Size: 598 KiB |
25
public/assets/ui-elements/character-select-ui-shape.svg
Normal file
@ -0,0 +1,25 @@
|
||||
<svg width="190" height="202" viewBox="0 0 190 202" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 8H8V0C7.40741 4.14815 4.14815 7.40741 0 8Z" fill="#1A1A1A"/>
|
||||
<path d="M7.55 0V0C7.55 4.16975 4.16975 7.55 0 7.55V7.55" stroke="#4D4D4D"/>
|
||||
<mask id="path-3-inside-1_632_705" fill="white">
|
||||
<path d="M0 8H8V194H0V8Z"/>
|
||||
</mask>
|
||||
<path d="M0 8H8V194H0V8Z" fill="#1A1A1A"/>
|
||||
<path d="M1 194V8H-1V194H1Z" fill="#4D4D4D" mask="url(#path-3-inside-1_632_705)"/>
|
||||
<path d="M0 194H8V202C7.40741 197.852 4.14815 194.593 0 194Z" fill="#1A1A1A"/>
|
||||
<path d="M7.55 202V202C7.55 197.83 4.16975 194.45 0 194.45V194.45" stroke="#4D4D4D"/>
|
||||
<mask id="path-7-inside-2_632_705" fill="white">
|
||||
<path d="M8 0H182V202H8V0Z"/>
|
||||
</mask>
|
||||
<path d="M8 0H182V202H8V0Z" fill="#1A1A1A"/>
|
||||
<path d="M8 1H182V-1H8V1ZM182 201H8V203H182V201Z" fill="#4D4D4D" mask="url(#path-7-inside-2_632_705)"/>
|
||||
<path d="M190 8H182V0C182.593 4.14815 185.852 7.40741 190 8Z" fill="#1A1A1A"/>
|
||||
<path d="M182.45 0V0C182.45 4.16975 185.83 7.55 190 7.55V7.55" stroke="#4D4D4D"/>
|
||||
<mask id="path-11-inside-3_632_705" fill="white">
|
||||
<path d="M190 8H182V194H190V8Z"/>
|
||||
</mask>
|
||||
<path d="M190 8H182V194H190V8Z" fill="#1A1A1A"/>
|
||||
<path d="M189 194V8H191V194H189Z" fill="#4D4D4D" mask="url(#path-11-inside-3_632_705)"/>
|
||||
<path d="M190 194H182V202C182.593 197.852 185.852 194.593 190 194Z" fill="#1A1A1A"/>
|
||||
<path d="M182.45 202V202C182.45 197.83 185.83 194.45 190 194.45V194.45" stroke="#4D4D4D"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 470 KiB After Width: | Height: | Size: 470 KiB |
23
public/assets/ui-elements/login-ui-box-inner.svg
Normal file
After Width: | Height: | Size: 302 KiB |
23
public/assets/ui-elements/login-ui-box-outer.svg
Normal file
After Width: | Height: | Size: 400 KiB |
Before Width: | Height: | Size: 453 KiB After Width: | Height: | Size: 454 KiB |
@ -1,18 +0,0 @@
|
||||
<svg width="190" height="202" viewBox="0 0 190 202" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_i_598_514)">
|
||||
<path d="M0 3.60002C0 1.61179 1.61177 0 3.6 0H186.4C188.388 0 190 1.61177 190 3.6V193.658C190 195.646 188.388 197.258 186.4 197.258H184.894C183.584 197.258 182.523 198.32 182.523 199.629C182.523 200.938 181.461 202 180.152 202H9.84847C8.53901 202 7.47748 200.938 7.47748 199.629C7.47748 198.32 6.41596 197.258 5.1065 197.258H3.6C1.61178 197.258 0 195.646 0 193.658V3.60002Z" fill="#181818"/>
|
||||
</g>
|
||||
<path d="M0.3 3.60002C0.3 1.77747 1.77746 0.3 3.6 0.3H186.4C188.223 0.3 189.7 1.77746 189.7 3.6V193.658C189.7 195.481 188.223 196.958 186.4 196.958H184.894C183.418 196.958 182.223 198.154 182.223 199.629C182.223 200.773 181.295 201.7 180.152 201.7H9.84847C8.7047 201.7 7.77748 200.773 7.77748 199.629C7.77748 198.154 6.58164 196.958 5.1065 196.958H3.6C1.77746 196.958 0.3 195.481 0.3 193.658V3.60002Z" stroke="#454442" stroke-width="0.6"/>
|
||||
<defs>
|
||||
<filter id="filter0_i_598_514" x="0" y="0" width="190" height="204.4" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="2.4"/>
|
||||
<feGaussianBlur stdDeviation="2.34"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.13 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_598_514"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 301 KiB |
Before Width: | Height: | Size: 400 KiB |
Before Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 2.1 MiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 1.1 MiB |
49
src/App.vue
@ -1,56 +1,65 @@
|
||||
<template>
|
||||
<Debug />
|
||||
<Notifications />
|
||||
<BackgroundImageLoader />
|
||||
<GmPanel v-if="gameStore.character?.role === 'gm'" />
|
||||
<component :is="currentScreen" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useZoneEditorStore } from '@/stores/zoneEditorStore'
|
||||
import Notifications from '@/components/utilities/Notifications.vue'
|
||||
import BackgroundImageLoader from '@/components/utilities/BackgroundImageLoader.vue'
|
||||
import GmPanel from '@/components/gameMaster/GmPanel.vue'
|
||||
import Login from '@/components/screens/Login.vue'
|
||||
import Characters from '@/components/screens/Characters.vue'
|
||||
import Game from '@/components/screens/Game.vue'
|
||||
import ZoneEditor from '@/components/screens/ZoneEditor.vue'
|
||||
import Loading from '@/components/screens/Loading.vue'
|
||||
import Login from '@/components/screens/Login.vue'
|
||||
import MapEditor from '@/components/screens/MapEditor.vue'
|
||||
import Debug from '@/components/utilities/Debug.vue'
|
||||
import Notifications from '@/components/utilities/Notifications.vue'
|
||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||
import { useSoundComposable } from '@/composables/useSoundComposable'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { computed, watch } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const zoneEditorStore = useZoneEditorStore()
|
||||
|
||||
const mapEditor = useMapEditorComposable()
|
||||
const { playSound } = useSoundComposable()
|
||||
|
||||
const currentScreen = computed(() => {
|
||||
if (!gameStore.connection) return Login
|
||||
if (!gameStore.token) return Login
|
||||
if (!gameStore.game.isLoaded) return Loading
|
||||
if (!socketManager.connection) return Login
|
||||
if (!socketManager.token) return Login
|
||||
if (!gameStore.character) return Characters
|
||||
if (zoneEditorStore.active) return ZoneEditor
|
||||
if (mapEditor.active.value) return MapEditor
|
||||
return Game
|
||||
})
|
||||
|
||||
// Watch zoneEditorStore.active and empty gameStore.game.loadedAssets
|
||||
// Watch mapEditor.active and empty gameStore.game.loadedAssets
|
||||
watch(
|
||||
() => zoneEditorStore.active,
|
||||
() => mapEditor.active.value,
|
||||
() => {
|
||||
gameStore.game.loadedAssets = []
|
||||
gameStore.game.loadedTextures = []
|
||||
}
|
||||
)
|
||||
|
||||
// #209: Play sound when a button is pressed
|
||||
addEventListener('click', (event) => {
|
||||
if (!(event.target instanceof HTMLButtonElement)) {
|
||||
return
|
||||
const classList = ['btn-cyan', 'btn-red', 'btn-indigo', 'btn-empty', 'btn-sound']
|
||||
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
|
||||
addEventListener('keydown', (event) => {
|
||||
if (gameStore.character?.role !== 'gm') return // Only allow toggling the gm panel if the character is a gm
|
||||
|
||||
// Check if no input is active
|
||||
if (event.repeat || event.isComposing || event.defaultPrevented) return
|
||||
// Check if no input is active or focus is on an input
|
||||
if (event.repeat || event.isComposing || event.defaultPrevented || document.activeElement?.tagName.toUpperCase() === 'INPUT' || document.activeElement?.tagName.toUpperCase() === 'TEXTAREA') {
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === 'G') {
|
||||
gameStore.toggleGmPanel()
|
||||
|
10
src/application/config.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export default {
|
||||
name: import.meta.env.VITE_NAME,
|
||||
domain: import.meta.env.VITE_DOMAIN,
|
||||
environment: import.meta.env.VITE_ENVIRONMENT,
|
||||
server_endpoint: import.meta.env.VITE_SERVER_ENDPOINT,
|
||||
tile_size: {
|
||||
width: Number(import.meta.env.VITE_TILE_SIZE_WIDTH),
|
||||
height: Number(import.meta.env.VITE_TILE_SIZE_HEIGHT)
|
||||
}
|
||||
}
|
63
src/application/enums.ts
Normal file
@ -0,0 +1,63 @@
|
||||
export enum Direction {
|
||||
POSITIVE,
|
||||
NEGATIVE,
|
||||
UNCHANGED
|
||||
}
|
||||
|
||||
export enum SocketEvent {
|
||||
CONNECT_ERROR = 'connect_error',
|
||||
RECONNECT_FAILED = 'reconnect_failed',
|
||||
CLOSE = '52',
|
||||
DATA = '51',
|
||||
CHARACTER_CONNECT = '50',
|
||||
CHARACTER_CREATE = '49',
|
||||
CHARACTER_DELETE = '48',
|
||||
CHARACTER_LIST = '47',
|
||||
GM_CHARACTERHAIR_CREATE = '46',
|
||||
GM_CHARACTERHAIR_REMOVE = '45',
|
||||
GM_CHARACTERHAIR_LIST = '44',
|
||||
GM_CHARACTERHAIR_UPDATE = '43',
|
||||
GM_CHARACTERTYPE_CREATE = '42',
|
||||
GM_CHARACTERTYPE_REMOVE = '41',
|
||||
GM_CHARACTERTYPE_LIST = '40',
|
||||
GM_CHARACTERTYPE_UPDATE = '39',
|
||||
GM_ITEM_CREATE = '38',
|
||||
GM_ITEM_REMOVE = '37',
|
||||
GM_ITEM_LIST = '36',
|
||||
GM_ITEM_UPDATE = '35',
|
||||
GM_MAPOBJECT_LIST = '34',
|
||||
GM_MAPOBJECT_REMOVE = '33',
|
||||
GM_MAPOBJECT_UPDATE = '32',
|
||||
GM_MAPOBJECT_UPLOAD = '31',
|
||||
GM_SPRITE_COPY = '30',
|
||||
GM_SPRITE_CREATE = '29',
|
||||
GM_SPRITE_DELETE = '28',
|
||||
GM_SPRITE_LIST = '27',
|
||||
GM_SPRITE_UPDATE = '26',
|
||||
GM_TILE_DELETE = '25',
|
||||
GM_TILE_LIST = '24',
|
||||
GM_TILE_UPDATE = '23',
|
||||
GM_TILE_UPLOAD = '22',
|
||||
GM_MAP_CREATE = '21',
|
||||
GM_MAP_DELETE = '20',
|
||||
GM_MAP_REQUEST = '19',
|
||||
GM_MAP_UPDATE = '18',
|
||||
MAP_CHARACTER_MOVEERROR = '17',
|
||||
DISCONNECT = 'disconnect',
|
||||
USER_DISCONNECT = '15',
|
||||
LOGIN = '14',
|
||||
LOGGED_IN = '13',
|
||||
NOTIFICATION = '12',
|
||||
DATE = '11',
|
||||
FAILED = '10',
|
||||
COMPLETED = '9',
|
||||
CONNECTION = 'connection',
|
||||
WEATHER = '7',
|
||||
CHARACTER_DISCONNECT = '6',
|
||||
MAP_CHARACTER_ATTACK = '5',
|
||||
MAP_CHARACTER_TELEPORT = '4',
|
||||
MAP_CHARACTER_JOIN = '3',
|
||||
MAP_CHARACTER_LEAVE = '2',
|
||||
MAP_CHARACTER_MOVE = '1',
|
||||
CHAT_MESSAGE = '0'
|
||||
}
|
@ -1,18 +1,28 @@
|
||||
export type UUID = `${string}-${string}-${string}-${string}-${string}`
|
||||
|
||||
export type Notification = {
|
||||
id?: string
|
||||
title?: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
export type AssetDataT = {
|
||||
export type HttpResponse<T> = {
|
||||
success: boolean
|
||||
message?: string
|
||||
data?: T
|
||||
}
|
||||
|
||||
export type TextureData = {
|
||||
key: string
|
||||
data: string
|
||||
group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
|
||||
data: string // URL or Base64 encoded blob
|
||||
group: 'tiles' | 'map_objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
|
||||
updatedAt: Date
|
||||
isAnimated?: boolean
|
||||
frameCount?: number
|
||||
originX?: number
|
||||
originY?: number
|
||||
frameRate?: number
|
||||
frameWidth?: number
|
||||
frameHeight?: number
|
||||
frameCount?: number
|
||||
}
|
||||
|
||||
export type Tile = {
|
||||
@ -23,97 +33,92 @@ export type Tile = {
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
export type Object = {
|
||||
export type MapObject = {
|
||||
id: string
|
||||
name: string
|
||||
tags: any | null
|
||||
tags: string[]
|
||||
pivotPoints: { x: number; y: number }[]
|
||||
originX: number
|
||||
originY: number
|
||||
isAnimated: boolean
|
||||
frameSpeed: number
|
||||
frameRate: number
|
||||
frameWidth: number
|
||||
frameHeight: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
ZoneObject: ZoneObject[]
|
||||
}
|
||||
|
||||
export type Item = {
|
||||
id: string
|
||||
name: string
|
||||
description: string | null
|
||||
itemType: ItemType
|
||||
stackable: boolean
|
||||
rarity: ItemRarity
|
||||
sprite?: Sprite
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
characters: CharacterItem[]
|
||||
}
|
||||
|
||||
export type Zone = {
|
||||
id: number
|
||||
export type ItemType = 'WEAPON' | 'HELMET' | 'CHEST' | 'LEGS' | 'BOOTS' | 'GLOVES' | 'RING' | 'NECKLACE'
|
||||
export type ItemRarity = 'COMMON' | 'UNCOMMON' | 'RARE' | 'EPIC' | 'LEGENDARY'
|
||||
|
||||
export type Map = {
|
||||
id: string
|
||||
name: string
|
||||
width: number
|
||||
height: number
|
||||
tiles: any | null
|
||||
tiles: string[][]
|
||||
pvp: boolean
|
||||
zoneEffects: ZoneEffect[]
|
||||
zoneEventTiles: ZoneEventTile[]
|
||||
zoneObjects: ZoneObject[]
|
||||
mapEffects: MapEffect[]
|
||||
mapEventTiles: MapEventTile[]
|
||||
placedMapObjects: PlacedMapObject[]
|
||||
characters: Character[]
|
||||
chats: Chat[]
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
export type ZoneEffect = {
|
||||
export type MapEffect = {
|
||||
id: string
|
||||
zoneId: number
|
||||
zone: Zone
|
||||
effect: string
|
||||
strength: number
|
||||
}
|
||||
|
||||
export type ZoneObject = {
|
||||
export type PlacedMapObject = {
|
||||
id: string
|
||||
zoneId: number
|
||||
zone: Zone
|
||||
objectId: string
|
||||
object: Object
|
||||
depth: number
|
||||
mapObject: MapObject | string
|
||||
isRotated: boolean
|
||||
positionX: number
|
||||
positionY: number
|
||||
}
|
||||
|
||||
export enum ZoneEventTileType {
|
||||
export enum MapEventTileType {
|
||||
BLOCK = 'BLOCK',
|
||||
TELEPORT = 'TELEPORT',
|
||||
NPC = 'NPC',
|
||||
ITEM = 'ITEM'
|
||||
}
|
||||
|
||||
export type ZoneEventTile = {
|
||||
export type MapEventTile = {
|
||||
id: string
|
||||
zoneId: number
|
||||
zone: Zone
|
||||
type: ZoneEventTileType
|
||||
map: string
|
||||
type: MapEventTileType
|
||||
positionX: number
|
||||
positionY: number
|
||||
teleport?: ZoneEventTileTeleport
|
||||
teleport?: MapEventTileTeleport
|
||||
}
|
||||
|
||||
export type ZoneEventTileTeleport = {
|
||||
export type MapEventTileTeleport = {
|
||||
id: string
|
||||
zoneEventTileId: string
|
||||
zoneEventTile: ZoneEventTile
|
||||
toZoneId: number
|
||||
toZone: Zone
|
||||
mapEventTile: MapEventTile
|
||||
toMap: Map
|
||||
toPositionX: number
|
||||
toPositionY: number
|
||||
toRotation: number
|
||||
}
|
||||
|
||||
export type User = {
|
||||
id: number
|
||||
id: string
|
||||
username: string
|
||||
password: string
|
||||
characters: Character[]
|
||||
@ -133,20 +138,27 @@ export enum CharacterRace {
|
||||
}
|
||||
|
||||
export type CharacterType = {
|
||||
id: number
|
||||
id: string
|
||||
name: string
|
||||
gender: CharacterGender
|
||||
race: CharacterRace
|
||||
characters: Character[]
|
||||
spriteId?: string
|
||||
isSelectable: boolean
|
||||
sprite?: Sprite
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
export type CharacterHair = {
|
||||
id: string
|
||||
name: string
|
||||
sprite?: Sprite
|
||||
gender: CharacterGender
|
||||
isSelectable: boolean
|
||||
}
|
||||
|
||||
export type Character = {
|
||||
id: number
|
||||
userId: number
|
||||
id: string
|
||||
userid: string
|
||||
user: User
|
||||
name: string
|
||||
hitpoints: number
|
||||
@ -158,27 +170,42 @@ export type Character = {
|
||||
positionX: number
|
||||
positionY: number
|
||||
rotation: number
|
||||
zoneId: number
|
||||
zone: Zone
|
||||
characterTypeId: number | null
|
||||
characterType: CharacterType | null
|
||||
characterType: UUID | null
|
||||
characterHair: UUID | null
|
||||
map: UUID
|
||||
chats: Chat[]
|
||||
items: CharacterItem[]
|
||||
equipment: CharacterEquipment[]
|
||||
}
|
||||
|
||||
export type ExtendedCharacter = Character & {
|
||||
isMoving?: boolean
|
||||
export type MapCharacter = {
|
||||
character: Character
|
||||
isMoving: boolean
|
||||
isAttacking?: boolean
|
||||
}
|
||||
|
||||
export type CharacterItem = {
|
||||
id: number
|
||||
characterId: number
|
||||
id: string
|
||||
character: Character
|
||||
itemId: string
|
||||
item: Item
|
||||
quantity: number
|
||||
}
|
||||
|
||||
export type CharacterEquipment = {
|
||||
id: string
|
||||
slot: CharacterEquipmentSlotType
|
||||
characterItem: CharacterItem
|
||||
}
|
||||
|
||||
export enum CharacterEquipmentSlotType {
|
||||
HEAD = 'HEAD',
|
||||
BODY = 'BODY',
|
||||
ARMS = 'ARMS',
|
||||
LEGS = 'LEGS',
|
||||
NECK = 'NECK',
|
||||
RING = 'RING'
|
||||
}
|
||||
|
||||
export type Sprite = {
|
||||
id: string
|
||||
name: string
|
||||
@ -188,46 +215,45 @@ export type Sprite = {
|
||||
characterTypes: CharacterType[]
|
||||
}
|
||||
|
||||
export interface SpriteImage {
|
||||
url: string
|
||||
offset: {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
}
|
||||
|
||||
export type SpriteAction = {
|
||||
id: string
|
||||
spriteId: string
|
||||
sprite: Sprite
|
||||
sprite: string
|
||||
action: string
|
||||
sprites: string[]
|
||||
sprites: SpriteImage[]
|
||||
originX: number
|
||||
originY: number
|
||||
isAnimated: boolean
|
||||
isLooping: boolean
|
||||
frameWidth: number
|
||||
frameHeight: number
|
||||
frameSpeed: number
|
||||
frameRate: number
|
||||
}
|
||||
|
||||
export type Chat = {
|
||||
id: number
|
||||
characterId: number
|
||||
id: string
|
||||
character: Character
|
||||
zoneId: number
|
||||
zone: Zone
|
||||
map: Map
|
||||
message: string
|
||||
createdAt: Date
|
||||
}
|
||||
|
||||
export type ChatMessage = {
|
||||
character: Character
|
||||
message: string
|
||||
}
|
||||
|
||||
export type WorldSettings = {
|
||||
date: Date
|
||||
isRainEnabled: boolean
|
||||
isFogEnabled: boolean
|
||||
fogDensity: number
|
||||
weatherState: WeatherState
|
||||
}
|
||||
|
||||
export type WeatherState = {
|
||||
isRainEnabled: boolean
|
||||
rainPercentage: number
|
||||
isFogEnabled: boolean
|
||||
fogDensity: number
|
||||
}
|
||||
|
||||
export type mapLoadData = {
|
||||
mapId: string
|
||||
characters: MapCharacter[]
|
||||
}
|
45
src/application/utilities.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import config from '@/application/config'
|
||||
import type { HttpResponse } from '@/application/types'
|
||||
import type { BaseStorage } from '@/storage/baseStorage'
|
||||
|
||||
export function uuidv4() {
|
||||
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) => (+c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))).toString(16))
|
||||
}
|
||||
|
||||
export function unduplicateArray(array: any[]) {
|
||||
const arrayToProcess = typeof array.flat === 'function' ? array.flat() : array
|
||||
return [...new Set(arrayToProcess)]
|
||||
}
|
||||
|
||||
export async function downloadCache<T extends { id: string; updatedAt: Date }>(endpoint: string, storage: BaseStorage<T>) {
|
||||
const request = await fetch(`${config.server_endpoint}/cache/${endpoint}`)
|
||||
const response = (await request.json()) as HttpResponse<T[]>
|
||||
|
||||
if (!response.success) {
|
||||
console.error(`Failed to download ${endpoint}:`, response.message)
|
||||
return
|
||||
}
|
||||
|
||||
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) {
|
||||
let overwrite = false
|
||||
const existingItem = await storage.getById(item.id)
|
||||
|
||||
if (!existingItem || item.updatedAt > existingItem.updatedAt) {
|
||||
overwrite = true
|
||||
}
|
||||
|
||||
await storage.add(item, overwrite)
|
||||
}
|
||||
}
|
@ -23,6 +23,14 @@ body {
|
||||
|
||||
// Disable pinch zoom
|
||||
touch-action: pan-x pan-y;
|
||||
|
||||
// Add custom focus outline
|
||||
*:focus-visible {
|
||||
@apply outline-gray-300;
|
||||
@apply outline;
|
||||
@apply outline-offset-2;
|
||||
@apply rounded-sm;
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
@ -45,7 +53,7 @@ label {
|
||||
|
||||
button,
|
||||
a {
|
||||
@apply font-medium drop-shadow-20;
|
||||
@apply font-medium;
|
||||
}
|
||||
|
||||
button,
|
||||
@ -58,13 +66,20 @@ input {
|
||||
appearance: textfield;
|
||||
}
|
||||
&[type='number']::-webkit-inner-spin-button,
|
||||
&[type='number']::-webkit-outer-spin-button {
|
||||
&[type='number']::-webkit-outer-spin-button,
|
||||
&[type='radio'] {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
}
|
||||
|
||||
.input-field {
|
||||
@apply px-4 py-2.5 text-base leading-5 focus-visible:outline-none bg-gray border border-solid border-gray-500 rounded text-gray-300;
|
||||
@apply px-4 py-2.5 text-base leading-5 bg-gray border border-solid border-gray-500 rounded text-gray-300 font-default;
|
||||
&:focus-visible {
|
||||
@apply outline-none border-cyan rounded bg-gray-900;
|
||||
}
|
||||
&::placeholder {
|
||||
@apply focus-visible:text-gray-300/50;
|
||||
}
|
||||
&.inactive {
|
||||
@apply bg-gray-600/50 hover:cursor-not-allowed;
|
||||
&::placeholder {
|
||||
@ -73,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 {
|
||||
@apply w-full flex flex-col mb-5;
|
||||
label {
|
||||
@ -99,11 +120,20 @@ button {
|
||||
}
|
||||
|
||||
&.btn-red {
|
||||
@apply bg-red text-gray-50 text-base leading-5 rounded py-2.5;
|
||||
@apply bg-red-300 text-gray-50 text-base leading-5 rounded py-2.5;
|
||||
|
||||
&.active,
|
||||
&:hover {
|
||||
@apply bg-red-300;
|
||||
@apply bg-red-400;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-indigo {
|
||||
@apply bg-indigo-500 text-gray-50 text-base leading-5 rounded py-2.5;
|
||||
|
||||
&.active,
|
||||
&:hover {
|
||||
@apply bg-indigo-600;
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,8 +141,9 @@ button {
|
||||
@apply text-gray-50 border-2 border-solid border-gray-500 text-base leading-5 rounded py-2.5;
|
||||
|
||||
&.active,
|
||||
&.selected,
|
||||
&:hover {
|
||||
@apply bg-gray-700 border-gray-700;
|
||||
@apply bg-gray border-gray;
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,14 +154,34 @@ button {
|
||||
&.eye-open {
|
||||
@apply bg-[url('/assets/icons/eye-closed.svg')] w-5 h-4 right-2.5;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
@apply bg-gray-500 border-gray-400;
|
||||
}
|
||||
}
|
||||
|
||||
.character {
|
||||
&.active {
|
||||
@apply pr-px border-r-0;
|
||||
.character.active {
|
||||
@apply bg-gray bg-none;
|
||||
}
|
||||
|
||||
.list-open {
|
||||
@apply w-[calc(75%_-_40px)] max-xl:w-[calc(100%_-_360px)];
|
||||
}
|
||||
|
||||
.hair-deselect:has(:checked) {
|
||||
img {
|
||||
@apply brightness-200;
|
||||
}
|
||||
}
|
||||
|
||||
.default-border {
|
||||
@apply border border-solid border-gray-500;
|
||||
}
|
||||
|
||||
.center-element {
|
||||
@apply absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2;
|
||||
}
|
||||
|
||||
.text-pixel {
|
||||
@apply text-white font-ui drop-shadow-pixel-black;
|
||||
}
|
||||
@ -139,6 +190,15 @@ button {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
.scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
@apply block w-0.5 bg-gray-300 rounded-sm;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
@apply bg-gray-175;
|
||||
}
|
||||
}
|
||||
|
||||
canvas {
|
||||
image-rendering: -moz-crisp-edges;
|
||||
image-rendering: -webkit-crisp-edges;
|
||||
|
@ -1,205 +0,0 @@
|
||||
<template>
|
||||
<Scene name="effects" @preload="preloadScene" @create="createScene" @update="updateScene" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Scene } from 'phavuer'
|
||||
import { useZoneStore } from '@/stores/zoneStore'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { onBeforeUnmount, ref, watch } from 'vue'
|
||||
import type { WeatherState } from '@/types'
|
||||
|
||||
// Constants
|
||||
const SUNRISE_HOUR = 6
|
||||
const SUNSET_HOUR = 20
|
||||
const DAY_STRENGTH = 100
|
||||
const NIGHT_STRENGTH = 30
|
||||
|
||||
// Stores
|
||||
const gameStore = useGameStore()
|
||||
const zoneStore = useZoneStore()
|
||||
|
||||
// Scene ref
|
||||
const sceneRef = ref<Phaser.Scene | null>(null)
|
||||
|
||||
// Effect refs
|
||||
const lightEffect = ref<Phaser.GameObjects.Graphics | null>(null)
|
||||
const rainEmitter = ref<Phaser.GameObjects.Particles.ParticleEmitter | null>(null)
|
||||
const fogSprite = ref<Phaser.GameObjects.Sprite | null>(null)
|
||||
|
||||
// State refs
|
||||
const weatherState = ref<WeatherState>({
|
||||
isRainEnabled: false,
|
||||
rainPercentage: 0,
|
||||
isFogEnabled: false,
|
||||
fogDensity: 0
|
||||
})
|
||||
|
||||
// Scene lifecycle methods
|
||||
const preloadScene = async (scene: Phaser.Scene) => {
|
||||
scene.load.image('raindrop', 'assets/raindrop.png')
|
||||
scene.load.image('fog', 'assets/fog.png')
|
||||
}
|
||||
|
||||
const createScene = async (scene: Phaser.Scene) => {
|
||||
sceneRef.value = scene
|
||||
setupEffects(scene)
|
||||
setupSocketListeners()
|
||||
}
|
||||
|
||||
const updateScene = () => {
|
||||
updateEffects()
|
||||
}
|
||||
|
||||
// Effect setup
|
||||
const setupEffects = (scene: Phaser.Scene) => {
|
||||
createLightEffect(scene)
|
||||
createRainEffect(scene)
|
||||
createFogEffect(scene)
|
||||
}
|
||||
|
||||
const createLightEffect = (scene: Phaser.Scene) => {
|
||||
lightEffect.value = scene.add.graphics()
|
||||
lightEffect.value.setDepth(1000)
|
||||
}
|
||||
|
||||
const createRainEffect = (scene: Phaser.Scene) => {
|
||||
rainEmitter.value = scene.add.particles(0, 0, 'raindrop', {
|
||||
x: { min: 0, max: window.innerWidth },
|
||||
y: -50,
|
||||
quantity: 5,
|
||||
lifespan: 2000,
|
||||
speedY: { min: 300, max: 500 },
|
||||
scale: { start: 0.005, end: 0.005 },
|
||||
alpha: { start: 0.5, end: 0 },
|
||||
blendMode: 'ADD'
|
||||
})
|
||||
rainEmitter.value.setDepth(900)
|
||||
rainEmitter.value.stop()
|
||||
}
|
||||
|
||||
const createFogEffect = (scene: Phaser.Scene) => {
|
||||
fogSprite.value = scene.add.sprite(window.innerWidth / 2, window.innerHeight / 2, 'fog')
|
||||
fogSprite.value.setScale(2)
|
||||
fogSprite.value.setAlpha(0)
|
||||
fogSprite.value.setDepth(950)
|
||||
}
|
||||
|
||||
// Lighting calculations
|
||||
const calculateLightStrength = (time: Date): number => {
|
||||
const hour = time.getHours()
|
||||
const minute = time.getMinutes()
|
||||
|
||||
let strength = DAY_STRENGTH
|
||||
|
||||
// Night time (10 PM - 6 AM)
|
||||
if (hour >= SUNSET_HOUR || hour < SUNRISE_HOUR) {
|
||||
strength = NIGHT_STRENGTH
|
||||
}
|
||||
// Full daylight (7 AM - 7 PM)
|
||||
else if (hour > SUNRISE_HOUR && hour < SUNSET_HOUR - 2) {
|
||||
strength = DAY_STRENGTH
|
||||
}
|
||||
// Sunrise transition (6 AM - 7 AM)
|
||||
else if (hour === SUNRISE_HOUR) {
|
||||
strength = NIGHT_STRENGTH + ((DAY_STRENGTH - NIGHT_STRENGTH) * minute) / 60
|
||||
}
|
||||
// Sunset transition (8 PM - 10 PM)
|
||||
else if (hour >= SUNSET_HOUR - 2 && hour < SUNSET_HOUR) {
|
||||
const totalMinutes = (hour - (SUNSET_HOUR - 2)) * 60 + minute
|
||||
const transitionProgress = totalMinutes / 120 // 2 hours = 120 minutes
|
||||
strength = DAY_STRENGTH - (DAY_STRENGTH - NIGHT_STRENGTH) * transitionProgress
|
||||
}
|
||||
|
||||
return strength
|
||||
}
|
||||
|
||||
// Effect updates
|
||||
const updateEffects = () => {
|
||||
const effects = zoneStore.zone?.zoneEffects || []
|
||||
|
||||
if (effects.length > 0) {
|
||||
updateZoneEffects(effects)
|
||||
} else {
|
||||
// Make sure we're getting the current time
|
||||
const lightStrength = calculateLightStrength(gameStore.world.date)
|
||||
updateLightEffect(lightStrength)
|
||||
updateWeatherEffects()
|
||||
}
|
||||
}
|
||||
|
||||
const updateZoneEffects = (effects: any[]) => {
|
||||
// Always update light based on time when zone effects are present
|
||||
updateLightEffect(calculateLightStrength(gameStore.world.date))
|
||||
|
||||
effects.forEach((effect) => {
|
||||
switch (effect.effect) {
|
||||
case 'rain':
|
||||
updateRainEffect(effect.strength)
|
||||
break
|
||||
case 'fog':
|
||||
updateFogEffect(effect.strength)
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const updateWeatherEffects = () => {
|
||||
updateRainEffect(weatherState.value.isRainEnabled ? weatherState.value.rainPercentage : 0)
|
||||
updateFogEffect(weatherState.value.isFogEnabled ? weatherState.value.fogDensity * 100 : 0)
|
||||
}
|
||||
|
||||
const updateLightEffect = (strength: number) => {
|
||||
if (!lightEffect.value) return
|
||||
const darkness = 1 - strength / 100
|
||||
lightEffect.value.clear()
|
||||
lightEffect.value.fillStyle(0x000000, darkness)
|
||||
lightEffect.value.fillRect(0, 0, window.innerWidth, window.innerHeight)
|
||||
}
|
||||
|
||||
const updateRainEffect = (strength: number) => {
|
||||
if (!rainEmitter.value) return
|
||||
if (strength > 0) {
|
||||
rainEmitter.value.start()
|
||||
rainEmitter.value.setQuantity(Math.floor((strength / 100) * 10))
|
||||
} else {
|
||||
rainEmitter.value.stop()
|
||||
}
|
||||
}
|
||||
|
||||
const updateFogEffect = (strength: number) => {
|
||||
if (!fogSprite.value) return
|
||||
fogSprite.value.setAlpha(strength / 100)
|
||||
}
|
||||
|
||||
// Socket handlers
|
||||
const setupSocketListeners = () => {
|
||||
// Initial weather state
|
||||
gameStore.connection?.emit('weather', (response: WeatherState) => {
|
||||
if (zoneStore.zone?.zoneEffects) return
|
||||
weatherState.value = response
|
||||
updateEffects()
|
||||
})
|
||||
|
||||
// Weather updates
|
||||
gameStore.connection?.on('weather', (data: WeatherState) => {
|
||||
weatherState.value = data
|
||||
updateEffects()
|
||||
})
|
||||
|
||||
// Time updates
|
||||
gameStore.connection?.on('date', () => {
|
||||
if (zoneStore.zone?.zoneEffects) return
|
||||
updateEffects()
|
||||
})
|
||||
}
|
||||
|
||||
// Watchers
|
||||
watch(() => zoneStore.zone?.zoneEffects, updateEffects, { deep: true })
|
||||
|
||||
// Cleanup
|
||||
onBeforeUnmount(() => {
|
||||
if (sceneRef.value) sceneRef.value.scene.remove('effects')
|
||||
gameStore.connection?.off('weather')
|
||||
})
|
||||
</script>
|
@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="flex flex-wrap items-center input-field gap-1">
|
||||
<div v-for="(chip, i) in internalValue" :key="i" class="flex gap-2.5 items-center bg-cyan rounded py-1 px-2">
|
||||
<div class="flex flex-wrap items-center input-field gap-1" @click="focusInput">
|
||||
<div v-for="(chip, i) in internalValue" :key="i" class="flex gap-2.5 items-center bg-cyan rounded py-1 px-2" role="listitem">
|
||||
<span class="text-xs text-white">{{ chip }}</span>
|
||||
<button type="button" class="text-xs cursor-pointer text-white font-light font-default not-italic hover:text-gray-50" @click="deleteChip(i)" aria-label="Remove chip">×</button>
|
||||
<button type="button" class="text-xs cursor-pointer text-white font-light font-default not-italic hover:text-gray-50" @click.stop="deleteChip(i)" aria-label="Remove tag">×</button>
|
||||
</div>
|
||||
<input class="outline-none border-none p-1 text-gray-300" placeholder="Tag name" v-model="currentInput" @keypress.enter.prevent="addChip" @keydown.backspace="handleBackspace" />
|
||||
<input ref="inputRef" class="outline-none border-none p-1 text-gray-300 min-w-[60px] flex-grow" :placeholder="placeholder" v-model.trim="currentInput" @keydown="handleKeydown" @paste="handlePaste" :maxlength="maxChipLength" aria-label="Add new tag" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -14,20 +14,29 @@ import type { Ref } from 'vue'
|
||||
|
||||
interface Props {
|
||||
modelValue?: string[]
|
||||
maxChips?: number
|
||||
maxChipLength?: number
|
||||
placeholder?: string
|
||||
allowDuplicates?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: () => []
|
||||
modelValue: () => [],
|
||||
maxChips: 10,
|
||||
maxChipLength: 20,
|
||||
placeholder: 'Add tag',
|
||||
allowDuplicates: false
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string[]): void
|
||||
(e: 'error', message: string): void
|
||||
}>()
|
||||
|
||||
const currentInput: Ref<string> = ref('')
|
||||
const internalValue = ref<string[]>([])
|
||||
const inputRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
// Initialize internalValue with props.modelValue
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
@ -36,9 +45,27 @@ watch(
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const validateChip = (chip: string): boolean => {
|
||||
if (!chip) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!props.allowDuplicates && internalValue.value.includes(chip)) {
|
||||
emit('error', 'Duplicate tags are not allowed')
|
||||
return false
|
||||
}
|
||||
|
||||
if (internalValue.value.length >= props.maxChips) {
|
||||
emit('error', `Maximum ${props.maxChips} tags allowed`)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const addChip = () => {
|
||||
const trimmedInput = currentInput.value.trim()
|
||||
if (trimmedInput && !internalValue.value.includes(trimmedInput)) {
|
||||
if (validateChip(trimmedInput)) {
|
||||
internalValue.value.push(trimmedInput)
|
||||
emit('update:modelValue', internalValue.value)
|
||||
currentInput.value = ''
|
||||
@ -50,10 +77,36 @@ const deleteChip = (index: number) => {
|
||||
emit('update:modelValue', internalValue.value)
|
||||
}
|
||||
|
||||
const handleBackspace = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Backspace' && currentInput.value === '' && internalValue.value.length > 0) {
|
||||
internalValue.value.pop()
|
||||
emit('update:modelValue', internalValue.value)
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
switch (event.key) {
|
||||
case 'Enter':
|
||||
event.preventDefault()
|
||||
addChip()
|
||||
break
|
||||
case 'Backspace':
|
||||
if (currentInput.value === '' && internalValue.value.length > 0) {
|
||||
deleteChip(internalValue.value.length - 1)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const handlePaste = (event: ClipboardEvent) => {
|
||||
event.preventDefault()
|
||||
const pastedText = event.clipboardData?.getData('text')
|
||||
if (pastedText) {
|
||||
const chips = pastedText
|
||||
.split(/[,\n]/)
|
||||
.map((chip) => chip.trim())
|
||||
.filter(Boolean)
|
||||
chips.forEach((chip) => {
|
||||
currentInput.value = chip
|
||||
addChip()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const focusInput = () => {
|
||||
inputRef.value?.focus()
|
||||
}
|
||||
</script>
|
||||
|
102
src/components/game/character/Character.vue
Normal file
@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<Container ref="characterContainer" :x="currentPositionX" :y="currentPositionY" :depth="isometricDepth">
|
||||
<ChatBubble :mapCharacter="props.mapCharacter" />
|
||||
<HealthBar :mapCharacter="props.mapCharacter" />
|
||||
<CharacterHair :mapCharacter="props.mapCharacter" />
|
||||
<Sprite ref="characterSprite" :origin-y="1" :flipX="isFlippedX" />
|
||||
</Container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type MapCharacter } from '@/application/types'
|
||||
import CharacterHair from '@/components/game/character/partials/CharacterHair.vue'
|
||||
import ChatBubble from '@/components/game/character/partials/ChatBubble.vue'
|
||||
import HealthBar from '@/components/game/character/partials/HealthBar.vue'
|
||||
import { useCharacterSpriteComposable } from '@/composables/useCharacterSpriteComposable'
|
||||
import { useSoundComposable } from '@/composables/useSoundComposable'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useMapStore } from '@/stores/mapStore'
|
||||
import { Container, Sprite, useScene } from 'phavuer'
|
||||
import { onMounted, onUnmounted, watch } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
tileMap: Phaser.Tilemaps.Tilemap
|
||||
mapCharacter: MapCharacter
|
||||
}>()
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const mapStore = useMapStore()
|
||||
const scene = useScene()
|
||||
|
||||
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) => {
|
||||
if (!newValues) return
|
||||
|
||||
if (!oldValues || newValues.positionX !== oldValues.positionX || newValues.positionY !== oldValues.positionY) {
|
||||
updatePosition(newValues.positionX, newValues.positionY)
|
||||
}
|
||||
|
||||
if (newValues.isMoving !== oldValues?.isMoving || newValues.rotation !== oldValues?.rotation) {
|
||||
updateSprite()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays walk sound when character is moving
|
||||
*/
|
||||
watch(
|
||||
() => props.mapCharacter.isMoving,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
playSound('/assets/sounds/walk.wav', false, true)
|
||||
} else {
|
||||
stopSound('/assets/sounds/walk.wav')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Plays attack animation and sound when character is attacking
|
||||
*/
|
||||
watch(
|
||||
() => props.mapCharacter.isAttacking,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
playAnimation('attack')
|
||||
playSound('/assets/sounds/attack.wav', false, true)
|
||||
} else {
|
||||
stopSound('/assets/sounds/attack.wav')
|
||||
}
|
||||
mapStore.updateCharacterProperty(props.mapCharacter.character.id, 'isAttacking', false)
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Handles position updates and movement delay
|
||||
*/
|
||||
watch(
|
||||
() => ({
|
||||
positionX: props.mapCharacter.character.positionX,
|
||||
positionY: props.mapCharacter.character.positionY,
|
||||
isMoving: props.mapCharacter.isMoving,
|
||||
rotation: props.mapCharacter.character.rotation,
|
||||
isAttacking: props.mapCharacter.isAttacking
|
||||
}),
|
||||
(oldValues, newValues) => {
|
||||
handlePositionUpdate(oldValues, newValues)
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
await initializeSprite()
|
||||
if (props.mapCharacter.character.id === gameStore.character!.id) {
|
||||
scene.cameras.main.startFollow(characterContainer.value as Phaser.GameObjects.Container)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanup()
|
||||
})
|
||||
</script>
|
54
src/components/game/character/partials/CharacterHair.vue
Normal file
@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<Image v-bind="imageProps" v-if="gameStore.isTextureLoaded(texture)" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { MapCharacter, Sprite as SpriteT } from '@/application/types'
|
||||
import { loadSpriteTextures } from '@/services/textureService'
|
||||
import { CharacterHairStorage, CharacterTypeStorage, SpriteStorage } from '@/storage/storages'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { Image, useScene } from 'phavuer'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
mapCharacter: MapCharacter
|
||||
}>()
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const scene = useScene()
|
||||
const hairSpriteId = ref('')
|
||||
const sprite = ref<SpriteT | null>(null)
|
||||
|
||||
const texture = computed(() => {
|
||||
const { rotation } = props.mapCharacter.character
|
||||
const direction = [0, 6].includes(rotation) ? 'back' : 'front'
|
||||
|
||||
return `${hairSpriteId.value}-${direction}`
|
||||
})
|
||||
|
||||
const isFlippedX = computed(() => [6, 4].includes(props.mapCharacter.character.rotation ?? 0))
|
||||
|
||||
const imageProps = computed(() => {
|
||||
const direction = [0, 6].includes(props.mapCharacter.character.rotation ?? 0) ? 'back' : 'front'
|
||||
const spriteAction = sprite.value?.spriteActions?.find((spriteAction) => spriteAction.action === direction)
|
||||
|
||||
return {
|
||||
depth: 9999,
|
||||
originX: Number(spriteAction?.originX) ?? 0,
|
||||
originY: Number(spriteAction?.originY) ?? 0,
|
||||
flipX: isFlippedX.value,
|
||||
texture: texture.value
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
const characterHairStorage = new CharacterHairStorage()
|
||||
const spriteId = await characterHairStorage.getSpriteId(props.mapCharacter.character.characterHair!)
|
||||
if (!spriteId) return
|
||||
|
||||
hairSpriteId.value = spriteId
|
||||
const spriteStorage = new SpriteStorage()
|
||||
sprite.value = await spriteStorage.getById(spriteId)
|
||||
await loadSpriteTextures(scene, spriteId)
|
||||
})
|
||||
</script>
|
45
src/components/game/character/partials/ChatBubble.vue
Normal file
@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<Container ref="characterChatContainer">
|
||||
<RoundRectangle @create="createChatBubble" :origin-x="0.5" :origin-y="7.5" :fillColor="0xffffff" :width="194" :height="21" :radius="20" />
|
||||
<Text @create="createChatText" :style="{ fontSize: 13, fontFamily: 'Arial', color: '#000' }" />
|
||||
</Container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { MapCharacter } from '@/application/types'
|
||||
import { Container, refObj, RoundRectangle, Text, useGame } from 'phavuer'
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
mapCharacter: MapCharacter
|
||||
}>()
|
||||
|
||||
const game = useGame()
|
||||
const characterChatContainer = refObj<Phaser.GameObjects.Container>()
|
||||
|
||||
const createChatBubble = (container: Phaser.GameObjects.Container) => {
|
||||
container.setName(`${props.mapCharacter.character.name}_chatBubble`)
|
||||
}
|
||||
|
||||
const createChatText = (text: Phaser.GameObjects.Text) => {
|
||||
text.setName(`${props.mapCharacter.character.name}_chatText`)
|
||||
text.setFontSize(13)
|
||||
text.setFontFamily('Arial')
|
||||
text.setOrigin(0.5, 10.9)
|
||||
text.setResolution(2)
|
||||
|
||||
// Fix text alignment on Windows and Android
|
||||
if (game.device.os.windows || game.device.os.android) {
|
||||
text.setOrigin(0.5, 9.75)
|
||||
|
||||
if (game.device.browser.firefox) {
|
||||
text.setOrigin(0.5, 10.9)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
characterChatContainer.value!.setName(`${props.mapCharacter.character!.name}_chatContainer`)
|
||||
characterChatContainer.value!.setVisible(false)
|
||||
})
|
||||
</script>
|
34
src/components/game/character/partials/HealthBar.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<Container :depth="999">
|
||||
<Text @create="createNicknameText" :text="props.mapCharacter.character.name" />
|
||||
<RoundRectangle :origin-x="0.5" :origin-y="18.5" :fillColor="0xffffff" :width="74" :height="6" :radius="5" />
|
||||
<RoundRectangle :origin-x="0.5" :origin-y="36.4" :fillColor="0x00b3b3" :width="70" :height="3" :radius="5" />
|
||||
</Container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { MapCharacter } from '@/application/types'
|
||||
import { Container, RoundRectangle, Text, useGame } from 'phavuer'
|
||||
|
||||
const props = defineProps<{
|
||||
mapCharacter: MapCharacter
|
||||
}>()
|
||||
|
||||
const game = useGame()
|
||||
|
||||
const createNicknameText = (text: Phaser.GameObjects.Text) => {
|
||||
text.setFontSize(13)
|
||||
text.setFontFamily('Arial')
|
||||
text.setOrigin(0.5, 9)
|
||||
text.setResolution(2)
|
||||
|
||||
// Fix text alignment on Windows and Android
|
||||
if (game.device.os.windows || game.device.os.android) {
|
||||
text.setOrigin(0.5, 8)
|
||||
|
||||
if (game.device.browser.firefox) {
|
||||
text.setOrigin(0.5, 9)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
173
src/components/game/gui/CharacterProfile.vue
Normal file
@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<div class="absolute" v-if="gameStore.uiSettings.isCharacterProfileOpen" :style="modalStyle">
|
||||
<div class="relative">
|
||||
<img src="/assets/ui-elements/profile-ui-box-outer.svg" class="absolute w-full h-full" alt="" />
|
||||
<img src="/assets/ui-elements/profile-ui-box-inner.svg" class="absolute left-2 bottom-2 w-[calc(100%_-_16px)] h-[calc(100%_-_40px)]" alt="" />
|
||||
<div @mousedown="startDrag" class="cursor-move px-5 py-2.5 flex justify-between items-center relative">
|
||||
<span class="text-xs text-white font-thin">Character Profile [Alt+C]</span>
|
||||
<button @click="gameStore.uiSettings.isCharacterProfileOpen = false" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
|
||||
<img draggable="false" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" alt="Close button icon" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="py-4 px-6 flex flex-col gap-7 relative z-10">
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<p class="text-sm m-0 font-bold text-white tracking-wide">{{ gameStore.character?.name }}</p>
|
||||
<span class="text-xs">{{ gameStore.character?.experience }} / 18.600XP</span>
|
||||
</div>
|
||||
<a class="hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-box-textured-small.svg')] bg-no-repeat block w-8 h-8 relative mx-[3px]">
|
||||
<img class="hover:drop-shadow-default w-3.5 h-3.5 m-[9px] object-contain" draggable="false" src="/assets/icons/plus-green-icon.svg" alt="Plus button icon" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
|
||||
<span class="absolute w-full top-1/2 -translate-y-1/2 text-[6px] text-center">CROWN</span>
|
||||
</div>
|
||||
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
|
||||
<span class="absolute w-full top-1/2 -translate-y-1/2 text-[6px] text-center">R-HAND</span>
|
||||
</div>
|
||||
<div class="flex gap-0.5 items-end">
|
||||
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
|
||||
<span class="absolute w-full top-1/2 -translate-y-1/2 text-[6px] text-center">L-HAND</span>
|
||||
</div>
|
||||
<div class="w-6 h-6 default-border rounded-sm bg-gray relative hover:bg-gray-600">
|
||||
<span class="absolute w-full top-1/2 -translate-y-1/2 text-[6px] text-center">RING</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<img src="/assets/placeholders/inventory_player.png" class="w-8 h-auto" alt="Player character sprite" />
|
||||
<div class="flex flex-col items-end gap-0.5">
|
||||
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
|
||||
<img class="w-6 h-6 center-element" src="/assets/icons/profile/helmet.svg" alt="Helmet icon" />
|
||||
</div>
|
||||
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
|
||||
<img class="w-6 h-6 center-element" src="/assets/icons/profile/chestplate.svg" alt="Chestplate icon" />
|
||||
</div>
|
||||
<div class="flex gap-0.5 items-end">
|
||||
<div class="w-6 h-6 default-border rounded-sm bg-gray relative hover:bg-gray-600">
|
||||
<img class="w-4 h-4 center-element" src="/assets/icons/profile/boots.svg" alt="Boots icon" />
|
||||
</div>
|
||||
<div class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600">
|
||||
<img class="w-6 h-6 center-element" src="/assets/icons/profile/legs.svg" alt="Legs icon" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<div class="flex flex-col">
|
||||
<div class="w-[105px] h-px mb-[3px] flex justify-between">
|
||||
<div class="w-px h-full bg-gray-300"></div>
|
||||
<div class="w-[101px] h-full bg-gray-300"></div>
|
||||
<div class="w-px h-full bg-gray-300"></div>
|
||||
</div>
|
||||
<div class="flex gap-11">
|
||||
<p class="m-0 text-xs text-white tracking-wide">Health</p>
|
||||
<span class="m-0 text-xs text-white tracking-wide">+ 15</span>
|
||||
</div>
|
||||
<div class="w-[105px] h-px my-[3px] flex justify-between">
|
||||
<div class="w-px h-full bg-gray-300"></div>
|
||||
<div class="w-[101px] h-full bg-gray-300"></div>
|
||||
<div class="w-px h-full bg-gray-300"></div>
|
||||
</div>
|
||||
<div class="flex gap-11">
|
||||
<p class="m-0 text-xs text-white tracking-wide">Health</p>
|
||||
<span class="m-0 text-xs text-white tracking-wide">+ 15</span>
|
||||
</div>
|
||||
<div class="w-[105px] h-px mt-[3px] flex justify-between">
|
||||
<div class="w-px h-full bg-gray-300"></div>
|
||||
<div class="w-[101px] h-full bg-gray-300"></div>
|
||||
<div class="w-px h-full bg-gray-300"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<div class="w-[105px] h-px mb-[3px] flex justify-between">
|
||||
<div class="w-px h-full bg-gray-300"></div>
|
||||
<div class="w-[101px] h-full bg-gray-300"></div>
|
||||
<div class="w-px h-full bg-gray-300"></div>
|
||||
</div>
|
||||
<div class="flex gap-11">
|
||||
<p class="m-0 text-xs text-white tracking-wide">Health</p>
|
||||
<span class="m-0 text-xs text-white tracking-wide">+ 15</span>
|
||||
</div>
|
||||
<div class="w-[105px] h-px my-[3px] flex justify-between">
|
||||
<div class="w-px h-full bg-gray-300"></div>
|
||||
<div class="w-[101px] h-full bg-gray-300"></div>
|
||||
<div class="w-px h-full bg-gray-300"></div>
|
||||
</div>
|
||||
<div class="flex gap-11">
|
||||
<p class="m-0 text-xs text-white tracking-wide">Health</p>
|
||||
<span class="m-0 text-xs text-white tracking-wide">+ 15</span>
|
||||
</div>
|
||||
<div class="w-[105px] h-px mt-[3px] flex justify-between">
|
||||
<div class="w-px h-full bg-gray-300"></div>
|
||||
<div class="w-[101px] h-full bg-gray-300"></div>
|
||||
<div class="w-px h-full bg-gray-300"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-rows-4 grid-cols-6 gap-0.5">
|
||||
<div v-for="n in 24" class="w-9 h-9 default-border rounded-sm bg-gray relative hover:bg-gray-600"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
|
||||
const width = ref(286)
|
||||
const height = ref(483)
|
||||
const x = ref(window.innerWidth / 2 - 143)
|
||||
const y = ref(window.innerHeight / 2 - 241)
|
||||
const isDragging = ref(false)
|
||||
|
||||
const modalStyle = computed(() => ({
|
||||
top: `${y.value}px`,
|
||||
left: `${x.value}px`,
|
||||
width: `${width.value}px`,
|
||||
height: `${height.value}px`
|
||||
}))
|
||||
|
||||
function startDrag(event: MouseEvent) {
|
||||
isDragging.value = true
|
||||
const startX = event.clientX - x.value
|
||||
const startY = event.clientY - y.value
|
||||
|
||||
function drag(event: MouseEvent) {
|
||||
if (!isDragging.value) return
|
||||
x.value = event.clientX - startX
|
||||
y.value = event.clientY - startY
|
||||
}
|
||||
|
||||
function stopDrag() {
|
||||
isDragging.value = false
|
||||
removeEventListener('mousemove', drag)
|
||||
removeEventListener('mouseup', stopDrag)
|
||||
}
|
||||
|
||||
addEventListener('mousemove', drag)
|
||||
addEventListener('mouseup', stopDrag)
|
||||
}
|
||||
|
||||
function keyPress(event: KeyboardEvent) {
|
||||
if (event.altKey && event.key === 'c') {
|
||||
gameStore.toggleCharacterProfile()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
addEventListener('keydown', keyPress)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
removeEventListener('keydown', keyPress)
|
||||
})
|
||||
</script>
|
@ -2,41 +2,60 @@
|
||||
<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 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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-96 mx-auto relative">
|
||||
<img src="/assets/icons/chat-icon.svg" class="absolute top-1/2 -translate-y-1/2 left-2.5 h-4 w-4 opacity-50" />
|
||||
<img src="/assets/icons/ingameUI/chat-icon.svg" class="absolute top-1/2 -translate-y-1/2 left-2.5 h-4 w-4 opacity-50" alt="" />
|
||||
<input
|
||||
class="w-[332px] h-8 rounded-sm text-xs font-default pl-8 pr-4 py-0 bg-gray-600 border-2 border-solid border-gray-500 text-gray-300 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover focus:outline-none focus:ring-0 focus:border-cyan-800"
|
||||
placeholder="Type something..."
|
||||
v-model="message"
|
||||
@keypress="handleKeyPress"
|
||||
@submit="handleSubmit"
|
||||
ref="chatInput"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref, nextTick } from 'vue'
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import type { Character, ChatMessage } from '@/types'
|
||||
import { useZoneStore } from '@/stores/zoneStore'
|
||||
import { useMapStore } from '@/stores/mapStore'
|
||||
import { onClickOutside, useFocus } from '@vueuse/core'
|
||||
import { useScene } from 'phavuer'
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
const scene = useScene()
|
||||
const gameStore = useGameStore()
|
||||
const zoneStore = useZoneStore()
|
||||
|
||||
const message = ref('')
|
||||
const chats = ref([] as ChatMessage[])
|
||||
const chats = ref<{ character: string; message: string }[]>([])
|
||||
const chatWindow = ref<HTMLElement | null>(null)
|
||||
const chatInput = ref<HTMLElement | null>(null)
|
||||
|
||||
const { focused } = useFocus(chatInput)
|
||||
|
||||
function focusChat(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter' && !focused.value) {
|
||||
focused.value = true
|
||||
}
|
||||
}
|
||||
|
||||
onClickOutside(chatInput, (event) => unfocusChat(event, chatInput.value as HTMLElement))
|
||||
|
||||
function unfocusChat(event: Event, targetElement: HTMLElement) {
|
||||
if (!(event.target instanceof Node) || !targetElement.contains(event.target)) {
|
||||
targetElement.blur()
|
||||
}
|
||||
}
|
||||
|
||||
const sendMessage = () => {
|
||||
if (!message.value.trim()) return
|
||||
gameStore.connection?.emit('chat:send_message', { message: message.value }, (response: boolean) => {})
|
||||
socketManager.emit(SocketEvent.CHAT_MESSAGE, { message: message.value }, (response: boolean) => {})
|
||||
message.value = ''
|
||||
}
|
||||
|
||||
@ -60,18 +79,30 @@ const scrollToBottom = () => {
|
||||
})
|
||||
}
|
||||
|
||||
gameStore.connection?.on('chat:message', (data: ChatMessage) => {
|
||||
chats.value.push(data)
|
||||
socketManager.on(SocketEvent.CHAT_MESSAGE, (data: { character: string; message: string }) => {
|
||||
if (!data.character || !data.message) return
|
||||
|
||||
chats.value.push({ character: data.character, message: data.message })
|
||||
scrollToBottom()
|
||||
|
||||
if (!zoneStore.characterLoaded) return
|
||||
const characterContainer = scene.children.getByName(data.character) as Phaser.GameObjects.Container
|
||||
if (!characterContainer) {
|
||||
console.log('No character container found')
|
||||
return
|
||||
}
|
||||
|
||||
const charChatContainer = scene.children.getByName(data.character.name + '_chatContainer') as Phaser.GameObjects.Container
|
||||
if (!charChatContainer) return
|
||||
const characterChatContainer = characterContainer.getByName(data.character + '_chatContainer') as Phaser.GameObjects.Container
|
||||
if (!characterChatContainer) {
|
||||
console.log('No character chat container found')
|
||||
return
|
||||
}
|
||||
|
||||
const chatBubble = charChatContainer.getByName(data.character.name + '_chatBubble') as Phaser.GameObjects.Container
|
||||
const chatText = charChatContainer.getByName(data.character.name + '_chatText') as Phaser.GameObjects.Text
|
||||
if (!chatText || !chatBubble) return
|
||||
const chatBubble = characterChatContainer.getByName(data.character + '_chatBubble') as Phaser.GameObjects.Container
|
||||
const chatText = characterChatContainer.getByName(data.character + '_chatText') as Phaser.GameObjects.Text
|
||||
if (!chatText || !chatBubble) {
|
||||
console.log('No chat text or bubble found')
|
||||
return
|
||||
}
|
||||
|
||||
function calculateTextWidth(text: string, font: string, fontSize: number): number {
|
||||
// Create a canvas element
|
||||
@ -96,28 +127,33 @@ gameStore.connection?.on('chat:message', (data: ChatMessage) => {
|
||||
// setText but with max. char limit of 90
|
||||
chatText.setText(data.message.substring(0, 90))
|
||||
|
||||
charChatContainer.setVisible(true)
|
||||
characterChatContainer.setVisible(true)
|
||||
|
||||
/**
|
||||
* Hide chat bubble after a few seconds
|
||||
*/
|
||||
|
||||
// Clear any existing hide timer
|
||||
if (charChatContainer.getData('hideTimer')) {
|
||||
clearTimeout(charChatContainer.getData('hideTimer'))
|
||||
if (characterChatContainer.getData('hideTimer')) {
|
||||
clearTimeout(characterChatContainer.getData('hideTimer'))
|
||||
}
|
||||
|
||||
// Set a new hide timer
|
||||
const hideTimer = setTimeout(() => {
|
||||
charChatContainer.setVisible(false)
|
||||
characterChatContainer.setVisible(false)
|
||||
}, 3000)
|
||||
|
||||
// Store the timer on the container itself
|
||||
charChatContainer.setData('hideTimer', hideTimer)
|
||||
characterChatContainer.setData('hideTimer', hideTimer)
|
||||
})
|
||||
scrollToBottom()
|
||||
|
||||
onMounted(() => {
|
||||
addEventListener('keydown', focusChat)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
gameStore.connection?.off('chat:message')
|
||||
socketManager.off(SocketEvent.CHAT_MESSAGE)
|
||||
removeEventListener('keydown', focusChat)
|
||||
})
|
||||
</script>
|
21
src/components/game/gui/Clock.vue
Normal file
@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div class="absolute top-0 right-4 hidden lg:block" v-if="gameStore.world.date && typeof gameStore.world.date === 'object'">
|
||||
<p class="text-white text-lg">
|
||||
{{ useDateFormat(gameStore.world.date, 'YYYY/MM/DD HH:mm') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SocketEvent } from '@/application/enums'
|
||||
import { socketManager } from '@/managers/SocketManager'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useDateFormat } from '@vueuse/core'
|
||||
import { onUnmounted } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
|
||||
onUnmounted(() => {
|
||||
socketManager.off(SocketEvent.DATE)
|
||||
})
|
||||
</script>
|
@ -2,44 +2,44 @@
|
||||
<div class="absolute top-4 left-[300px] w-[422px]">
|
||||
<div class="flex gap-2.5">
|
||||
<div class="relative">
|
||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-frame-empty.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||
<span class="z-10 text-pixel absolute top-1 left-2">F1</span>
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f1-icon.png')] bg-no-repeat"></div>
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/ingameUI/f1-icon.png')] bg-no-repeat"></div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-frame-empty.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||
<span class="z-10 text-pixel absolute top-1 left-2">F2</span>
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f2-icon.png')] bg-no-repeat"></div>
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/ingameUI/f2-icon.png')] bg-no-repeat"></div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-frame-empty.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||
<span class="z-10 text-pixel absolute top-1 left-2">F3</span>
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f3-icon.png')] bg-no-repeat"></div>
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/ingameUI/f3-icon.png')] bg-no-repeat"></div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-frame-empty.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||
<span class="z-10 text-pixel absolute top-1 left-2">F4</span>
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f4-icon.png')] bg-no-repeat"></div>
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/ingameUI/f4-icon.png')] bg-no-repeat"></div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-frame-empty.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||
<span class="z-10 text-pixel absolute top-1 left-2">F5</span>
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f5-icon.png')] bg-no-repeat"></div>
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/ingameUI/f5-icon.png')] bg-no-repeat"></div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-frame-empty.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||
<span class="z-10 text-pixel absolute top-1 left-2">F6</span>
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f6-icon.png')] bg-no-repeat"></div>
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/ingameUI/f6-icon.png')] bg-no-repeat"></div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-frame-empty.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||
<span class="z-10 text-pixel absolute top-1 left-2">F7</span>
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f7-icon.png')] bg-no-repeat"></div>
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/ingameUI/f7-icon.png')] bg-no-repeat"></div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/ui-border-4-corners-light.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||
<button class="z-20 group-hover:cursor-pointer bg-[url('/assets/ui-elements/button-ui-frame-empty.svg')] bg-no-repeat block w-[42px] h-[42px] relative p-0"></button>
|
||||
<span class="z-10 text-pixel absolute top-1 left-2">F8</span>
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/f8-icon.png')] bg-no-repeat"></div>
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-[url('/assets/icons/ingameUI/f8-icon.png')] bg-no-repeat"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|