1
0
forked from noxious/server

Merge remote-tracking branch 'origin/main' into feature/#362-final-depth-sort-fix

# Conflicts:
#	src/entities/base/mapObject.ts
#	src/events/gameMaster/assetManager/mapObject/update.ts
This commit is contained in:
Dennis Postma 2025-03-14 00:18:14 +01:00
commit f1395340b8
38 changed files with 1148 additions and 707 deletions

View File

@ -6,10 +6,10 @@ JWT_SECRET="secret"
CLIENT_URL="http://localhost:5173"
# Database configuration
REDIS_URL="redis://@redis:6379/4"
DB_HOST="mariadb"
DB_USER="mariadb"
DB_PASS="mariadb"
REDIS_URL="redis://@127.0.0.1:6379/4"
DB_HOST="localhost"
DB_USER="root"
DB_PASS=""
DB_PORT="3306"
DB_NAME="game"

450
package-lock.json generated
View File

@ -59,14 +59,14 @@
}
},
"node_modules/@babel/generator": {
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz",
"integrity": "sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==",
"version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz",
"integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.26.9",
"@babel/types": "^7.26.9",
"@babel/parser": "^7.26.10",
"@babel/types": "^7.26.10",
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25",
"jsesc": "^3.0.2"
@ -96,13 +96,13 @@
}
},
"node_modules/@babel/parser": {
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz",
"integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==",
"version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz",
"integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.26.9"
"@babel/types": "^7.26.10"
},
"bin": {
"parser": "bin/babel-parser.js"
@ -127,17 +127,17 @@
}
},
"node_modules/@babel/traverse": {
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.9.tgz",
"integrity": "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==",
"version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.10.tgz",
"integrity": "sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.26.2",
"@babel/generator": "^7.26.9",
"@babel/parser": "^7.26.9",
"@babel/generator": "^7.26.10",
"@babel/parser": "^7.26.10",
"@babel/template": "^7.26.9",
"@babel/types": "^7.26.9",
"@babel/types": "^7.26.10",
"debug": "^4.3.1",
"globals": "^11.1.0"
},
@ -146,9 +146,9 @@
}
},
"node_modules/@babel/types": {
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz",
"integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==",
"version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz",
"integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -194,9 +194,9 @@
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz",
"integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz",
"integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==",
"cpu": [
"ppc64"
],
@ -211,9 +211,9 @@
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz",
"integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz",
"integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==",
"cpu": [
"arm"
],
@ -228,9 +228,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz",
"integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz",
"integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==",
"cpu": [
"arm64"
],
@ -245,9 +245,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz",
"integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz",
"integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==",
"cpu": [
"x64"
],
@ -262,9 +262,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz",
"integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz",
"integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==",
"cpu": [
"arm64"
],
@ -279,9 +279,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz",
"integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz",
"integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==",
"cpu": [
"x64"
],
@ -296,9 +296,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz",
"integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz",
"integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==",
"cpu": [
"arm64"
],
@ -313,9 +313,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz",
"integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz",
"integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==",
"cpu": [
"x64"
],
@ -330,9 +330,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz",
"integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz",
"integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==",
"cpu": [
"arm"
],
@ -347,9 +347,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz",
"integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz",
"integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==",
"cpu": [
"arm64"
],
@ -364,9 +364,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz",
"integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz",
"integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==",
"cpu": [
"ia32"
],
@ -381,9 +381,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz",
"integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz",
"integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==",
"cpu": [
"loong64"
],
@ -398,9 +398,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz",
"integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz",
"integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==",
"cpu": [
"mips64el"
],
@ -415,9 +415,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz",
"integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz",
"integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==",
"cpu": [
"ppc64"
],
@ -432,9 +432,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz",
"integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz",
"integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==",
"cpu": [
"riscv64"
],
@ -449,9 +449,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz",
"integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz",
"integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==",
"cpu": [
"s390x"
],
@ -466,9 +466,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz",
"integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz",
"integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==",
"cpu": [
"x64"
],
@ -482,10 +482,27 @@
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz",
"integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz",
"integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz",
"integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==",
"cpu": [
"x64"
],
@ -500,9 +517,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz",
"integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz",
"integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==",
"cpu": [
"arm64"
],
@ -517,9 +534,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz",
"integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz",
"integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==",
"cpu": [
"x64"
],
@ -534,9 +551,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz",
"integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz",
"integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==",
"cpu": [
"x64"
],
@ -551,9 +568,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz",
"integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz",
"integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==",
"cpu": [
"arm64"
],
@ -568,9 +585,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz",
"integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz",
"integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==",
"cpu": [
"ia32"
],
@ -585,9 +602,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz",
"integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz",
"integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==",
"cpu": [
"x64"
],
@ -1056,14 +1073,14 @@
}
},
"node_modules/@mikro-orm/cli": {
"version": "6.4.6",
"resolved": "https://registry.npmjs.org/@mikro-orm/cli/-/cli-6.4.6.tgz",
"integrity": "sha512-sTMoDSJrnHZBT+ZAG40OeZwR9zRTYHtaaub9OoMM2CrxfI1KeiNqL/XFB4LaM5SVRAbnoEFpMJwQ8KS+5NcN9w==",
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/@mikro-orm/cli/-/cli-6.4.9.tgz",
"integrity": "sha512-LQzVsmar/0DoJkPGyz3OpB8pa9BCQtvYreEC71h0O+RcizppJjgBQNTkj5tJd2Iqvh4hSaMv6qTv0l5UK6F2Vw==",
"license": "MIT",
"dependencies": {
"@jercle/yargonaut": "1.1.5",
"@mikro-orm/core": "6.4.6",
"@mikro-orm/knex": "6.4.6",
"@mikro-orm/core": "6.4.9",
"@mikro-orm/knex": "6.4.9",
"fs-extra": "11.3.0",
"tsconfig-paths": "4.2.0",
"yargs": "17.7.2"
@ -1077,9 +1094,9 @@
}
},
"node_modules/@mikro-orm/core": {
"version": "6.4.6",
"resolved": "https://registry.npmjs.org/@mikro-orm/core/-/core-6.4.6.tgz",
"integrity": "sha512-xVm/ALG/3vTMgh6SrvojJ6jjMa0s2hNzWN0triDB16BaNdLwWE4aAaAe+3CuoMFqJAArSOUISTEjExbzELB1ZA==",
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/@mikro-orm/core/-/core-6.4.9.tgz",
"integrity": "sha512-osB2TbvSH4ZL1s62LCBQFAnxPqLycX5fakPHOoztudixqfbVD5QQydeGizJXMMh2zKP6vRCwIJy3MeSuFxPjHg==",
"license": "MIT",
"dependencies": {
"dataloader": "2.2.3",
@ -1087,7 +1104,7 @@
"esprima": "4.0.1",
"fs-extra": "11.3.0",
"globby": "11.1.0",
"mikro-orm": "6.4.6",
"mikro-orm": "6.4.9",
"reflect-metadata": "0.2.2"
},
"engines": {
@ -1098,9 +1115,9 @@
}
},
"node_modules/@mikro-orm/knex": {
"version": "6.4.6",
"resolved": "https://registry.npmjs.org/@mikro-orm/knex/-/knex-6.4.6.tgz",
"integrity": "sha512-o6t67tFH/GuPZCCEtKbTTL8HDXNgB2ITjButCTZLwteL0qI9yE/f7K6K+dEUKW+hAL3KRvc2BQeumvCVWFeISg==",
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/@mikro-orm/knex/-/knex-6.4.9.tgz",
"integrity": "sha512-iGXJfe/TziVOQsWuxMIqkOpurysWzQA6kj3+FDtOkHJAijZhqhjSBnfUVHHY/JzU9o0M0rgLrDVJFry/uEaJEA==",
"license": "MIT",
"dependencies": {
"fs-extra": "11.3.0",
@ -1129,12 +1146,12 @@
}
},
"node_modules/@mikro-orm/mariadb": {
"version": "6.4.6",
"resolved": "https://registry.npmjs.org/@mikro-orm/mariadb/-/mariadb-6.4.6.tgz",
"integrity": "sha512-n6pOf69heOsbrggqYcf9SeF9hUdkw0FbzuUAcI72jWuyNRyzNR1UATblD+vRJnwt8JDWwakjINU/bduZbcEwPw==",
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/@mikro-orm/mariadb/-/mariadb-6.4.9.tgz",
"integrity": "sha512-KuCzDGkC9cmNA8WxE9pZca6/Ds2sso3JUxiGoVyekOj/t9qer81UQYWasI80TBJ82TxrUdLM9NFBBO++tz+NYA==",
"license": "MIT",
"dependencies": {
"@mikro-orm/knex": "6.4.6",
"@mikro-orm/knex": "6.4.9",
"mariadb": "3.4.0"
},
"engines": {
@ -1145,12 +1162,12 @@
}
},
"node_modules/@mikro-orm/migrations": {
"version": "6.4.6",
"resolved": "https://registry.npmjs.org/@mikro-orm/migrations/-/migrations-6.4.6.tgz",
"integrity": "sha512-i0/H07g1jQS0tKVSTSkHhrmuDEHxDD3/IzkiObezTgGlD5tqN7acaSr8RDJ3DgICb8MHUDVMLwxeGy8igDB4ag==",
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/@mikro-orm/migrations/-/migrations-6.4.9.tgz",
"integrity": "sha512-vwTXG8PU3bpzTZxxu1dWlhnUumM2Yob2IWajoh+Rj9+19VBZYc5N3tm8FRekt5oPzxeK5/9+sDfxT9FzXgjeNw==",
"license": "MIT",
"dependencies": {
"@mikro-orm/knex": "6.4.6",
"@mikro-orm/knex": "6.4.9",
"fs-extra": "11.3.0",
"umzug": "3.8.2"
},
@ -1162,13 +1179,13 @@
}
},
"node_modules/@mikro-orm/mysql": {
"version": "6.4.6",
"resolved": "https://registry.npmjs.org/@mikro-orm/mysql/-/mysql-6.4.6.tgz",
"integrity": "sha512-KVP9Wif9MX/RrroVgYQQUrXe9SALBQLfB9CbuJlUB7MnEcZtDi5JNX7z5kghToz0aBrTtOgsr93G1bCoM0SJkg==",
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/@mikro-orm/mysql/-/mysql-6.4.9.tgz",
"integrity": "sha512-rmHonMzvurB+50BNpKb9FORFVs3+V8S4Om1Tmv6MFvSdeD1Qqb/efvYp7cgv+NncHSrgMtKLMy3FDm7guU6yYQ==",
"license": "MIT",
"dependencies": {
"@mikro-orm/knex": "6.4.6",
"mysql2": "3.12.0"
"@mikro-orm/knex": "6.4.9",
"mysql2": "3.13.0"
},
"engines": {
"node": ">= 18.12.0"
@ -1178,9 +1195,9 @@
}
},
"node_modules/@mikro-orm/reflection": {
"version": "6.4.6",
"resolved": "https://registry.npmjs.org/@mikro-orm/reflection/-/reflection-6.4.6.tgz",
"integrity": "sha512-7mL7HFVnaOOhDNgLjjndWyeJUtOl2wKn0spSqB8uRjS4XtwNEGVZNkW5YD1t/x7TJ99wUhe+oRDiySciiJSeBQ==",
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/@mikro-orm/reflection/-/reflection-6.4.9.tgz",
"integrity": "sha512-fgY7yLrcZm3J/8dv9reUC4PQo7C2muImU31jmzz1SxmNKPJFDJl7OzcDZlM5NOisXzsWUBrcNdCyuQiWViVc3A==",
"license": "MIT",
"dependencies": {
"globby": "11.1.0",
@ -1307,9 +1324,9 @@
}
},
"node_modules/@rushstack/node-core-library": {
"version": "5.11.0",
"resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.11.0.tgz",
"integrity": "sha512-I8+VzG9A0F3nH2rLpPd7hF8F7l5Xb7D+ldrWVZYegXM6CsKkvWc670RlgK3WX8/AseZfXA/vVrh0bpXe2Y2UDQ==",
"version": "5.12.0",
"resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.12.0.tgz",
"integrity": "sha512-QSwwzgzWoil1SCQse+yCHwlhRxNv2dX9siPnAb9zR/UmMhac4mjMrlMZpk64BlCeOFi1kJKgXRkihSwRMbboAQ==",
"license": "MIT",
"dependencies": {
"ajv": "~8.13.0",
@ -1358,12 +1375,12 @@
}
},
"node_modules/@rushstack/terminal": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.15.0.tgz",
"integrity": "sha512-vXQPRQ+vJJn4GVqxkwRe+UGgzNxdV8xuJZY2zem46Y0p3tlahucH9/hPmLGj2i9dQnUBFiRnoM9/KW7PYw8F4Q==",
"version": "0.15.1",
"resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.15.1.tgz",
"integrity": "sha512-3vgJYwumcjoDOXU3IxZfd616lqOdmr8Ezj4OWgJZfhmiBK4Nh7eWcv8sU8N/HdzXcuHDXCRGn/6O2Q75QvaZMA==",
"license": "MIT",
"dependencies": {
"@rushstack/node-core-library": "5.11.0",
"@rushstack/node-core-library": "5.12.0",
"supports-color": "~8.1.1"
},
"peerDependencies": {
@ -1391,12 +1408,12 @@
}
},
"node_modules/@rushstack/ts-command-line": {
"version": "4.23.5",
"resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.23.5.tgz",
"integrity": "sha512-jg70HfoK44KfSP3MTiL5rxsZH7X1ktX3cZs9Sl8eDu1/LxJSbPsh0MOFRC710lIuYYSgxWjI5AjbCBAl7u3RxA==",
"version": "4.23.6",
"resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.23.6.tgz",
"integrity": "sha512-7WepygaF3YPEoToh4MAL/mmHkiIImQq3/uAkQX46kVoKTNOOlCtFGyNnze6OYuWw2o9rxsyrHVfIBKxq/am2RA==",
"license": "MIT",
"dependencies": {
"@rushstack/terminal": "0.15.0",
"@rushstack/terminal": "0.15.1",
"@types/argparse": "1.0.38",
"argparse": "~1.0.9",
"string-argv": "~0.3.1"
@ -1563,9 +1580,9 @@
}
},
"node_modules/@types/jsonwebtoken": {
"version": "9.0.8",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.8.tgz",
"integrity": "sha512-7fx54m60nLFUVYlxAB1xpe9CBWX2vSrk50Y6ogRJ1v5xxtba7qXTg5BgYDN5dq+yuQQ9HaVlHJyAAt1/mxryFg==",
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz",
"integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1588,9 +1605,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.17.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.19.tgz",
"integrity": "sha512-LEwC7o1ifqg/6r2gn9Dns0f1rhK+fPFDoMiceTJ6kWmVk6bgXBI/9IOWfVan4WiAavK9pIVWdX0/e3J+eEUh5A==",
"version": "20.17.24",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.24.tgz",
"integrity": "sha512-d7fGCyB96w9BnWQrOsJtpyiSaBcAYYr75bnK6ZRjDbql2cGLj/3GsL5OYmLPNq76l7Gf2q4Rv9J2o6h5CrD9sA==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.19.2"
@ -1657,9 +1674,9 @@
}
},
"node_modules/acorn": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
"version": "8.14.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
"dev": true,
"license": "MIT",
"bin": {
@ -1932,9 +1949,9 @@
}
},
"node_modules/bullmq": {
"version": "5.41.0",
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.41.0.tgz",
"integrity": "sha512-GGfKu2DHGIvbnMtQjR/82wvWsdCaGxN5JGR3pvKd1mkDI9DsWn8r0+pAzZ6Y4ImWXFaetaAqywOhv2Ik0R2m3g==",
"version": "5.41.9",
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.41.9.tgz",
"integrity": "sha512-dcmBEo6CzLh3PQ7KN6qz5M8VsSv49vcXfEadgrUdfmUvYDpg630/yPV/Ov18unPT4IOCjz1XZSgpjs5DjwlBYw==",
"license": "MIT",
"dependencies": {
"cron-parser": "^4.9.0",
@ -1969,13 +1986,13 @@
}
},
"node_modules/call-bound": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz",
"integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"get-intrinsic": "^1.2.6"
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
@ -2421,9 +2438,9 @@
}
},
"node_modules/esbuild": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz",
"integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz",
"integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@ -2434,30 +2451,31 @@
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.23.1",
"@esbuild/android-arm": "0.23.1",
"@esbuild/android-arm64": "0.23.1",
"@esbuild/android-x64": "0.23.1",
"@esbuild/darwin-arm64": "0.23.1",
"@esbuild/darwin-x64": "0.23.1",
"@esbuild/freebsd-arm64": "0.23.1",
"@esbuild/freebsd-x64": "0.23.1",
"@esbuild/linux-arm": "0.23.1",
"@esbuild/linux-arm64": "0.23.1",
"@esbuild/linux-ia32": "0.23.1",
"@esbuild/linux-loong64": "0.23.1",
"@esbuild/linux-mips64el": "0.23.1",
"@esbuild/linux-ppc64": "0.23.1",
"@esbuild/linux-riscv64": "0.23.1",
"@esbuild/linux-s390x": "0.23.1",
"@esbuild/linux-x64": "0.23.1",
"@esbuild/netbsd-x64": "0.23.1",
"@esbuild/openbsd-arm64": "0.23.1",
"@esbuild/openbsd-x64": "0.23.1",
"@esbuild/sunos-x64": "0.23.1",
"@esbuild/win32-arm64": "0.23.1",
"@esbuild/win32-ia32": "0.23.1",
"@esbuild/win32-x64": "0.23.1"
"@esbuild/aix-ppc64": "0.25.1",
"@esbuild/android-arm": "0.25.1",
"@esbuild/android-arm64": "0.25.1",
"@esbuild/android-x64": "0.25.1",
"@esbuild/darwin-arm64": "0.25.1",
"@esbuild/darwin-x64": "0.25.1",
"@esbuild/freebsd-arm64": "0.25.1",
"@esbuild/freebsd-x64": "0.25.1",
"@esbuild/linux-arm": "0.25.1",
"@esbuild/linux-arm64": "0.25.1",
"@esbuild/linux-ia32": "0.25.1",
"@esbuild/linux-loong64": "0.25.1",
"@esbuild/linux-mips64el": "0.25.1",
"@esbuild/linux-ppc64": "0.25.1",
"@esbuild/linux-riscv64": "0.25.1",
"@esbuild/linux-s390x": "0.25.1",
"@esbuild/linux-x64": "0.25.1",
"@esbuild/netbsd-arm64": "0.25.1",
"@esbuild/netbsd-x64": "0.25.1",
"@esbuild/openbsd-arm64": "0.25.1",
"@esbuild/openbsd-x64": "0.25.1",
"@esbuild/sunos-x64": "0.25.1",
"@esbuild/win32-arm64": "0.25.1",
"@esbuild/win32-ia32": "0.25.1",
"@esbuild/win32-x64": "0.25.1"
}
},
"node_modules/escalade": {
@ -2599,9 +2617,9 @@
}
},
"node_modules/fastq": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz",
"integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==",
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
"integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
"license": "ISC",
"dependencies": {
"reusify": "^1.0.4"
@ -2739,17 +2757,17 @@
}
},
"node_modules/get-intrinsic": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz",
"integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.0.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.0",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
@ -2971,9 +2989,9 @@
}
},
"node_modules/ioredis": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.5.0.tgz",
"integrity": "sha512-7CutT89g23FfSa8MDoIFs2GYYa0PaNiW/OrT+nRyjRXHDZd17HmIgy+reOQ/yhh72NznNjGuS8kbCAcA4Ro4mw==",
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.0.tgz",
"integrity": "sha512-tBZlIIWbndeWBWCXWZiqtOF/yxf6yZX3tAlTJ7nfo5jhd6dctNxF7QnYlZLZ1a0o0pDoen7CgZqO+zjNaFbJAg==",
"license": "MIT",
"dependencies": {
"@ioredis/commands": "^1.1.1",
@ -3316,9 +3334,9 @@
"license": "MIT"
},
"node_modules/long": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.0.tgz",
"integrity": "sha512-5vvY5yF1zF/kXk+L94FRiTDa1Znom46UjPCH6/XbSvS8zBKMFBHTJk8KDMqJ+2J6QezQFi7k1k8v21ClJYHPaw==",
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.1.tgz",
"integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==",
"license": "Apache-2.0"
},
"node_modules/lru-cache": {
@ -3328,9 +3346,9 @@
"license": "ISC"
},
"node_modules/lru.min": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.1.tgz",
"integrity": "sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.2.tgz",
"integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==",
"license": "MIT",
"engines": {
"bun": ">=1.0.0",
@ -3375,9 +3393,9 @@
}
},
"node_modules/mariadb/node_modules/@types/node": {
"version": "22.13.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz",
"integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==",
"version": "22.13.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz",
"integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.20.0"
@ -3460,9 +3478,9 @@
}
},
"node_modules/mikro-orm": {
"version": "6.4.6",
"resolved": "https://registry.npmjs.org/mikro-orm/-/mikro-orm-6.4.6.tgz",
"integrity": "sha512-Lr3uFK06O/4F/AtQAsuYD6QH7DgmUooSVFVGf1y02IuiKVFKOMJ4iKimkRMyoA+ykKhgYIp8WiaEqbWJVuz4Vw==",
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/mikro-orm/-/mikro-orm-6.4.9.tgz",
"integrity": "sha512-XwVrWNT4NNwS6kHIKFNDfvy8L1eWcBBEHeTVzFFYcnb2ummATaLxqeVkNEmKA68jmdtfQdUmWBqGdbcIPwtL2Q==",
"license": "MIT",
"engines": {
"node": ">= 18.12.0"
@ -3561,9 +3579,9 @@
}
},
"node_modules/mysql2": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.12.0.tgz",
"integrity": "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==",
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.13.0.tgz",
"integrity": "sha512-M6DIQjTqKeqXH5HLbLMxwcK5XfXHw30u5ap6EZmu7QVmcF/gnh2wS/EOiQ4MTbXz/vQeoXrmycPlVRM00WSslg==",
"license": "MIT",
"dependencies": {
"aws-ssl-profiles": "^1.1.1",
@ -3883,9 +3901,9 @@
}
},
"node_modules/prettier": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.1.tgz",
"integrity": "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw==",
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"dev": true,
"license": "MIT",
"bin": {
@ -4127,9 +4145,9 @@
}
},
"node_modules/reusify": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
"license": "MIT",
"engines": {
"iojs": ">=1.0.0",
@ -4763,13 +4781,13 @@
"license": "0BSD"
},
"node_modules/tsx": {
"version": "4.19.2",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz",
"integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==",
"version": "4.19.3",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz",
"integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.23.0",
"esbuild": "~0.25.0",
"get-tsconfig": "^4.7.5"
},
"bin": {
@ -4783,9 +4801,9 @@
}
},
"node_modules/type-fest": {
"version": "4.34.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.34.1.tgz",
"integrity": "sha512-6kSc32kT0rbwxD6QL1CYe8IqdzN/J/ILMrNK+HMQCKH3insCDRY/3ITb0vcBss0a3t72fzh2YSzj8ko1HgwT3g==",
"version": "4.37.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.37.0.tgz",
"integrity": "sha512-S/5/0kFftkq27FPNye0XM1e2NsnoD/3FS+pBmbjmmtLT6I+i344KoOf7pvXreaFsDamWeaJX55nczA1m5PsBDg==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=16"
@ -4808,9 +4826,9 @@
}
},
"node_modules/typescript": {
"version": "5.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",

Binary file not shown.

View File

@ -45,15 +45,16 @@ export abstract class BaseEvent {
return character?.getRole() === 'gm'
}
protected emitError(message: string): void {
protected sendNotificationAndLog(message: string): void {
console.log(message)
this.socket.emit(SocketEvent.NOTIFICATION, { title: 'Server message', message })
this.logger.error('Base event error', `Player ${this.socket.userId}: ${message}`)
this.logger.error('Base event error' + `Player ${this.socket.userId}: ${message}`)
}
protected handleError(context: string, error: unknown): void {
console.log(error)
const errorMessage = error instanceof Error ? error.message : error && typeof error === 'object' && 'toString' in error ? error.toString() : String(error)
this.socket.emit(SocketEvent.NOTIFICATION, { title: 'Server message', message: `Server error occured. Please contact the server administrator.` })
this.logger.error('Base event error', errorMessage)
this.logger.error('Base event error: ' + errorMessage)
}
}

View File

@ -1,4 +1,6 @@
export enum SocketEvent {
CONNECT_ERROR = 'connect_error',
RECONNECT_FAILED = 'reconnect_failed',
CLOSE = '52',
DATA = '51',
CHARACTER_CONNECT = '50',
@ -35,7 +37,7 @@ export enum SocketEvent {
GM_MAP_REQUEST = '19',
GM_MAP_UPDATE = '18',
MAP_CHARACTER_MOVEERROR = '17',
DISCONNECT = '16',
DISCONNECT = 'disconnect',
USER_DISCONNECT = '15',
LOGIN = '14',
LOGGED_IN = '13',
@ -43,7 +45,7 @@ export enum SocketEvent {
DATE = '11',
FAILED = '10',
COMPLETED = '9',
CONNECTION = '8',
CONNECTION = 'connection',
WEATHER = '7',
CHARACTER_DISCONNECT = '6',
MAP_CHARACTER_ATTACK = '5',

View File

@ -58,3 +58,16 @@ export const ZCharacterCreate = z.object({
.max(255, { message: 'Name must be at most 255 characters long' })
.regex(/^[A-Za-z][A-Za-z0-9_-]*$/, { message: 'Name must start with a letter and can only contain letters, numbers, underscores, or dashes' })
})
export const ZCharacterConnect = z.object({
characterId: z.string(),
characterHairId: z.string().optional().nullable(),
newNickname: z
.string()
.min(3, { message: 'Name must be at least 3 characters long' })
.max(255, { message: 'Name must be at most 255 characters long' })
.regex(/^[A-Za-z][A-Za-z0-9_-]*$/, {
message: 'Name must start with a letter and can only contain letters, numbers, underscores, or dashes'
})
.optional()
})

View File

@ -30,7 +30,8 @@ export default class InitCommand extends BaseCommand {
// Assets
await this.importTiles()
await this.importMapObjects()
await this.createCharacterType()
await this.createMaleCharacterType()
// await this.createFemaleCharacterType()
await this.createCharacterHair()
// await this.createCharacterEquipment()
@ -74,9 +75,9 @@ export default class InitCommand extends BaseCommand {
}
}
private async createCharacterType(): Promise<void> {
private async createMaleCharacterType(): Promise<void> {
const characterSprite = new Sprite()
characterSprite.setId('023d1e9d-f57f-4faa-8412-86c07107cf85').setName('Character')
characterSprite.setId('023d1e9d-f57f-4faa-8412-86c07107cf85').setName('Male character')
await characterSprite.save()
const idleRightDownAction = new SpriteAction()
@ -272,7 +273,213 @@ export default class InitCommand extends BaseCommand {
const characterType = new CharacterType()
await characterType
.setId('75b70c78-17f0-44c0-a4fa-15043cb95be0')
.setName('New character type')
.setName('Male character')
.setGender(CharacterGender.MALE)
.setRace(CharacterRace.HUMAN)
.setIsSelectable(true)
.setSprite(characterSprite)
.save()
}
private async createFemaleCharacterType(): Promise<void> {
const characterSprite = new Sprite()
characterSprite.setId('023d1e9d-f57f-4faa-8412-86c07107cf85').setName('Male character')
await characterSprite.save()
const idleRightDownAction = new SpriteAction()
await idleRightDownAction
.setAction('idle_right_down')
.setSprites([
{
url: 'data:image/png;base64,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',
offset: {
x: 0,
y: 0
}
}
])
.setOriginX(0)
.setOriginY(0)
.setFrameWidth(28)
.setFrameHeight(94)
.setFrameRate(0)
.setSprite(characterSprite)
.save()
const idleLeftUpAction = new SpriteAction()
await idleLeftUpAction
.setAction('idle_left_up')
.setSprites([
{
url: 'data:image/png;base64,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',
offset: {
x: 0,
y: 0
}
}
])
.setOriginX(0)
.setOriginY(0)
.setFrameWidth(26)
.setFrameHeight(93)
.setFrameRate(0)
.setSprite(characterSprite)
.save()
const walkRightDownAction = new SpriteAction()
await walkRightDownAction
.setAction('walk_right_down')
.setSprites([
{
url: 'data:image/png;base64,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',
offset: {
x: 7,
y: 8
}
},
{
url: 'data:image/png;base64,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',
offset: {
x: 7,
y: 2
}
},
{
url: 'data:image/png;base64,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',
offset: {
x: 2,
y: 2
}
},
{
url: 'data:image/png;base64,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',
offset: {
x: 0,
y: 0
}
}
])
.setOriginX(0)
.setOriginY(0)
.setFrameWidth(36)
.setFrameHeight(102)
.setFrameRate(7)
.setSprite(characterSprite)
.save()
const walkLeftUpAction = new SpriteAction()
await walkLeftUpAction
.setAction('walk_left_up')
.setSprites([
{
url: 'data:image/png;base64,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',
offset: {
x: 3,
y: 2
}
},
{
url: 'data:image/png;base64,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',
offset: {
x: 0,
y: 2
}
},
{
url: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAABZCAYAAADVeL+8AAAFcElEQVR4nLVZzUtbWRT/JUqURAkkIWlBeIUmuDHQrqQI3Ygu/Qtk6LLMUldDqYtAF10ozKrMLjDC7LNsEMSdm7GQboIRfBDQ99BHxQ/aQM0sknM9971z33upmQOB5H78zu/ee+75uEks//EXoiTXqPWldm9tKxE2bzIO6MbqIqzqvNZ3tHeA+rDfpMQInmvU+huri6gsr2Lm+ROt7+bkHC+XXyNXLMFzHdQbtb6kQATPNWr9Pzd/w3RlAelCBhP5itY/A+AGgFUFLAxWJClImhinipYIrCZmZ5EqWoM5xZI8RmqsLK8imZ0VJ0hiVefx5sWzwMFr4MQ6TH5eHuPu4nYwOYJAYM+t6ryadHdxizSOtX4CjiOhphgFdn91DQCwW22xX9xzmhRXPNfBq7e/R4ObWJgIHO0diMAiuH9yWN/hP38r4O/HXwPjAnvuuU4sBYBu39KKReY91w4FpTHkb74ff9VIkWjMvbWtRL1R6+eKJVhVfWCqaAWU2q02LANrI3PPdSIPlvrtVltkLYJ7a1uJ+pfTgAJ+G6ndcx0jsJn5UAEHkoC51L+cBvy60RS9ta3EzudDIysOKgGHgo8ipkgUG3xUlwAACX+A5j6Z3K8/fpIc7R0AkPc7AJ5r1PpvXjxTN8+qzgfsm6IPtZEpkgFwJZMSMDFNFa1BOMNAAf1OFzK4Gc6jOCoFa+2GSsDpQgZ3AOAOxqQLGQADu7+/ulYrkYJ1krPmwCQT+YoC5G1cZp4/Qe7VoiJIoqyFGqVrz8Ek4Il8BRP5CqYrC1qwVuDcIvgB/rzUY6i/7ebkXP1OFzLaqjU7t1tt7Hw+DLDnYH5lPdfGzcm5WlUyO6t2QYFzjaMIEZFWOJbr7xfa4kjwuHnK3cVtgH0s5mEKRvbnJCZnZVLmb49kblLA200BfeQDlVhHBuieaxv3z88+rm8fmymSQu6HjOCmfZRYe66DnmurSoQu5KOYR2VmY9kWikIjgcfJGcPEmM5xBaSEK+u5dmTKZ2RumsiVRUkA/LI0j+2zqchkNCwBJRELrpWVFWw3m8DZN3xAUAGBbp9NAQDyQ2UUR2llxmpufX0djuPgXbMp9E4pEgDw7+4DAe52RfBSqYRyuYxyuYylpSUAQKfTEUk4jnlrRPByuSwO9rd3Oh2USnLdD7AD9R/e3Nyc+pgUmkgQlmiKBOj/7VcYJQo8yqyiCJBwf58EHsoUk48g6Xa7xj7Pdcxhjqe++/v7AVCylm63qz4mCyIJWMvm0x/Y3t0F8HBgBCKB+U2RF7xifv6hCLwbKiChC0OmR6DNZhN5BmxkzvPz3apeNYgXdbjSOiNvt9qqjNHA+dvW/dX1oERvAcApNp/+UAQAPSvmNSu3Os3OqZKg78CDiXJL4kUCT2BplWQcGjgFWH8lAehlOwEns7NIZmfx6eN7VezyV7rINy7OxP+qMZ1dUKv89PE9i06nD9tClsJLElPiQ2W73Wor6+BjxbKFREriRSXD7SFgKiUBBCsLXoXRNTaFOdp/u9VWMZUXw6RYYx6XtZ89MfcTCmwLdz6jekoSMgINnIDjZrH+1+fQS3R/da0dUFwxVYKhT3+mpw5ezoeVmGIF/RgxXn8uUXmgsuWQN/SxpNB+JxcA/5Xy3D8nVyxpjivgcklGtXGJ3CSdPAGTZzNZCpeeayMF/c1LA6cv6UJmpP8jPNcZRqm23uYH99+0qC0hv05+W+pX4L9i41FbBvxP7y2h4Ka/w8YCPi4RwcflZ4zMpT/yHgWeLmQwXVlAqmjh5fJrbKwuPkqBAh/lAsUVdUPvr64HD8LjBrdbbVhVaH4iToUcCe6tbSV2GrX+BgDyE6bH9lFFvfz7D+4xoCT/AaNCPNJBtKgMAAAAAElFTkSuQmCC',
offset: {
x: 5,
y: 6
}
},
{
url: 'data:image/png;base64,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',
offset: {
x: 2,
y: 6
}
}
])
.setOriginX(0)
.setOriginY(0)
.setFrameWidth(34)
.setFrameHeight(101)
.setFrameRate(7)
.setSprite(characterSprite)
.save()
const attackRightDownAction = new SpriteAction()
await attackRightDownAction
.setAction('attack_right_down')
.setSprites([
{
url: 'data:image/png;base64,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',
offset: {
x: 20,
y: 0
}
},
{
url: 'data:image/png;base64,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',
offset: {
x: 19,
y: 8
}
},
{
url: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAABbCAYAAAD5qDz1AAAHtElEQVR4nL1aTWgbRxT+LAfF2BEqa0drQ2ANsdBFpjKUCDckF1fuJfiQnErdoINoTCgYYgjkEB9cSunBgVza5GbSQ0mPPjrooosRFOQgQxFyQAJDsouzxCg2jiF2D6s3ml3t7M6u5X6woP3/9s1733vzRn0zj54jDJT1lVO34+bccl+oBwK4EJZEPjOOqZmbXecXV63zYUgFIqOsr5zmM+NQEiq0yRQGkmkMjgzZrnn+LI2j+jYWV1dOgxKKBCUyNXPTRqR/OGnbBkeGMJBM4+nSXeFQnpkMACgJFQAwkEwLrzkLISkyZBVtMiX7XADepEOTEeFw7wCf39c9rwlincDR5EZoEPWuY2FwZjJnebkT0sM0vXAfzWoNAHCy35J+weLqi96TOapvI3X7DirFEo6Npu3cvYWHuLfwsOueewsPUUpP9J4MWWV64T4qxVLXC1++bnQRevm6IU0kEBnT0HFU3wZg6Q0NFb1w7PtvAr88FBlzbrlvbavBrKNNpnBsNHGy38KHzb/Zdfxv2q98+7U0mUDRRNZpVmuMUBQa/v3pR9t1fL468pYhG+SHibOOaejsODnz4MgQ2whBQz6QAptzy31PNsoAYHPik/0WDvcOuraT/RYWV19IlxOh0sHaVsNGiPzHuTklwA+hFDifGWeECO6FlrxVQpMhQkDHSk5yQPBqr0+2BubLTar0RGhWayDfCkLK1zI8CQIRoWrvcO+ACeJAMo1UMo2nkykWeWvrciWop2WoqAI6VZ6bRfhwd7NapVjC2lbD10JCMs7iW5nOAgDMzbJrBceLIdCp8ug4ADzZKHsS8hwmnkj/cLJ91PKFWCrDrrOqvTS09j5frAOABst6D2azeOIxZK46w9e8A8k0RwSIJjSOgAX+vNsxsphfDe0petGEZpN3ngDVv7Q54XY8mtCsaBTUxMJhUhIqIvEY+8LP7+s43DtgyTESj9lyD1/9WZGV5n5bRC5dHW0HQsP1nV2WUdZXTh/MZpGcmWVWadW2upKeSPrJWY/q2zjZb7FIi1+7gf7hpOdQCYeJrMKb+qi+7Vp20n6lWIJp6KgUS6gUSyj/9ScTP/45oqFyHSayCg0NfTl9NU+ASAAdLSLwpYa5WcblW0lcvjUPcBlfSIaiKBKPAejUI0SEHl5xPGxq5iaLMp6sWbSuX9tqQEmoUKbr6B9OCv3GNx2c7Le6Xs6rbDShMfKUGgAgCjs509Dx8c07xMlvuNwlJEOm5oeHMjKlBicRXuBiw+3Ic/mwenEDU1dHhR/OHJiGKHX7Djt5bDRZTUJlJ4G3iFP0nPt0r2noMDfLQr2xRZOSUFk48znFC85mkd9xeqbT2bvI8BrgLLxlwKuuU5ecltUmU13WsfmMMxvLpH2eCCOwtwUArlFjGrql4o7oAwSid7LfkrZKkOmIczLoHKoI0HFeGmeKoCA1bCBS7Q91pgZmGb66l3FcoJMcnSnDk0jbOvXiBpTpLP747THzG9swUUgGdVwesh/Cv4/AyARtBhJoNkkgnZJpKJFFKap60kYL0skikPX5GodZRiRSbmhWa4Gnrm74+OYdgI4jR5T1ldOnS3dtF/lFkmnojJCI1LHRFPqeW2oBAk5v36sprL5t77z9ALwuY2nsEwB7Jif1Xn17EVBTEH3VsdEEuMQZ2GdyuZxtf/XVKxu5Di4il8vhFZ13QaVYQjahIX7tRjgyPFRVxfz8vPC8rvtLxLHRxOf3o2Iy+cy47/xYVS0pn5jotFavXLnCfu/u7nqSMOeW+9bWV06VhMp8pis3dcpCf4iI0P7ExARyuRxOyy+FaweUOG1kSLj++fXxmRTYSej69evC8xRVVMBFALuEm4YunSR3dnbY793dXdehcVrMjRC96wIRCKOiTkJOAoVCAcN6DVDl1qkiZCrLq+u+VqFQ1XXdNVp2dnZQKBRQKBSwNPYJ+cy4RUgCLJoqxRJm2j0YEYb1GvKZ8Y62CLA09omJYLNaQz4Dqe6VNUztMJuWLJB++fIL2z45/NpWw9bziyY0aJNgkzk/2EJ789nvvjfw7TSSf5EUROIxRBOatFQwMmxuIxFFfLlIv+leZwv20tVR15mAJxkiJPMFBFJOkS5RWSJbuIVevSUiblMOJ7nBkSGpoTrTUjIQbiZxZjJ8T5hHkAK8Z2TC4FzXm9zg9A8+omgOdbh3IBVRof6YAXTaaCJ/oeH7+OaddN4L9McM54tkZwiy4heq7DQNHah6zzxNQ4eGFOtsuXWynJCPJu6rlIQqXfeQ8MnMy3zJ8EMk0/8H7K0PiqjDvQPfCjLwMFEmRhUQtd15HNW3pde25RzYZxmwVwhkGbc8JANKlEq1Bi9relrGLQVQt9xvVc2JnjhwGPAf4fZfvtBkeH/pRRskFBmvLC07RNpkKlDf51yzdlBIR1Mv6xYRpC1D6itbXPPdBUCutpEmE01obMt+94PsbQwyZYQ0GT6S6sWNQERkKz6hz9AsM5+xZoRW1rb8JkjLhF9EC02GJ2TtNbrOed1L2hSku+EbTb2Ygsji3HQmTFL930TPqoNSeDCbFcrCuZHhHTYSjyESj0GZzuKrRz8LdercyFA7n1/zBqyVFNEsoSerKm6gGQR5Dl96iqRB+t9oQeGXLtyi9D+rtYwUcNakSwAAAABJRU5ErkJggg==',
offset: {
x: 17,
y: 3
}
}
])
.setOriginX(0)
.setOriginY(0)
.setFrameWidth(69)
.setFrameHeight(111)
.setFrameRate(5)
.setSprite(characterSprite)
.save()
const attackLeftUpAction = new SpriteAction()
await attackLeftUpAction
.setAction('attack_left_up')
.setSprites([
{
url: 'data:image/png;base64,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',
offset: {
x: 2,
y: 0
}
},
{
url: 'data:image/png;base64,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',
offset: {
x: 5,
y: 0
}
},
{
url: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAABcCAYAAACFtS4PAAAFtUlEQVR4nKVaPWgbSRT+ZB9KsGMcZLHKcYY1xELFRcSpQpo0wi7dXXFgDpd3VzpVOHAhcBnDcVU6QwLXi6ss3BhcuLJBbnRywAvmkl1kcca28AkiXbF+4/l5MxrZXxNrd+Z73868efPeTDKVt+/hQq5WHXDPO8vrGWdHAN8MI11beomwXFLeHezsYuvmvcsIS56rVQdrSy9RrCzh0dMnyrvLT1/wovIauaCAThJjq1Yd2AwY5LladfD7m5/wsPgME/lJjM8UlfePAFwCCMtAiPSLbAbGOMXZIGSJRafpKWSDMO0TFNg2BjkAFCtLGJuesnbQEZZLWF2YYydekJNqF76etdBtX6UdPQQoYx6WS6JTt32FCbSUxkTsC6srDiPrn18AAKJG09rGGHPq5ItOEuPVz78OJ3ep4AQc7OxaiQ1yvbPr3f6fHwTxdeuIbauMeSeJvQwAqn/bvthQ3ksiJym1oXhz3TpSRMkQyjvL65mtWnWQCwoIy2qjbBAaRqNGE6FDNau8k8RDJ5beR42mVbVB3llez2wdnhgG5NVIzztJ7CTmld8YkIk4YhlbhydsXGddsbO8ntnc3neqIlIbsZVch8stXTtRhvZQPWRShNS3OMLBzi6AVL3NSKby9j1ytepgdWEuNXKzOMJyyXBB2iDomewtnBFlherEY9NTyCI1QL8n8pO4vGkvb3Xcnpr54fvvBqsLc0gXT0koJKJu+wrXrSNkg1Bs1t32FTsPvSRKM4ObSRYTSmqBW78enyliIj+pEBgb9tMnwmg2CJWYM0afRMQ6ZDJuwx6fKWJ8pigMFCtLYk8VyuVVSJ/89awFHdwzMkIGSL0g759foJdExqqUyXTiXhJZjSnkgP9OJOPy0xc7OS31XhINDUS+IB7n8h+WShzs7LKbi+GKAPDX3/94G/DZsazKbcHKZoybWEHOTabNQP/8QmlPxN32lfJFXiGXIKvWJ59CAkVLL3JdvfybVjYR95JI2TwE+V3cUI/1+tCyym2ewM0BhQ1unYwB6qbsC3JbPWoa5KNC/jJbaWOQt7s9lkCHLTw7yQF1YntJJIzIxnpJpAgh6GnGrbdID23RUTYmQ19UrPKzQgnvPj8Ymi/6hmajJlpcXMS7eh34/C82YJLY1gP3nC24VlZWEMcxfqvXmbcPAAAzTs0W8kKhgPn5eWEEgPgt449ffhxKbngLEen/2uDaQ511KBHPzs4qz09PT1WFlmqaXaEyGf2dz+eRz+eVZ1GjmVba+Un2gMG6/HW1NlDKx8E6LPqnt9ttth0p52Ao39vbE8THx8eGEd2wt/KZuIl6PXVHAIhj9wbSSWJn+mEMy5tv/8O7jx+VZ4uLiwD8jVrJAWDj+eM0kT88werCXBoONMgr1FZFs+RhuYTOzm3jjeePlf0yajSxuX1LPJJygNLgE+SCgnLu1T+/SMt3qZSMGk22ZGQraADY3N7H6sKcOJoid9NXo6tEZxcRxWvKS+gYkPNnmhuuVGSHRVZiixurC3POGtRKTgjLJePwknIXV1kuhBmqb8adil6CfKbou80585ZsELJ5iW/qZyXPBQVRncmqKdm8FzkHWwphw9AT0Qm0nHn5ncn75xfoSn+PCie5TupTZMmwjrme2BMx56Yjk3OQw8K9yOU0mVSPWtoY5PKRlAwKqxQpbVcJTnIdtkn0GRqWnOtIQ0Jh2Ec9S2477vMRYSWn8X5YfGac3+ou6KOeVe4q/2RkgxAvKq+H3xMJ9Y7xHtWAQR6WS87aEjArOxtGWqFyHJcrO1sYFoGLbhNzr9QrNDknydWqg04SAw2VZOidhQ/oXgM4sb53kuvjHZZLSnblcz1M8BpznzhyZ/JcULiTAYOcSr+J/ORIl65e5KPeeY5E3j+/UPKU+0DxlqjRRFiGuDagU+m7nvEqd3ObtepgDQCk0wr5wmMUNwSkqxzCff77g47/AX/tU4BIJmZXAAAAAElFTkSuQmCC',
offset: {
x: 6,
y: 1
}
}
])
.setOriginX(0)
.setOriginY(0)
.setFrameWidth(34)
.setFrameHeight(100)
.setFrameRate(5)
.setSprite(characterSprite)
.save()
const characterType = new CharacterType()
await characterType
.setId('75b70c78-17f0-44c0-a4fa-15043cb95be0')
.setName('Male character')
.setGender(CharacterGender.MALE)
.setRace(CharacterRace.HUMAN)
.setIsSelectable(true)
@ -282,7 +489,7 @@ export default class InitCommand extends BaseCommand {
private async createCharacterHair(): Promise<void> {
const hairSprite = new Sprite()
hairSprite.setId('922ee95f-1500-49c0-8ead-f8cc46dad136').setName('Hair 1')
hairSprite.setId('922ee95f-1500-49c0-8ead-f8cc46dad136').setName('Hair 1').setWidth(30).setHeight(40)
await hairSprite.save()
const frontAction = new SpriteAction()
@ -326,7 +533,7 @@ export default class InitCommand extends BaseCommand {
.save()
const characterHair = new CharacterHair()
await characterHair.setId('a2471230-d238-4ffb-9eca-9eab869f1b67').setName('Hair 1').setGender(CharacterGender.MALE).setIsSelectable(true).setSprite(hairSprite).save()
await characterHair.setId('a2471230-d238-4ffb-9eca-9eab869f1b67').setName('Hair 1').setGender(CharacterGender.MALE).setColor('#1B1212').setIsSelectable(true).setSprite(hairSprite).save()
}
private async createMap(): Promise<void> {
@ -345,6 +552,8 @@ export default class InitCommand extends BaseCommand {
private async createUser(): Promise<void> {
const user = new User()
await user.setId('6f9a58b4-172d-425e-b9ea-71e1d13d81ee').setUsername('root').setEmail('local@host').setPassword('password').setOnline(false).save()
const map = await this.mapRepository.getFirst()
if (!map) return
const character = new Character()
await character
@ -352,7 +561,7 @@ export default class InitCommand extends BaseCommand {
.setUser(user)
.setName('root')
.setRole('gm')
.setMap(await this.mapRepository.getFirst())
.setMap(map)
.setCharacterType(await this.characterTypeRepository.getFirst())
.setCharacterHair(await this.characterHairRepository.getFirst())
.save()

View File

@ -65,9 +65,12 @@ export class AvatarController extends BaseController {
return this.sendError(res, 'Body sprite file not found', 404)
}
// Get body sprite metadata
const bodyMetadata = await sharp(bodySpritePath).metadata()
let avatar = sharp(bodySpritePath).extend({
top: 2,
bottom: 2,
top: 0,
bottom: 0,
background: { r: 0, g: 0, b: 0, alpha: 0 }
})
@ -76,7 +79,21 @@ export class AvatarController extends BaseController {
if (characterHair?.sprite?.id) {
const hairSpritePath = Storage.getPublicPath('sprites', characterHair.sprite.id, 'front.png')
if (fs.existsSync(hairSpritePath)) {
avatar = avatar.composite([{ input: hairSpritePath, gravity: 'north' }])
// Resize hair sprite to match body dimensions
const resizedHair = await sharp(hairSpritePath)
.resize(bodyMetadata.width, bodyMetadata.height, {
fit: 'contain',
background: { r: 0, g: 0, b: 0, alpha: 0 }
})
.toBuffer()
avatar = avatar.composite([
{
input: resizedHair,
left: 0,
top: -27 // Apply vertical offset
}
])
}
}
}
@ -84,6 +101,7 @@ export class AvatarController extends BaseController {
res.setHeader('Content-Type', 'image/png')
return avatar.pipe(res)
} catch (error) {
console.error('Avatar generation error:', error)
return this.sendError(res, 'Error generating avatar', 500)
}
}

View File

@ -16,11 +16,14 @@ export class BaseCharacterHair extends BaseEntity {
@Property()
gender: CharacterGender = CharacterGender.MALE
@Property()
color: string = '#000000'
@Property()
isSelectable = false
@ManyToOne()
sprite?: Sprite
sprite!: Sprite
@Property()
createdAt = new Date()
@ -55,6 +58,15 @@ export class BaseCharacterHair extends BaseEntity {
return this.gender
}
setColor(color: string) {
this.color = color
return this
}
getColor() {
return this.color
}
setIsSelectable(isSelectable: boolean) {
this.isSelectable = isSelectable
return this

View File

@ -25,8 +25,8 @@ export class BaseItem extends BaseEntity {
@Enum(() => ItemRarity)
rarity: ItemRarity = ItemRarity.COMMON
@ManyToOne(() => Sprite)
sprite?: Sprite
@ManyToOne()
sprite!: Sprite
@Property()
createdAt = new Date()

View File

@ -11,7 +11,7 @@ export class BaseMap extends BaseEntity {
id = randomUUID()
@Property()
name!: string
name: string = ''
@Property()
width = 10
@ -19,7 +19,7 @@ export class BaseMap extends BaseEntity {
@Property()
height = 10
@Property({ type: 'json', nullable: true })
@Property({ type: 'json' })
tiles: Array<Array<string>> = []
@Property()

View File

@ -17,12 +17,12 @@ export class BaseMapEventTile extends BaseEntity {
type!: MapEventTileType
@Property()
positionX!: number
positionX: number = 0
@Property()
positionY!: number
positionY: number = 0
@OneToOne({ eager: true })
@OneToOne({ eager: true, deleteRule: 'cascade', orphanRemoval: true })
teleport?: MapEventTileTeleport
setId(id: UUID) {

View File

@ -9,7 +9,7 @@ export class BaseMapEventTileTeleport extends BaseEntity {
@PrimaryKey()
id = randomUUID()
@OneToOne({ deleteRule: 'cascade' })
@OneToOne({ deleteRule: 'cascade', orphanRemoval: true })
mapEventTile!: MapEventTile
@ManyToOne({ deleteRule: 'cascade', eager: true })

View File

@ -16,6 +16,9 @@ export class BaseMapObject extends BaseEntity {
@Property({ type: 'json' })
depthOffsets = [0]
@Property({ type: 'json' })
pivotPoints: { x: number; y: number }[] = []
@Property({ type: 'decimal', precision: 10, scale: 2 })
originX = 0
@ -66,6 +69,10 @@ export class BaseMapObject extends BaseEntity {
setDepthOffsets(offsets: any) {
this.depthOffsets = offsets
}
setPivotPoints(pivotPoints: { x: number; y: number }[]) {
this.pivotPoints = pivotPoints
return this
}

View File

@ -11,9 +11,15 @@ export class BaseSprite extends BaseEntity {
@Property()
name!: string
@OneToMany(() => SpriteAction, (action) => action.sprite)
@OneToMany({ mappedBy: 'sprite', orphanRemoval: true })
spriteActions = new Collection<SpriteAction>(this)
@Property({ nullable: true })
width: number | null = null
@Property({ nullable: true })
height: number | null = null
@Property()
createdAt = new Date()
@ -47,6 +53,24 @@ export class BaseSprite extends BaseEntity {
return this.spriteActions
}
setWidth(width: number | null) {
this.width = width
return this
}
getWidth() {
return this.width
}
setHeight(height: number | null) {
this.height = height
return this
}
getHeight() {
return this.height
}
setCreatedAt(createdAt: Date) {
this.createdAt = createdAt
return this

View File

@ -2,6 +2,7 @@ import { BaseMap } from '@/entities/base/map'
import { Entity } from '@mikro-orm/core'
export type MapCacheT = ReturnType<Map['cache']> | {}
export type MapEditorMapT = ReturnType<Map['mapEditorObject']> | {}
@Entity()
export class Map extends BaseMap {
@ -35,4 +36,42 @@ export class Map extends BaseMap {
return {}
}
}
public async mapEditorObject() {
try {
await this.getPlacedMapObjects().load()
await this.getMapEffects().load()
await this.getMapEventTiles().load()
return {
id: this.getId(),
name: this.getName(),
width: this.getWidth(),
height: this.getHeight(),
tiles: this.getTiles(),
pvp: this.getPvp(),
createdAt: this.getCreatedAt(),
updatedAt: this.getUpdatedAt(),
placedMapObjects: this.getPlacedMapObjects(),
mapEffects: this.getMapEffects(),
mapEventTiles: this.getMapEventTiles().map((mapEventTile) => ({
id: mapEventTile.getId(),
type: mapEventTile.getType(),
positionX: mapEventTile.getPositionX(),
positionY: mapEventTile.getPositionY(),
teleport: mapEventTile.getTeleport()
? {
toMap: mapEventTile.getTeleport()?.getToMap().getId(),
toPositionX: mapEventTile.getTeleport()?.getToPositionX(),
toPositionY: mapEventTile.getTeleport()?.getToPositionY(),
toRotation: mapEventTile.getTeleport()?.getToRotation()
}
: undefined
}))
}
} catch (error) {
console.error(error)
return {}
}
}
}

View File

@ -1,6 +1,7 @@
import { BaseEvent } from '@/application/base/baseEvent'
import { SocketEvent } from '@/application/enums'
import type { UUID } from '@/application/types'
import { ZCharacterConnect } from '@/application/zodTypes'
import MapManager from '@/managers/mapManager'
import CharacterHairRepository from '@/repositories/characterHairRepository'
import CharacterRepository from '@/repositories/characterRepository'
@ -8,7 +9,8 @@ import TeleportService from '@/services/characterTeleportService'
interface CharacterConnectPayload {
characterId: UUID
characterHairId?: UUID
characterHairId: UUID | null
newNickname?: string
}
export default class CharacterConnectEvent extends BaseEvent {
@ -21,18 +23,35 @@ export default class CharacterConnectEvent extends BaseEvent {
private async handleEvent(data: CharacterConnectPayload, callback: (response: any) => void): Promise<void> {
try {
if (await this.checkForActiveCharacters()) {
this.emitError('You are already connected to another character')
if (data.newNickname === '') data.newNickname = undefined
const result = ZCharacterConnect.safeParse(data)
if (!result.success) {
this.sendNotificationAndLog(result.error?.errors[0]?.message ?? 'Invalid data')
return
}
const character = await this.characterRepository.getByUserAndId(this.socket.userId, data.characterId)
if (await this.checkForActiveCharacters()) {
this.sendNotificationAndLog('You are already connected to another character')
return
}
let character = await this.characterRepository.getByUserAndId(this.socket.userId!, data.characterId)
if (!character) {
this.emitError('Character not found or does not belong to this user')
this.sendNotificationAndLog('Character not found or does not belong to this user')
return
}
if (data.newNickname) {
const existingCharacter = await this.characterRepository.getByName(data.newNickname)
if (existingCharacter) {
this.sendNotificationAndLog('Nickname already in use: ' + data.newNickname)
return
}
await character.setName(data.newNickname).save()
}
// Set character id
this.socket.characterId = character.id
@ -40,6 +59,8 @@ export default class CharacterConnectEvent extends BaseEvent {
if (data.characterHairId !== undefined && data.characterHairId !== null) {
const characterHair = await this.characterHairRepository.getById(data.characterHairId)
await character.setCharacterHair(characterHair).save()
} else {
await character.setCharacterHair(null).save()
}
// Emit character connect event
@ -62,7 +83,7 @@ export default class CharacterConnectEvent extends BaseEvent {
}
private async checkForActiveCharacters(): Promise<boolean> {
const characters = await this.characterRepository.getByUserId(this.socket.userId)
const characters = await this.characterRepository.getByUserId(this.socket.userId!)
return characters?.some((char) => MapManager.getCharacterById(char.id)) ?? false
}
}

View File

@ -32,7 +32,7 @@ export default class CharacterCreateEvent extends BaseEvent {
}
private async createCharacter(data: z.infer<typeof ZCharacterCreate>): Promise<void> {
const user = await this.userRepository.getById(this.socket.userId)
const user = await this.userRepository.getById(this.socket.userId!)
if (!user) {
throw new Error('You are not logged in')
}

View File

@ -4,7 +4,7 @@ import MapManager from '@/managers/mapManager'
export default class DisconnectEvent extends BaseEvent {
public listen(): void {
this.socket.on('disconnect', this.handleEvent.bind(this))
this.socket.on(SocketEvent.DISCONNECT, this.handleEvent.bind(this))
}
private async handleEvent(): Promise<void> {

View File

@ -1,6 +1,7 @@
import { BaseEvent } from '@/application/base/baseEvent'
import { SocketEvent } from '@/application/enums'
import { CharacterGender, SocketEvent } from '@/application/enums'
import { CharacterHair } from '@/entities/characterHair'
import SpriteRepository from '@/repositories/spriteRepository'
export default class CharacterHairCreateEvent extends BaseEvent {
public listen(): void {
@ -11,8 +12,17 @@ export default class CharacterHairCreateEvent extends BaseEvent {
try {
if (!(await this.isCharacterGM())) return
// Get first sprite
const spriteRepository = new SpriteRepository()
const firstSprite = await spriteRepository.getFirst()
if (!firstSprite) {
this.sendNotificationAndLog('No sprites found')
return callback(false)
}
const newCharacterHair = new CharacterHair()
await newCharacterHair.setName('New hair').save()
await newCharacterHair.setName('New hair').setGender(CharacterGender.MALE).setSprite(firstSprite).save()
return callback(true)
} catch (error) {

View File

@ -8,6 +8,7 @@ type Payload = {
id: UUID
name: string
gender: CharacterGender
color: string
isSelectable: boolean
spriteId: UUID
}
@ -29,7 +30,7 @@ export default class CharacterHairUpdateEvent extends BaseEvent {
const characterHair = await characterHairRepository.getById(data.id)
if (!characterHair) return callback(false)
await characterHair.setName(data.name).setGender(data.gender).setIsSelectable(data.isSelectable).setSprite(sprite).setUpdatedAt(new Date()).save()
await characterHair.setName(data.name).setGender(data.gender).setColor(data.color).setIsSelectable(data.isSelectable).setSprite(sprite).setUpdatedAt(new Date()).save()
return callback(true)
} catch (error) {

View File

@ -14,7 +14,10 @@ export default class ItemCreateEvent extends BaseEvent {
const spriteRepository = new SpriteRepository()
const sprite = await spriteRepository.getFirst()
if (!sprite) return callback(false)
if (!sprite) {
this.sendNotificationAndLog('No sprites found')
return callback(false)
}
const newItem = new Item()
await newItem.setName('New Item').setItemType(ItemType.WEAPON).setStackable(false).setRarity(ItemRarity.COMMON).setSprite(sprite).save()

View File

@ -8,6 +8,7 @@ type Payload = {
name: string
tags: string[]
depthOffsets: number[]
pivotPoints: { x: number; y: number }[]
originX: number
originY: number
frameRate: number
@ -32,6 +33,7 @@ export default class MapObjectUpdateEvent extends BaseEvent {
if (data.name !== undefined) mapObject.name = data.name
if (data.tags !== undefined) mapObject.tags = data.tags
if (data.depthOffsets !== undefined) mapObject.depthOffsets = data.depthOffsets
if (data.pivotPoints !== undefined) mapObject.pivotPoints = data.pivotPoints
if (data.originX !== undefined) mapObject.originX = data.originX
if (data.originY !== undefined) mapObject.originY = data.originY
if (data.frameRate !== undefined) mapObject.frameRate = data.frameRate

View File

@ -2,6 +2,7 @@ import { BaseEvent } from '@/application/base/baseEvent'
import { SocketEvent } from '@/application/enums'
import type { UUID } from '@/application/types'
import { Sprite } from '@/entities/sprite'
import { SpriteAction } from '@/entities/spriteAction'
import SpriteRepository from '@/repositories/spriteRepository'
interface CopyPayload {
@ -29,7 +30,21 @@ export default class SpriteCopyEvent extends BaseEvent {
await spriteRepository.getEntityManager().populate(sourceSprite, ['spriteActions'])
const newSprite = new Sprite()
await newSprite.setName(`${sourceSprite.getName()} (Copy)`).setSpriteActions(sourceSprite.getSpriteActions()).save()
await newSprite.setName(`${sourceSprite.getName()} (Copy)`).save()
for (const spriteAction of sourceSprite.getSpriteActions()) {
const newSpriteAction = new SpriteAction()
await newSpriteAction
.setSprite(newSprite)
.setAction(spriteAction.getAction())
.setSprites(spriteAction.getSprites() ?? [])
.setOriginX(spriteAction.getOriginX())
.setOriginY(spriteAction.getOriginY())
.setFrameWidth(spriteAction.getFrameWidth())
.setFrameHeight(spriteAction.getFrameHeight())
.setFrameRate(spriteAction.getFrameRate())
.save()
}
return callback(true)
} catch (error) {

View File

@ -14,76 +14,103 @@ interface SpriteImage {
}
}
interface ImageDimensions {
width: number
height: number
offsetX: number
offsetY: number
interface SpriteActionData {
action: string
sprites: SpriteImage[]
originX: number
originY: number
frameRate: number
}
interface EffectiveDimensions {
width: number
height: number
top: number
bottom: number
}
type Payload = {
interface UpdateSpritePayload {
id: UUID
name: string
spriteActions: Array<{
action: string
sprites: SpriteImage[]
originX: number
originY: number
frameRate: number
}>
spriteActions: SpriteActionData[]
}
export default class SpriteUpdateEvent extends BaseEvent {
private readonly spriteRepository: SpriteRepository = new SpriteRepository()
public listen(): void {
this.socket.on(SocketEvent.GM_SPRITE_UPDATE, this.handleEvent.bind(this))
}
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> {
private async handleEvent(data: UpdateSpritePayload, callback: (success: boolean) => void): Promise<void> {
try {
if (!(await this.isCharacterGM())) return
const spriteRepository = new SpriteRepository()
const sprite = await spriteRepository.getById(data.id)
if (!sprite) return callback(false)
await spriteRepository.getEntityManager().populate(sprite, ['spriteActions'])
// Update sprite in database
await sprite.setName(data.name).save()
// First verify all sprite sheets can be generated
for (const actionData of data.spriteActions) {
if (!(await this.generateSpriteSheet(actionData.sprites, sprite.getId(), actionData.action))) {
return callback(false)
}
// Validate request and permissions
if (!(await this.isCharacterGM())) {
callback(false)
return
}
// Get and validate sprite
const sprite = await this.spriteRepository.getById(data.id)
if (!sprite) {
callback(false)
return
}
await this.spriteRepository.getEntityManager().populate(sprite, ['spriteActions'])
// Update sprite name
await sprite.setName(data.name).setUpdatedAt(new Date()).save()
// Process all sprite actions
const success = await this.processAllSpriteActions(data.spriteActions, sprite)
callback(success)
} catch (error) {
console.error(`Error updating sprite ${data.id}:`, error)
callback(false)
}
}
private async processAllSpriteActions(actionsData: SpriteActionData[], sprite: any): Promise<boolean> {
try {
// Remove existing actions
const existingActions = sprite.getSpriteActions()
// Remove existing actions only after confirming sprite sheets generated successfully
for (const existingAction of existingActions) {
await spriteRepository.getEntityManager().removeAndFlush(existingAction)
await this.spriteRepository.getEntityManager().removeAndFlush(existingAction)
}
// Create new actions
for (const actionData of data.spriteActions) {
// Process images and calculate dimensions
const imageData = await Promise.all(actionData.sprites.map((sprite) => this.processImage(sprite)))
const effectiveDimensions = imageData.map((dimensions) => this.calculateEffectiveDimensions(dimensions))
// First pass: find the global maximum dimensions across all actions
let globalMaxWidth = 0
let globalMaxHeight = 0
// Calculate total height needed for the sprite sheet
const maxHeight = Math.max(...effectiveDimensions.map((d) => d.height))
const maxTop = Math.max(...effectiveDimensions.map((d) => d.top))
const maxBottom = Math.max(...effectiveDimensions.map((d) => d.bottom))
const totalHeight = maxHeight + maxTop + maxBottom
// Extract all image metadata to find global maximums
for (const actionData of actionsData) {
if (actionData.sprites.length === 0) continue
const imagesData = await Promise.all(
actionData.sprites.map(async (sprite) => {
const base64Data = sprite.url.split(';base64,').pop()
if (!base64Data) throw new Error('Invalid base64 image')
const buffer = Buffer.from(base64Data, 'base64')
const metadata = await sharp(buffer).metadata()
return {
width: metadata.width || 0,
height: metadata.height || 0
}
})
)
// Update global maximums with this action's maximums
const actionMaxWidth = Math.max(...imagesData.map((data) => data.width), 0)
const actionMaxHeight = Math.max(...imagesData.map((data) => data.height), 0)
globalMaxWidth = Math.max(globalMaxWidth, actionMaxWidth)
globalMaxHeight = Math.max(globalMaxHeight, actionMaxHeight)
}
// Process each action using the global maximum dimensions
for (const actionData of actionsData) {
if (actionData.sprites.length === 0) continue
// Generate and save the sprite sheet using global dimensions
const frameDimensions = await this.generateAndSaveSpriteSheet(actionData.sprites, sprite.getId(), actionData.action, globalMaxWidth, globalMaxHeight)
if (!frameDimensions) return false
// Create and save sprite action
const spriteAction = new SpriteAction()
spriteAction.setSprite(sprite)
sprite.getSpriteActions().add(spriteAction)
@ -93,73 +120,85 @@ export default class SpriteUpdateEvent extends BaseEvent {
.setSprites(actionData.sprites)
.setOriginX(actionData.originX)
.setOriginY(actionData.originY)
.setFrameWidth(await this.calculateMaxWidth(actionData.sprites))
.setFrameHeight(totalHeight)
.setFrameWidth(frameDimensions.frameWidth)
.setFrameHeight(frameDimensions.frameHeight)
.setFrameRate(actionData.frameRate)
await spriteRepository.getEntityManager().persistAndFlush(spriteAction)
await this.spriteRepository.getEntityManager().persistAndFlush(spriteAction)
}
return callback(true)
return true
} catch (error) {
console.error(`Error updating sprite ${data.id}:`, error)
return callback(false)
console.error('Error processing sprite actions:', error)
return false
}
}
private async generateSpriteSheet(sprites: SpriteImage[], spriteId: string, action: string): Promise<boolean> {
private async generateAndSaveSpriteSheet(sprites: SpriteImage[], spriteId: string, action: string, maxWidth: number, maxHeight: number): Promise<{ frameWidth: number; frameHeight: number } | null> {
try {
if (!sprites.length) return true
if (sprites.length === 0) return { frameWidth: 0, frameHeight: 0 }
// Process all images and get their dimensions
const imageData = await Promise.all(sprites.map((sprite) => this.processImage(sprite)))
const effectiveDimensions = imageData.map((dimensions) => this.calculateEffectiveDimensions(dimensions))
// Extract image data
const imagesData = await Promise.all(
sprites.map(async (sprite) => {
const base64Data = sprite.url.split(';base64,').pop()
if (!base64Data) throw new Error('Invalid base64 image')
const buffer = Buffer.from(base64Data, 'base64')
const metadata = await sharp(buffer).metadata()
// Calculate maximum dimensions
const maxWidth = Math.max(...effectiveDimensions.map((d) => d.width))
const maxHeight = Math.max(...effectiveDimensions.map((d) => d.height))
const maxTop = Math.max(...effectiveDimensions.map((d) => d.top))
const maxBottom = Math.max(...effectiveDimensions.map((d) => d.bottom))
return {
buffer,
width: metadata.width || 0,
height: metadata.height || 0
}
})
)
// Calculate total height needed
const totalHeight = maxHeight + maxTop + maxBottom
// Skip creation if any image has invalid dimensions
if (imagesData.some((data) => data.width === 0 || data.height === 0)) {
console.error('One or more sprites have invalid dimensions')
return null
}
// Process images and create sprite sheet
const processedImages = await Promise.all(
sprites.map(async (sprite, index) => {
const { width, height, offsetX, offsetY } = await this.processImage(sprite)
const uri = sprite.url.split(';base64,').pop()
if (!uri) throw new Error('Invalid base64 image')
const buffer = Buffer.from(uri, 'base64')
// Create frames of uniform size with the original sprites centered
const uniformFrames = await Promise.all(
imagesData.map(async (imageData) => {
// Calculate centering offsets to position the sprite in the middle of the frame
const xOffset = Math.floor((maxWidth - imageData.width) / 2)
const yOffset = Math.floor((maxHeight - imageData.height) / 2)
// Create individual frame
const left = offsetX >= 0 ? offsetX : 0
const verticalOffset = totalHeight - height - (offsetY >= 0 ? offsetY : 0)
// Create a uniform-sized frame with the sprite centered
return sharp({
create: {
width: maxWidth,
height: totalHeight,
height: maxHeight,
channels: 4,
background: { r: 0, g: 0, b: 0, alpha: 0 }
}
})
.composite([{ input: buffer, left, top: verticalOffset }])
.composite([
{
input: imageData.buffer,
left: xOffset,
top: yOffset
}
])
.png()
.toBuffer()
})
)
// Combine frames into sprite sheet
// Create the sprite sheet with uniform frames
const spriteSheet = await sharp({
create: {
width: maxWidth * sprites.length,
height: totalHeight,
width: maxWidth * uniformFrames.length,
height: maxHeight,
channels: 4,
background: { r: 0, g: 0, b: 0, alpha: 0 }
}
})
.composite(
processedImages.map((buffer, index) => ({
uniformFrames.map((buffer, index) => ({
input: buffer,
left: index * maxWidth,
top: 0
@ -168,50 +207,19 @@ export default class SpriteUpdateEvent extends BaseEvent {
.png()
.toBuffer()
// Ensure directory exists
// Save sprite sheet
const dir = `public/sprites/${spriteId}`
await fs.promises.mkdir(dir, { recursive: true })
// Save the sprite sheet
await fs.promises.writeFile(`${dir}/${action}.png`, spriteSheet)
return true
// Return the uniform frame dimensions (now global maximum dimensions)
return {
frameWidth: maxWidth,
frameHeight: maxHeight
}
} catch (error) {
console.error('Error generating sprite sheet:', error)
return false
return null
}
}
private async processImage(sprite: SpriteImage): Promise<ImageDimensions> {
const uri = sprite.url.split(';base64,').pop()
if (!uri) throw new Error('Invalid base64 image')
const buffer = Buffer.from(uri, 'base64')
const metadata = await sharp(buffer).metadata()
return {
width: metadata.width ?? 0,
height: metadata.height ?? 0,
offsetX: sprite.offset?.x ?? 0,
offsetY: sprite.offset?.y ?? 0
}
}
private calculateEffectiveDimensions(imageDimensions: ImageDimensions): EffectiveDimensions {
return {
width: imageDimensions.width + Math.abs(imageDimensions.offsetX),
height: imageDimensions.height + Math.abs(imageDimensions.offsetY),
top: imageDimensions.offsetY >= 0 ? imageDimensions.offsetY : 0,
bottom: imageDimensions.offsetY < 0 ? Math.abs(imageDimensions.offsetY) : 0
}
}
private async calculateMaxWidth(sprites: SpriteImage[]): Promise<number> {
if (!sprites.length) return 0
// Process all images and get their dimensions
const imageData = await Promise.all(sprites.map((sprite) => this.processImage(sprite)))
const effectiveDimensions = imageData.map((dimensions) => this.calculateEffectiveDimensions(dimensions))
// Calculate maximum width needed
return Math.max(...effectiveDimensions.map((d) => d.width))
}
}

View File

@ -1,7 +1,7 @@
import { BaseEvent } from '@/application/base/baseEvent'
import { SocketEvent } from '@/application/enums'
import type { UUID } from '@/application/types'
import { Map } from '@/entities/map'
import { type MapEditorMapT } from '@/entities/map'
import MapRepository from '@/repositories/mapRepository'
interface IPayload {
@ -13,7 +13,7 @@ export default class MapRequestEvent extends BaseEvent {
this.socket.on(SocketEvent.GM_MAP_REQUEST, this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: Map | null) => void): Promise<void> {
private async handleEvent(data: IPayload, callback: (response: MapEditorMapT | null) => void): Promise<void> {
try {
if (!(await this.isCharacterGM())) return
@ -34,9 +34,7 @@ export default class MapRequestEvent extends BaseEvent {
await mapRepository.getEntityManager().populate(map, mapRepository.POPULATE_MAP_EDITOR as any)
// Remove map.mapEventTiles.teleport.toMap and add map.mapEventTiles.teleport.toMapId
return callback(map)
return callback(await map.mapEditorObject())
} catch (error: any) {
this.logger.error('gm:map:request error', error.message)
return callback(null)

View File

@ -1,7 +1,7 @@
import { BaseEvent } from '@/application/base/baseEvent'
import { MapEventTileType, SocketEvent } from '@/application/enums'
import type { UUID } from '@/application/types'
import { Map } from '@/entities/map'
import { type MapEditorMapT } from '@/entities/map'
import { MapEffect } from '@/entities/mapEffect'
import { MapEventTile } from '@/entities/mapEventTile'
import { MapEventTileTeleport } from '@/entities/mapEventTileTeleport'
@ -21,7 +21,7 @@ interface IPayload {
positionX: number
positionY: number
teleport?: {
toMapId: string
toMap: string
toPositionX: number
toPositionY: number
toRotation: number
@ -36,7 +36,7 @@ export default class MapUpdateEvent extends BaseEvent {
this.socket.on(SocketEvent.GM_MAP_UPDATE, this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: Map | null) => void): Promise<void> {
private async handleEvent(data: IPayload, callback: (response: MapEditorMapT | null) => void): Promise<void> {
try {
if (!(await this.isCharacterGM())) return
@ -80,17 +80,17 @@ export default class MapUpdateEvent extends BaseEvent {
map.getMapEffects().removeAll()
// Create and add new map event tiles
for (const tile of data.mapEventTiles) {
const mapEventTile = new MapEventTile().setType(tile.type).setPositionX(tile.positionX).setPositionY(tile.positionY).setMap(map)
if (tile.teleport) {
const toMap = await mapRepository.getById(tile.teleport.toMapId as UUID)
for (const eventTile of data.mapEventTiles) {
const mapEventTile = new MapEventTile().setMap(map).setType(eventTile.type).setPositionX(eventTile.positionX).setPositionY(eventTile.positionY)
if (eventTile.teleport) {
const toMap = await mapRepository.getById(eventTile.teleport.toMap as UUID)
if (!toMap) continue
const teleport = new MapEventTileTeleport()
.setMapEventTile(mapEventTile)
.setToMap(toMap)
.setToPositionX(tile.teleport.toPositionX)
.setToPositionY(tile.teleport.toPositionY)
.setToRotation(tile.teleport.toRotation)
.setToPositionX(eventTile.teleport.toPositionX)
.setToPositionY(eventTile.teleport.toPositionY)
.setToRotation(eventTile.teleport.toRotation)
mapEventTile.setTeleport(teleport)
}
@ -128,9 +128,9 @@ export default class MapUpdateEvent extends BaseEvent {
mapManager.unloadMap(data.mapId)
await mapManager.loadMap(map)
return callback(map)
return callback(await map.mapEditorObject())
} catch (error: any) {
this.emitError(`gm:map:update error: ${error instanceof Error ? error.message + error.stack : String(error)}`)
this.sendNotificationAndLog(`gm:map:update error: ${error instanceof Error ? error.message + error.stack : String(error)}`)
return callback(null)
}
}

View File

@ -19,6 +19,11 @@ export default class CharacterMove extends BaseEvent {
// Don't attack if the character is already moving
if (this.getMapCharacter()?.isMoving) return
const throttleKey = `attack_${this.socket.characterId}`
if (this.isThrottled(throttleKey, 1000)) {
return
}
// Start attack
await this.characterAttackService.attack(this.socket.characterId!)
} catch (error) {

View File

@ -3,29 +3,24 @@ import { SocketEvent } from '@/application/enums'
import type { MapEventTileWithTeleport } from '@/application/types'
import MapManager from '@/managers/mapManager'
import MapCharacter from '@/models/mapCharacter'
import MapEventTileRepository from '@/repositories/mapEventTileRepository'
import CharacterService from '@/services/characterMoveService'
import TeleportService from '@/services/characterTeleportService'
import CharacterMoveService from '@/services/characterMoveService'
export default class CharacterMove extends BaseEvent {
private readonly characterService = CharacterService
private readonly STEP_DELAY = 100
private readonly THROTTLE_DELAY = 230
private readonly MAX_REQUEST_DISTANCE = 30 // Maximum allowed distance for movement requests
private readonly THROTTLE_DELAY = 200
private readonly MAX_REQUEST_DISTANCE = 30
private movementTimeout: NodeJS.Timeout | null = null
private lastKnownPosition: { x: number; y: number } | null = null
private isProcessingMove = false
public listen(): void {
this.socket.on(SocketEvent.MAP_CHARACTER_MOVE, this.handleEvent.bind(this))
}
private async handleEvent({ positionX, positionY }: { positionX: number; positionY: number }): Promise<void> {
private async handleEvent([positionX, positionY]: [number, number]): Promise<void> {
try {
const mapCharacter = MapManager.getCharacterById(this.socket.characterId!)
if (!mapCharacter?.getCharacter()) {
this.logger.error('map:character:move error: Character not found or not initialized')
return
}
if (!mapCharacter?.getCharacter()) return
const character = mapCharacter.getCharacter()
const currentX = character.getPositionX()
@ -33,63 +28,56 @@ export default class CharacterMove extends BaseEvent {
// Enhanced throttling with position tracking
const throttleKey = `movement_${this.socket.characterId}`
if (this.isThrottled(throttleKey, this.THROTTLE_DELAY)) {
return
}
if (this.isThrottled(throttleKey, this.THROTTLE_DELAY)) return
// Validate current position against last known position
const movementValidation = this.characterService.validateMovementDistance(currentX, currentY, this.lastKnownPosition, this.STEP_DELAY, mapCharacter.isMoving)
// Stop any existing movement before starting a new one
await this.stopExistingMovement(mapCharacter)
// Validate movement
const movementValidation = CharacterMoveService.validateMovementDistance(currentX, currentY, this.lastKnownPosition, this.STEP_DELAY, mapCharacter.isMoving)
if (!movementValidation.isValid) {
this.logger.warn(`Suspicious movement detected: ${this.socket.characterId}`)
// Force position reset
character.setPositionX(this.lastKnownPosition!.x).setPositionY(this.lastKnownPosition!.y)
this.broadcastMovement(character, false)
CharacterMoveService.broadcastMovement(character, false)
return
}
// Validate requested position distance
const requestValidation = this.characterService.validateRequestDistance(currentX, currentY, positionX, positionY, this.MAX_REQUEST_DISTANCE)
if (!requestValidation.isValid) {
this.logger.warn(`Invalid movement distance detected: ${this.socket.characterId}`)
return
}
// If character is already moving to the same target position, ignore the request
if (mapCharacter.isMoving && mapCharacter.currentPath?.length) {
const lastPathPoint = mapCharacter.currentPath[mapCharacter.currentPath.length - 1]
if (lastPathPoint && Math.abs(lastPathPoint.positionX - positionX) < 1 && Math.abs(lastPathPoint.positionY - positionY) < 1) {
return
}
}
// Cancel any ongoing movement
this.cancelCurrentMovement(mapCharacter)
// Update last known position
this.lastKnownPosition = { x: currentX, y: currentY }
// Calculate path to target position
const path = await this.characterService.calculatePath(mapCharacter.character, Math.floor(positionX), Math.floor(positionY))
// Calculate and validate path
const path = await CharacterMoveService.calculatePath(character, Math.floor(positionX), Math.floor(positionY))
if (!path?.length) {
this.io.in(mapCharacter.character.map.id).emit(SocketEvent.MAP_CHARACTER_MOVEERROR, 'No valid path found')
mapCharacter.isMoving = false
mapCharacter.currentPath = null
CharacterMoveService.broadcastMovement(character, false)
return
}
// Start new movement
mapCharacter.isMoving = true
// Start movement
mapCharacter.currentPath = path
mapCharacter.isMoving = true
this.isProcessingMove = true
await this.moveAlongPath(mapCharacter)
} catch (error: any) {
this.logger.error('map:character:move error: ' + error.message)
} finally {
this.isProcessingMove = false
}
}
private cancelCurrentMovement(mapCharacter: MapCharacter): void {
if (!mapCharacter.isMoving) return
mapCharacter.isMoving = false
private async stopExistingMovement(mapCharacter: MapCharacter): Promise<void> {
// Clear existing movement timeout
if (this.movementTimeout) {
clearTimeout(this.movementTimeout)
this.movementTimeout = null
}
// Wait for any ongoing movement processing to complete
if (this.isProcessingMove) {
await new Promise((resolve) => setTimeout(resolve, this.STEP_DELAY))
}
// Only clear the path, keep isMoving state for continuous animation
mapCharacter.currentPath = null
}
@ -97,18 +85,18 @@ export default class CharacterMove extends BaseEvent {
const character = mapCharacter.getCharacter()
const path = mapCharacter.currentPath
if (!path?.length) return
if (!path?.length || !character) return
let lastMoveTime = Date.now()
let currentTile, nextTile
try {
for (let i = 0; i < path.length - 1; i++) {
// Check if this movement sequence is still valid
if (!mapCharacter.isMoving || mapCharacter.currentPath !== path) {
return
}
// Ensure minimum time between moves using a single Date.now() call
const timeSinceLastMove = Date.now() - lastMoveTime
if (timeSinceLastMove < this.STEP_DELAY) {
await new Promise((resolve) => setTimeout(resolve, this.STEP_DELAY - timeSinceLastMove))
@ -117,36 +105,41 @@ export default class CharacterMove extends BaseEvent {
currentTile = path[i]
nextTile = path[i + 1]
if (!currentTile || !nextTile || !this.isValidStep(currentTile, nextTile)) {
this.logger.error('Invalid movement step detected')
break
if (!currentTile || !nextTile || !CharacterMoveService.isValidStep(currentTile, nextTile)) {
return
}
// Update character rotation and position in a single operation
// Update character position and rotation
character
.setRotation(CharacterService.calculateRotation(currentTile.positionX, currentTile.positionY, nextTile.positionX, nextTile.positionY))
.setRotation(CharacterMoveService.calculateRotation(currentTile.positionX, currentTile.positionY, nextTile.positionX, nextTile.positionY))
.setPositionX(nextTile.positionX)
.setPositionY(nextTile.positionY)
// Check for map events at the next tile
const mapEventTile = await this.checkMapEvents(character, nextTile)
// Check for map events
const mapEventTile = await CharacterMoveService.checkMapEvents(character.getMap().getId(), nextTile)
if (mapEventTile) {
if (mapEventTile.type === 'BLOCK') break
if (mapEventTile.type === 'TELEPORT' && mapEventTile.teleport) {
await this.handleTeleportMapEventTile(mapEventTile as MapEventTileWithTeleport)
// Force clear movement state before teleport
mapCharacter.isMoving = false
mapCharacter.currentPath = null
this.lastKnownPosition = null // Reset last known position
await CharacterMoveService.handleTeleportMapEventTile(character.id, mapEventTile as MapEventTileWithTeleport)
return
}
}
// Broadcast movement
this.broadcastMovement(character, true)
// Only broadcast if this is still the current movement
if (mapCharacter.isMoving && mapCharacter.currentPath === path) {
CharacterMoveService.broadcastMovement(character, true)
}
// Apply movement delay between steps
if (i < path.length - 2) {
await new Promise((resolve) => setTimeout(resolve, this.STEP_DELAY))
}
// Update last known position and move time
this.lastKnownPosition = {
x: nextTile.positionX,
y: nextTile.positionY
@ -154,62 +147,31 @@ export default class CharacterMove extends BaseEvent {
lastMoveTime = Date.now()
}
} finally {
// Only finalize if this movement is still active
if (mapCharacter.isMoving && mapCharacter.currentPath === path) {
this.finalizeMovement(mapCharacter)
await this.finalizeMovement(mapCharacter)
}
}
}
private isValidStep(current: { positionX: number; positionY: number }, next: { positionX: number; positionY: number }): boolean {
return Math.abs(next.positionX - current.positionX) <= 1 && Math.abs(next.positionY - current.positionY) <= 1
}
private async checkMapEvents(character: any, nextTile: { positionX: number; positionY: number }) {
const mapEventTileRepository = new MapEventTileRepository()
return mapEventTileRepository.getEventTileByMapIdAndPosition(character.getMap().getId(), Math.floor(nextTile.positionX), Math.floor(nextTile.positionY))
}
private async handleTeleportMapEventTile(mapEventTile: MapEventTileWithTeleport): Promise<void> {
const teleport = mapEventTile.getTeleport()
if (teleport) {
await TeleportService.teleportCharacter(this.socket.characterId!, {
targetMapId: teleport.getToMap().getId(),
targetX: teleport.getToPositionX(),
targetY: teleport.getToPositionY(),
rotation: teleport.getToRotation()
})
}
}
private broadcastMovement(character: any, isMoving: boolean): void {
this.io.in(character.map.id).emit(SocketEvent.MAP_CHARACTER_MOVE, {
characterId: character.id,
positionX: character.getPositionX(),
positionY: character.getPositionY(),
rotation: character.getRotation(),
isMoving
})
}
private finalizeMovement(mapCharacter: MapCharacter): void {
// Clear any existing timeout
private async finalizeMovement(mapCharacter: MapCharacter): Promise<void> {
if (this.movementTimeout) {
clearTimeout(this.movementTimeout)
}
// Clear the current path immediately
mapCharacter.currentPath = null
// Set new timeout for movement state cleanup
this.movementTimeout = setTimeout(() => {
// Ensure the character is still in a valid state
if (!mapCharacter.isMoving || !mapCharacter.currentPath) {
mapCharacter.isMoving = false
// Save the final position and broadcast it
mapCharacter.savePosition().then(() => {
this.broadcastMovement(mapCharacter.character, false)
})
}
}, this.STEP_DELAY * 2) // Increased delay to ensure all movement processing is complete
const character = mapCharacter.getCharacter()
if (character) {
await mapCharacter.savePosition()
// Set a timeout to stop movement animation if no new movement is received
this.movementTimeout = setTimeout(() => {
if (!mapCharacter.currentPath) {
mapCharacter.isMoving = false
CharacterMoveService.broadcastMovement(character, false)
}
}, this.THROTTLE_DELAY) // Use THROTTLE_DELAY to match input timing
}
}
}

View File

@ -15,16 +15,20 @@ class MapManager {
await mapRepository.getEntityManager().populate(maps, mapRepository.POPULATE_ALL as never[])
await Promise.all(maps.map((map) => this.loadMap(map)))
this.logger.info(`Map manager loaded with ${Object.keys(this.maps).length} maps`)
}
public async loadMap(map: Map): Promise<void> {
this.maps[map.id] = new LoadedMap(map)
public async loadMap(map: Map): Promise<LoadedMap> {
const loadedMap = new LoadedMap(map)
this.maps[map.id] = loadedMap
this.logger.info(`Map ID ${map.id} loaded`)
return loadedMap
}
public unloadMap(mapId: UUID): void {
const map = this.maps[mapId]
if (!map) return
delete this.maps[mapId]
this.logger.info(`Map ID ${mapId} unloaded`)
}
@ -38,15 +42,11 @@ class MapManager {
}
public getCharacterById(characterId: UUID): MapCharacter | null {
return (
Object.values(this.maps)
.flatMap((map) => map.getCharactersInMap())
.find((char) => char.character.id === characterId) ?? null
)
}
public removeCharacter(characterId: UUID): void {
Object.values(this.maps).forEach((map) => map.removeCharacter(characterId))
for (const map of Object.values(this.maps)) {
const character = map.getCharacterById(characterId)
if (character) return character
}
return null
}
}

View File

@ -1,6 +1,7 @@
import fs from 'fs'
import { Server as HTTPServer } from 'http'
import { pathToFileURL } from 'url'
import { SocketEvent } from '@/application/enums'
import Logger, { LoggerType } from '@/application/logger'
import Storage from '@/application/storage'
import type { TSocket, UUID } from '@/application/types'
@ -21,7 +22,7 @@ class SocketManager {
this.io.use(Authentication)
// Set up connection handler
this.io.on('connection', this.handleConnection.bind(this))
this.io.on(SocketEvent.CONNECTION, this.handleConnection.bind(this))
}
/**

View File

@ -21,6 +21,7 @@
"primary": false,
"nullable": false,
"length": 255,
"default": "''",
"mappedType": "string"
},
"width": {
@ -51,7 +52,7 @@
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"nullable": false,
"length": null,
"mappedType": "json"
},
@ -233,6 +234,7 @@
"primary": false,
"nullable": false,
"length": null,
"default": "0",
"mappedType": "integer"
},
"position_y": {
@ -243,6 +245,7 @@
"primary": false,
"nullable": false,
"length": null,
"default": "0",
"mappedType": "integer"
},
"teleport_id": {
@ -314,7 +317,7 @@
"id"
],
"referencedTableName": "map_event_tile_teleport",
"deleteRule": "set null",
"deleteRule": "cascade",
"updateRule": "cascade"
}
},
@ -736,6 +739,26 @@
"length": 255,
"mappedType": "string"
},
"width": {
"name": "width",
"type": "int",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"length": null,
"mappedType": "integer"
},
"height": {
"name": "height",
"type": "int",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"length": null,
"mappedType": "integer"
},
"created_at": {
"name": "created_at",
"type": "datetime",
@ -860,7 +883,7 @@
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"nullable": false,
"length": 255,
"mappedType": "string"
},
@ -920,7 +943,6 @@
"id"
],
"referencedTableName": "sprite",
"deleteRule": "set null",
"updateRule": "cascade"
}
},
@ -1093,6 +1115,17 @@
"default": "'MALE'",
"mappedType": "string"
},
"color": {
"name": "color",
"type": "varchar(255)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 255,
"default": "'#000000'",
"mappedType": "string"
},
"is_selectable": {
"name": "is_selectable",
"type": "tinyint(1)",
@ -1110,7 +1143,7 @@
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"nullable": false,
"length": 255,
"mappedType": "string"
},
@ -1170,7 +1203,6 @@
"id"
],
"referencedTableName": "sprite",
"deleteRule": "set null",
"updateRule": "cascade"
}
},

View File

@ -1,14 +1,14 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20250215152716 extends Migration {
export class Migration20250221004940 extends Migration {
override async up(): Promise<void> {
this.addSql(`create table \`map\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`width\` int not null default 10, \`height\` int not null default 10, \`tiles\` json null, \`pvp\` tinyint(1) not null default false, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`create table \`map\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null default '', \`width\` int not null default 10, \`height\` int not null default 10, \`tiles\` json not null, \`pvp\` tinyint(1) not null default false, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`create table \`map_effect\` (\`id\` varchar(255) not null, \`map_id\` varchar(255) not null, \`effect\` varchar(255) not null, \`strength\` int not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`map_effect\` add index \`map_effect_map_id_index\`(\`map_id\`);`);
this.addSql(`create table \`map_event_tile\` (\`id\` varchar(255) not null, \`map_id\` varchar(255) not null, \`type\` enum('BLOCK', 'TELEPORT', 'NPC', 'ITEM') not null, \`position_x\` int not null, \`position_y\` int not null, \`teleport_id\` varchar(255) null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`create table \`map_event_tile\` (\`id\` varchar(255) not null, \`map_id\` varchar(255) not null, \`type\` enum('BLOCK', 'TELEPORT', 'NPC', 'ITEM') not null, \`position_x\` int not null default 0, \`position_y\` int not null default 0, \`teleport_id\` varchar(255) null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`map_event_tile\` add index \`map_event_tile_map_id_index\`(\`map_id\`);`);
this.addSql(`alter table \`map_event_tile\` add unique \`map_event_tile_teleport_id_unique\`(\`teleport_id\`);`);
@ -22,15 +22,15 @@ export class Migration20250215152716 extends Migration {
this.addSql(`alter table \`placed_map_object\` add index \`placed_map_object_map_id_index\`(\`map_id\`);`);
this.addSql(`alter table \`placed_map_object\` add index \`placed_map_object_map_object_id_index\`(\`map_object_id\`);`);
this.addSql(`create table \`sprite\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`create table \`sprite\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`width\` int null, \`height\` int null, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`create table \`item\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`description\` varchar(255) not null default '', \`item_type\` enum('WEAPON', 'HELMET', 'CHEST', 'LEGS', 'BOOTS', 'GLOVES', 'RING', 'NECKLACE') not null, \`stackable\` tinyint(1) not null default false, \`rarity\` enum('COMMON', 'UNCOMMON', 'RARE', 'EPIC', 'LEGENDARY') not null default 'COMMON', \`sprite_id\` varchar(255) null, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`create table \`item\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`description\` varchar(255) not null default '', \`item_type\` enum('WEAPON', 'HELMET', 'CHEST', 'LEGS', 'BOOTS', 'GLOVES', 'RING', 'NECKLACE') not null, \`stackable\` tinyint(1) not null default false, \`rarity\` enum('COMMON', 'UNCOMMON', 'RARE', 'EPIC', 'LEGENDARY') not null default 'COMMON', \`sprite_id\` varchar(255) not null, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`item\` add index \`item_sprite_id_index\`(\`sprite_id\`);`);
this.addSql(`create table \`character_type\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`gender\` enum('MALE', 'FEMALE') not null, \`race\` enum('HUMAN', 'ELF', 'DWARF', 'ORC', 'GOBLIN') not null, \`is_selectable\` tinyint(1) not null default false, \`sprite_id\` varchar(255) null, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`character_type\` add index \`character_type_sprite_id_index\`(\`sprite_id\`);`);
this.addSql(`create table \`character_hair\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`gender\` varchar(255) not null default 'MALE', \`is_selectable\` tinyint(1) not null default false, \`sprite_id\` varchar(255) null, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`create table \`character_hair\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`gender\` varchar(255) not null default 'MALE', \`color\` varchar(255) not null default '#000000', \`is_selectable\` tinyint(1) not null default false, \`sprite_id\` varchar(255) not null, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`character_hair\` add index \`character_hair_sprite_id_index\`(\`sprite_id\`);`);
this.addSql(`create table \`sprite_action\` (\`id\` varchar(255) not null, \`sprite_id\` varchar(255) not null, \`action\` varchar(255) not null, \`sprites\` json null, \`origin_x\` numeric(5,2) not null default 0, \`origin_y\` numeric(5,2) not null default 0, \`frame_width\` int not null default 0, \`frame_height\` int not null default 0, \`frame_rate\` int not null default 0, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
@ -70,7 +70,7 @@ export class Migration20250215152716 extends Migration {
this.addSql(`alter table \`map_effect\` add constraint \`map_effect_map_id_foreign\` foreign key (\`map_id\`) references \`map\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`map_event_tile\` add constraint \`map_event_tile_map_id_foreign\` foreign key (\`map_id\`) references \`map\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`map_event_tile\` add constraint \`map_event_tile_teleport_id_foreign\` foreign key (\`teleport_id\`) references \`map_event_tile_teleport\` (\`id\`) on update cascade on delete set null;`);
this.addSql(`alter table \`map_event_tile\` add constraint \`map_event_tile_teleport_id_foreign\` foreign key (\`teleport_id\`) references \`map_event_tile_teleport\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`map_event_tile_teleport\` add constraint \`map_event_tile_teleport_map_event_tile_id_foreign\` foreign key (\`map_event_tile_id\`) references \`map_event_tile\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`map_event_tile_teleport\` add constraint \`map_event_tile_teleport_to_map_id_foreign\` foreign key (\`to_map_id\`) references \`map\` (\`id\`) on update cascade on delete cascade;`);
@ -78,11 +78,11 @@ export class Migration20250215152716 extends Migration {
this.addSql(`alter table \`placed_map_object\` add constraint \`placed_map_object_map_id_foreign\` foreign key (\`map_id\`) references \`map\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`placed_map_object\` add constraint \`placed_map_object_map_object_id_foreign\` foreign key (\`map_object_id\`) references \`map_object\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`item\` add constraint \`item_sprite_id_foreign\` foreign key (\`sprite_id\`) references \`sprite\` (\`id\`) on update cascade on delete set null;`);
this.addSql(`alter table \`item\` add constraint \`item_sprite_id_foreign\` foreign key (\`sprite_id\`) references \`sprite\` (\`id\`) on update cascade;`);
this.addSql(`alter table \`character_type\` add constraint \`character_type_sprite_id_foreign\` foreign key (\`sprite_id\`) references \`sprite\` (\`id\`) on update cascade on delete set null;`);
this.addSql(`alter table \`character_hair\` add constraint \`character_hair_sprite_id_foreign\` foreign key (\`sprite_id\`) references \`sprite\` (\`id\`) on update cascade on delete set null;`);
this.addSql(`alter table \`character_hair\` add constraint \`character_hair_sprite_id_foreign\` foreign key (\`sprite_id\`) references \`sprite\` (\`id\`) on update cascade;`);
this.addSql(`alter table \`sprite_action\` add constraint \`sprite_action_sprite_id_foreign\` foreign key (\`sprite_id\`) references \`sprite\` (\`id\`) on update cascade on delete cascade;`);

View File

@ -1,13 +1,13 @@
import serverConfig from '@/application/config'
import { Migrator } from '@mikro-orm/migrations'
import { defineConfig, MariaDbDriver } from '@mikro-orm/mariadb'
// import { defineConfig, MySqlDriver } from '@mikro-orm/mysql'
import { Migrator } from '@mikro-orm/migrations'
import { TsMorphMetadataProvider } from '@mikro-orm/reflection'
export default defineConfig({
extensions: [Migrator],
metadataProvider: TsMorphMetadataProvider,
entities: ['./dist/entities/*.js'],
entities: ['./src/entities/*.ts'],
entitiesTs: ['./src/entities/*.ts'],
driver: MariaDbDriver,
host: serverConfig.DB_HOST,

View File

@ -16,17 +16,23 @@ class LoadedMap {
return this.map
}
public addCharacter(character: Character) {
public addCharacter(character: Character): MapCharacter {
const existingCharacter = this.getCharacterById(character.id)
if (existingCharacter) {
return existingCharacter
}
const mapCharacter = new MapCharacter(character)
this.characters.push(mapCharacter)
return mapCharacter
}
public async removeCharacter(id: UUID) {
public async removeCharacter(id: UUID): Promise<void> {
const mapCharacter = this.getCharacterById(id)
if (mapCharacter) {
await mapCharacter.savePosition()
this.characters = this.characters.filter((c) => c.character.id !== id)
}
if (!mapCharacter) return
await mapCharacter.savePosition()
this.characters = this.characters.filter((c) => c.character.id !== id)
}
public getCharacterById(id: UUID): MapCharacter | undefined {
@ -38,12 +44,11 @@ class LoadedMap {
}
public async getGrid(): Promise<number[][]> {
let grid: number[][] = Array.from({ length: this.map.height }, () => Array.from({ length: this.map.width }, () => 0))
const grid: number[][] = Array.from({ length: this.map.height }, () => Array.from({ length: this.map.width }, () => 0))
const mapEventTileRepository = new MapEventTileRepository()
const eventTiles = await mapEventTileRepository.getAll(this.map.id)
// Set the grid values based on the event tiles, these are strings
eventTiles.forEach((eventTile) => {
if (eventTile.type === 'BLOCK') {
grid[eventTile.positionY]![eventTile.positionX] = 1

View File

@ -38,14 +38,16 @@ class MapCharacter {
await this.savePosition()
// Leave map and remove from manager
if (this.character.map) {
socket.leave(this.character.map.id)
MapManager.removeCharacter(this.character.id)
socket.leave(this.character.map.id)
// Notify map players
io.in(this.character.map.id).emit(SocketEvent.MAP_CHARACTER_LEAVE, this.character.id)
const map = MapManager.getMapById(this.character.map.id)
if (map) {
await map.removeCharacter(this.character.id)
}
// Notify map players
io.in(this.character.map.id).emit(SocketEvent.MAP_CHARACTER_LEAVE, this.character.id)
// Notify all players
io.emit(SocketEvent.CHARACTER_DISCONNECT, this.character.id)
} catch (error) {

View File

@ -1,75 +1,17 @@
import { BaseService } from '@/application/base/baseService'
import config from '@/application/config'
import { SocketEvent } from '@/application/enums'
import type { MapEventTileWithTeleport, UUID } from '@/application/types'
import { Character } from '@/entities/character'
import MapManager from '@/managers/mapManager'
import SocketManager from '@/managers/socketManager'
import MapEventTileRepository from '@/repositories/mapEventTileRepository'
import TeleportService from '@/services/characterTeleportService'
import type { Server } from 'socket.io'
type Position = { positionX: number; positionY: number }
export type Node = Position & { parent?: Node; g: number; h: number; f: number }
class PriorityQueue<T> {
private items: T[] = []
constructor(private compare: (a: T, b: T) => number) {}
enqueue(item: T): void {
this.items.push(item)
this.siftUp(this.items.length - 1)
}
dequeue(): T | undefined {
if (this.items.length === 0) return undefined
const result = this.items[0]
const last = this.items.pop()
if (this.items.length > 0 && last !== undefined) {
this.items[0] = last
this.siftDown(0)
}
return result
}
get length(): number {
return this.items.length
}
private siftUp(index: number): void {
while (index > 0) {
const parentIndex = Math.floor((index - 1) / 2)
if (this.compare(this.items[index]!, this.items[parentIndex]!) < 0) {
;[this.items[index], this.items[parentIndex]] = [this.items[parentIndex]!, this.items[index]!]
index = parentIndex
} else {
break
}
}
}
private siftDown(index: number): void {
const length = this.items.length
while (true) {
let minIndex = index
const leftChild = 2 * index + 1
const rightChild = 2 * index + 2
if (leftChild < length && this.compare(this.items[leftChild]!, this.items[minIndex]!) < 0) {
minIndex = leftChild
}
if (rightChild < length && this.compare(this.items[rightChild]!, this.items[minIndex]!) < 0) {
minIndex = rightChild
}
if (minIndex === index) break
;[this.items[index], this.items[minIndex]] = [this.items[minIndex]!, this.items[index]!]
index = minIndex
}
}
clear(): void {
this.items = []
}
}
class CharacterMoveService extends BaseService {
// Rotation lookup table for better performance
private readonly ROTATION_MAP = {
@ -174,12 +116,14 @@ class CharacterMoveService extends BaseService {
private findPath(start: Position, end: Position, grid: number[][]): Node[] {
const openList = new PriorityQueue<Node>((a, b) => a.f - b.f)
const closedSet = new Set<string>()
const MAX_ITERATIONS = 1000 // Prevent infinite loops for impossible paths
const MAX_ITERATIONS = 1000
let iterations = 0
const getKey = (p: Position) => `${p.positionX},${p.positionY}`
const gScoreMap = new Map<string, number>()
openList.enqueue({ ...start, g: 0, h: 0, f: 0 })
openList.enqueue({ ...start, g: 0, h: this.getDistance(start, end), f: this.getDistance(start, end) })
gScoreMap.set(getKey(start), 0)
try {
while (openList.length > 0 && iterations < MAX_ITERATIONS) {
@ -191,28 +135,34 @@ class CharacterMoveService extends BaseService {
return this.reconstructPath(current)
}
closedSet.add(getKey(current))
const currentKey = getKey(current)
closedSet.add(currentKey)
const neighbors = this.getValidNeighbors(current, grid, end)
for (const neighbor of neighbors) {
if (closedSet.has(getKey(neighbor))) continue
const neighborKey = getKey(neighbor)
if (closedSet.has(neighborKey)) continue
const g = current.g + this.getDistance(current, neighbor)
const h = this.getDistance(neighbor, end)
const f = g + h
const tentativeGScore = current.g + this.getDistance(current, neighbor)
const node: Node = { ...neighbor, g, h, f, parent: current }
openList.enqueue(node)
if (!gScoreMap.has(neighborKey) || tentativeGScore < gScoreMap.get(neighborKey)!) {
gScoreMap.set(neighborKey, tentativeGScore)
const h = this.getDistance(neighbor, end)
const f = tentativeGScore + h
const node: Node = { ...neighbor, g: tentativeGScore, h, f, parent: current }
openList.enqueue(node)
}
}
}
this.logger.warn(`Path not found after ${iterations} iterations`)
return [] // No path found
return []
} finally {
// Clean up resources
while (openList.length > 0) openList.dequeue()
openList.clear()
closedSet.clear()
gScoreMap.clear()
}
}
@ -226,13 +176,32 @@ class CharacterMoveService extends BaseService {
}
private isValidPosition(pos: Position, grid: number[][], end: Position): boolean {
return (
pos.positionX >= 0 &&
pos.positionY >= 0 &&
pos.positionX < grid[0]!.length &&
pos.positionY < grid.length &&
(grid[pos.positionY]![pos.positionX] === 0 || (pos.positionX === end.positionX && pos.positionY === end.positionY))
)
if (pos.positionX < 0 || pos.positionY < 0 || pos.positionX >= grid[0]!.length || pos.positionY >= grid.length) {
return false
}
return grid[pos.positionY]![pos.positionX] === 0 || (pos.positionX === end.positionX && pos.positionY === end.positionY)
}
isValidStep(current: { positionX: number; positionY: number }, next: { positionX: number; positionY: number }): boolean {
return Math.abs(next.positionX - current.positionX) <= 1 && Math.abs(next.positionY - current.positionY) <= 1
}
async checkMapEvents(map_id: UUID, nextTile: { positionX: number; positionY: number }) {
const mapEventTileRepository = new MapEventTileRepository()
return mapEventTileRepository.getEventTileByMapIdAndPosition(map_id, nextTile.positionX, nextTile.positionY)
}
async handleTeleportMapEventTile(character_id: UUID, mapEventTile: MapEventTileWithTeleport): Promise<void> {
const teleport = mapEventTile.getTeleport()
if (teleport) {
await TeleportService.teleportCharacter(character_id, {
targetMapId: teleport.getToMap().getId(),
targetX: teleport.getToPositionX(),
targetY: teleport.getToPositionY(),
rotation: teleport.getToRotation()
})
}
}
private getDistance(a: Position, b: Position): number {
@ -252,6 +221,11 @@ class CharacterMoveService extends BaseService {
return path
}
public broadcastMovement(character: Character, isMoving: boolean = false): void {
const io: Server = SocketManager.getIO()
io.in(character.map.id).emit(SocketEvent.MAP_CHARACTER_MOVE, [character.id, character.getPositionX(), character.getPositionY(), character.getRotation(), isMoving])
}
public validateMovementDistance(
currentX: number,
currentY: number,
@ -263,7 +237,7 @@ class CharacterMoveService extends BaseService {
return { isValid: true, maxAllowedDistance: 0, actualDistance: 0 }
}
const maxAllowedDistance = Math.ceil((stepDelay / 1000) * 2)
const maxAllowedDistance = Math.ceil((stepDelay / 1000) * (config.ALLOW_DIAGONAL_MOVEMENT ? 2.5 : 2))
const actualDistance = Math.sqrt(Math.pow(currentX - lastKnownPosition.x, 2) + Math.pow(currentY - lastKnownPosition.y, 2))
return {
@ -272,18 +246,70 @@ class CharacterMoveService extends BaseService {
actualDistance
}
}
public validateRequestDistance(currentX: number, currentY: number, targetX: number, targetY: number, maxRequestDistance: number): { isValid: boolean; distance: number } {
const requestDistance = Math.sqrt(Math.pow(targetX - currentX, 2) + Math.pow(targetY - currentY, 2))
return {
isValid: requestDistance <= maxRequestDistance,
distance: requestDistance
}
}
public isValidStep(current: { positionX: number; positionY: number }, next: { positionX: number; positionY: number }): boolean {
return Math.abs(next.positionX - current.positionX) <= 1 && Math.abs(next.positionY - current.positionY) <= 1
}
}
export default new CharacterMoveService()
class PriorityQueue<T> {
private items: T[] = []
constructor(private compare: (a: T, b: T) => number) {}
enqueue(item: T): void {
this.items.push(item)
this.siftUp(this.items.length - 1)
}
dequeue(): T | undefined {
if (this.items.length === 0) return undefined
const result = this.items[0]
const last = this.items.pop()
if (this.items.length > 0 && last !== undefined) {
this.items[0] = last
this.siftDown(0)
}
return result
}
get length(): number {
return this.items.length
}
private siftUp(index: number): void {
while (index > 0) {
const parentIndex = Math.floor((index - 1) / 2)
if (this.compare(this.items[index]!, this.items[parentIndex]!) < 0) {
;[this.items[index], this.items[parentIndex]] = [this.items[parentIndex]!, this.items[index]!]
index = parentIndex
} else {
break
}
}
}
private siftDown(index: number): void {
const length = this.items.length
while (true) {
let minIndex = index
const leftChild = 2 * index + 1
const rightChild = 2 * index + 2
if (leftChild < length && this.compare(this.items[leftChild]!, this.items[minIndex]!) < 0) {
minIndex = leftChild
}
if (rightChild < length && this.compare(this.items[rightChild]!, this.items[minIndex]!) < 0) {
minIndex = rightChild
}
if (minIndex === index) break
;[this.items[index], this.items[minIndex]] = [this.items[minIndex]!, this.items[index]!]
index = minIndex
}
}
clear(): void {
this.items = []
}
}

View File

@ -6,6 +6,7 @@ import MapManager from '@/managers/mapManager'
import SocketManager from '@/managers/socketManager'
import MapCharacter from '@/models/mapCharacter'
import MapRepository from '@/repositories/mapRepository'
import CharacterMoveService from '@/services/characterMoveService'
interface TeleportOptions {
targetMapId: UUID
@ -20,77 +21,25 @@ class CharacterTeleportService {
private readonly logger = Logger.type(LoggerType.GAME)
public async teleportCharacter(characterId: UUID, options: TeleportOptions): Promise<boolean> {
const mapRepository = new MapRepository()
const socket = SocketManager.getSocketByCharacterId(characterId)
const targetMap = MapManager.getMapById(options.targetMapId)
if (!socket || !targetMap) {
this.logger.error(`Teleport failed - Missing socket or target map for character ${characterId}`)
return false
}
if (options.isInitialJoin && !options.character) {
this.logger.error('Initial join requires character data')
return false
}
const existingCharacter = !options.isInitialJoin && MapManager.getCharacterById(characterId)
const mapCharacter = options.isInitialJoin
? new MapCharacter(options.character)
: existingCharacter ||
(() => {
this.logger.error(`Teleport failed - Character ${characterId} not found in MapManager`)
return null
})()
if (!mapCharacter) return false
try {
const currentMapId = mapCharacter.character.map?.id
const { socket, targetMap, mapCharacter } = await this.validateTeleportRequest(characterId, options)
if (!socket || !targetMap || !mapCharacter) return false
const currentMapId = mapCharacter.character.map.id
const currentMap = MapManager.getMapById(currentMapId)
const io = SocketManager.getIO()
// Update character position and map
await mapCharacter
.getCharacter()
.setPositionX(options.targetX)
.setPositionY(options.targetY)
.setRotation(options.rotation ?? 0)
.setMap(targetMap.getMap())
.save()
await this.updateCharacterPosition(mapCharacter, options, targetMap)
// If the current map is the target map and we are not joining, send move event
// Handle same map teleport
if (currentMapId === options.targetMapId && !options.isInitialJoin) {
// If the current map is the target map, send move event
io.in(currentMapId).emit(SocketEvent.MAP_CHARACTER_MOVE, {
characterId: mapCharacter.character.id,
positionX: options.targetX,
positionY: options.targetY,
rotation: options.rotation ?? 0,
isMoving: false
})
CharacterMoveService.broadcastMovement(mapCharacter.character, false)
return true
}
// Handle current map cleanup
if (currentMapId) {
socket.leave(currentMapId)
MapManager.removeCharacter(characterId)
io.in(currentMapId).emit(SocketEvent.MAP_CHARACTER_LEAVE, characterId)
}
// Join new map
socket.join(options.targetMapId)
targetMap.addCharacter(mapCharacter.getCharacter())
const map = await mapRepository.getById(options.targetMapId)
await mapRepository.getEntityManager().populate(map, mapRepository.POPULATE_TELEPORT as any)
// Notify clients
io.in(options.targetMapId).emit(SocketEvent.MAP_CHARACTER_JOIN, mapCharacter)
socket.emit(SocketEvent.MAP_CHARACTER_TELEPORT, {
mapId: options.targetMapId,
characters: targetMap.getCharactersInMap()
})
// Handle map transition
await this.handleMapTransition(socket, io, currentMapId, currentMap, options.targetMapId, targetMap, characterId, mapCharacter)
return true
} catch (error) {
@ -98,6 +47,64 @@ class CharacterTeleportService {
return false
}
}
private async validateTeleportRequest(characterId: UUID, options: TeleportOptions) {
const socket = SocketManager.getSocketByCharacterId(characterId)
const targetMap = MapManager.getMapById(options.targetMapId)
if (!socket || !targetMap) {
this.logger.error(`Teleport failed - Missing socket or target map for character ${characterId}`)
return { socket: null, targetMap: null, mapCharacter: null }
}
if (options.isInitialJoin && !options.character) {
this.logger.error('Initial join requires character data')
return { socket, targetMap, mapCharacter: null }
}
const existingCharacter = !options.isInitialJoin && MapManager.getCharacterById(characterId)
const mapCharacter = options.isInitialJoin ? new MapCharacter(options.character!) : existingCharacter || null
if (!mapCharacter) {
this.logger.error(`Teleport failed - Character ${characterId} not found in MapManager`)
}
return { socket, targetMap, mapCharacter }
}
private async updateCharacterPosition(mapCharacter: MapCharacter, options: TeleportOptions, targetMap: any) {
await mapCharacter
.getCharacter()
.setPositionX(options.targetX)
.setPositionY(options.targetY)
.setRotation(options.rotation ?? 0)
.setMap(targetMap.getMap())
.save()
}
private async handleMapTransition(socket: any, io: any, currentMapId: UUID | undefined, currentMap: any, targetMapId: UUID, targetMap: any, characterId: UUID, mapCharacter: MapCharacter) {
// Clean up current map
if (currentMapId && currentMap) {
socket.leave(currentMapId)
await currentMap.removeCharacter(characterId)
io.in(currentMapId).emit(SocketEvent.MAP_CHARACTER_LEAVE, characterId)
}
// Join new map
socket.join(targetMapId)
targetMap.addCharacter(mapCharacter.getCharacter())
const mapRepository = new MapRepository()
const map = await mapRepository.getById(targetMapId)
await mapRepository.getEntityManager().populate(map!, mapRepository.POPULATE_TELEPORT as any)
// Notify clients
io.in(targetMapId).emit(SocketEvent.MAP_CHARACTER_JOIN, mapCharacter)
socket.emit(SocketEvent.MAP_CHARACTER_TELEPORT, {
mapId: targetMapId,
characters: targetMap.getCharactersInMap()
})
}
}
export default new CharacterTeleportService()