Compare commits
83 Commits
feature/#2
...
feature/im
Author | SHA1 | Date | |
---|---|---|---|
bd8caaf27c | |||
3cf86e322c | |||
6f32fbdc79 | |||
743d4594df | |||
2be49c010f | |||
2ac9416fe6 | |||
43fe6ab33e | |||
1cbf116ad4 | |||
3f10b03d24 | |||
a525d80530 | |||
4748044ab3 | |||
3b0138130b | |||
54c75896f9 | |||
9467797dc9 | |||
a8934f8e40 | |||
65cae5d824 | |||
179ccdbc55 | |||
ff39628f0c | |||
d4680b198e | |||
550b961505 | |||
1839bd9a22 | |||
1017013032 | |||
d5c7cd0294 | |||
4a62bbb118 | |||
40c7f6289a | |||
72d731c6f2 | |||
fc92d9ea79 | |||
2e267a36aa | |||
6ee8bb8334 | |||
ec3bf0f51e | |||
27f8bc8784 | |||
3185c478a6 | |||
72ef04d683 | |||
446e8fa617 | |||
f7072acdd2 | |||
fda8cc532e | |||
821e742527 | |||
460308d555 | |||
3f8f8745eb | |||
bf7f585270 | |||
fee4277b4f | |||
ffc07b7403 | |||
344ddbaf39 | |||
86ed3ae4b0 | |||
719c75616e | |||
cf954979c5 | |||
01ed1bce29 | |||
d4e0cbe398 | |||
628b3bf1fa | |||
709d34d59b | |||
c4a42066ab | |||
26dbaa45a7 | |||
ae0241fecb | |||
3a566dae5a | |||
ad4f33676f | |||
44cfbd6ee8 | |||
881e3375ab | |||
6a76c4797a | |||
bf75ad001b | |||
3b473e5826 | |||
929a36554a | |||
3fbc5f4e87 | |||
1526e0947a | |||
7ec4303b40 | |||
f475b69022 | |||
27d8c7cff6 | |||
b9a7f9aa8e | |||
5b6b968541 | |||
93abf4b631 | |||
0de574b9e1 | |||
c04c52aed0 | |||
3f19730bd8 | |||
d0e3c95bb0 | |||
82f51b2b7e | |||
1b9db64854 | |||
41c71d5964 | |||
bd04dc2ab8 | |||
a4e96f9ede | |||
f6bac403a2 | |||
8460d0b535 | |||
5a36d10f0e | |||
8f8f019ab7 | |||
6a1823586a |
@ -4,6 +4,7 @@ PORT=4000
|
||||
DATABASE_URL="mysql://root@localhost:3306/nq"
|
||||
REDIS_URL="redis://@127.0.0.1:6379/4"
|
||||
JWT_SECRET="secret"
|
||||
CLIENT_URL="http://localhost:5173"
|
||||
|
||||
# Game configuration
|
||||
ALLOW_DIAGONAL_MOVEMENT=false
|
||||
@ -12,3 +13,9 @@ ALLOW_DIAGONAL_MOVEMENT=false
|
||||
DEFAULT_CHARACTER_ZONE="0"
|
||||
DEFAULT_CHARACTER_POS_X="0"
|
||||
DEFAULT_CHARACTER_POS_Y="0"
|
||||
|
||||
# SMTP configuration
|
||||
SMTP_HOST="my.directonline.io"
|
||||
SMTP_PORT="587"
|
||||
SMTP_USER="no-reply@sylvan.quest"
|
||||
SMTP_PASSWORD=""
|
439
package-lock.json
generated
439
package-lock.json
generated
@ -14,11 +14,11 @@
|
||||
"express": "^4.19.2",
|
||||
"ioredis": "^5.4.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"nodemailer": "^6.9.15",
|
||||
"pino": "^9.3.2",
|
||||
"prisma": "^5.17.0",
|
||||
"sharp": "^0.33.4",
|
||||
"socket.io": "^4.7.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.5.3",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
@ -27,14 +27,17 @@
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/node": "^20.14.11",
|
||||
"@types/nodemailer": "^6.4.16",
|
||||
"nodemon": "^3.1.4",
|
||||
"prettier": "^3.3.3"
|
||||
"prettier": "^3.3.3",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "0.3.9"
|
||||
@ -424,6 +427,7 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
@ -433,12 +437,14 @@
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
@ -524,9 +530,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@prisma/client": {
|
||||
"version": "5.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.21.1.tgz",
|
||||
"integrity": "sha512-3n+GgbAZYjaS/k0M03yQsQfR1APbr411r74foknnsGpmhNKBG49VuUkxIU6jORgvJPChoD4WC4PqoHImN1FP0w==",
|
||||
"version": "5.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz",
|
||||
"integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
@ -542,48 +548,48 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/debug": {
|
||||
"version": "5.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.21.1.tgz",
|
||||
"integrity": "sha512-uY8SAhcnORhvgtOrNdvWS98Aq/nkQ9QDUxrWAgW8XrCZaI3j2X7zb7Xe6GQSh6xSesKffFbFlkw0c2luHQviZA==",
|
||||
"version": "5.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz",
|
||||
"integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/engines": {
|
||||
"version": "5.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.21.1.tgz",
|
||||
"integrity": "sha512-hGVTldUkIkTwoV8//hmnAAiAchi4oMEKD3aW5H2RrnI50tTdwza7VQbTTAyN3OIHWlK5DVg6xV7X8N/9dtOydA==",
|
||||
"version": "5.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz",
|
||||
"integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "5.21.1",
|
||||
"@prisma/engines-version": "5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36",
|
||||
"@prisma/fetch-engine": "5.21.1",
|
||||
"@prisma/get-platform": "5.21.1"
|
||||
"@prisma/debug": "5.22.0",
|
||||
"@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
|
||||
"@prisma/fetch-engine": "5.22.0",
|
||||
"@prisma/get-platform": "5.22.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/engines-version": {
|
||||
"version": "5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36.tgz",
|
||||
"integrity": "sha512-qvnEflL0//lh44S/T9NcvTMxfyowNeUxTunPcDfKPjyJNrCNf2F1zQLcUv5UHAruECpX+zz21CzsC7V2xAeM7Q==",
|
||||
"version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz",
|
||||
"integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/fetch-engine": {
|
||||
"version": "5.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.21.1.tgz",
|
||||
"integrity": "sha512-70S31vgpCGcp9J+mh/wHtLCkVezLUqe/fGWk3J3JWZIN7prdYSlr1C0niaWUyNK2VflLXYi8kMjAmSxUVq6WGQ==",
|
||||
"version": "5.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz",
|
||||
"integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "5.21.1",
|
||||
"@prisma/engines-version": "5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36",
|
||||
"@prisma/get-platform": "5.21.1"
|
||||
"@prisma/debug": "5.22.0",
|
||||
"@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
|
||||
"@prisma/get-platform": "5.22.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/get-platform": {
|
||||
"version": "5.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.21.1.tgz",
|
||||
"integrity": "sha512-sRxjL3Igst3ct+e8ya/x//cDXmpLbZQ5vfps2N4tWl4VGKQAmym77C/IG/psSMsQKszc8uFC/q1dgmKFLUgXZQ==",
|
||||
"version": "5.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz",
|
||||
"integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "5.21.1"
|
||||
"@prisma/debug": "5.22.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
@ -596,24 +602,28 @@
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
|
||||
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node12": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node14": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node16": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/bcryptjs": {
|
||||
@ -719,18 +729,28 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.17.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.1.tgz",
|
||||
"integrity": "sha512-j2VlPv1NnwPJbaCNv69FO/1z4lId0QmGvpT41YxitRtWlg96g/j8qcv2RKsLKe2F6OJgyXhupN1Xo17b2m139Q==",
|
||||
"version": "20.17.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.10.tgz",
|
||||
"integrity": "sha512-/jrvh5h6NXhEauFFexRin69nA0uHJ5gwk4iDivp/DeoEua3uwCUto6PC86IpRITBOs4+6i2I56K5x5b6WYGXHA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/nodemailer": {
|
||||
"version": "6.4.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz",
|
||||
"integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.9.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz",
|
||||
"integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==",
|
||||
"version": "6.9.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz",
|
||||
"integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@ -778,9 +798,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.13.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz",
|
||||
"integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==",
|
||||
"version": "8.14.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
||||
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
@ -793,6 +814,7 @@
|
||||
"version": "8.3.4",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
|
||||
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.11.0"
|
||||
@ -819,6 +841,7 @@
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
@ -926,14 +949,14 @@
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/bullmq": {
|
||||
"version": "5.21.2",
|
||||
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.21.2.tgz",
|
||||
"integrity": "sha512-LPuNoGaDc5CON2X6h4cJ2iVfd+B+02xubFU+IB/fyJHd+/HqUZRqnlYryUCAuhVHBhUKtA6oyVdJxqSa62i+og==",
|
||||
"version": "5.34.3",
|
||||
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.34.3.tgz",
|
||||
"integrity": "sha512-S8/V11w7p6jYAGvv+00skLza/4inTOupWPe0uCD8mZSUiYKzvmW4/YEB+KVjZI2CC2oD3KJ3t7/KkUd31MxMig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cron-parser": "^4.6.0",
|
||||
"ioredis": "^5.4.1",
|
||||
"msgpackr": "^1.10.1",
|
||||
"msgpackr": "^1.11.2",
|
||||
"node-abort-controller": "^3.1.1",
|
||||
"semver": "^7.5.4",
|
||||
"tslib": "^2.0.0",
|
||||
@ -949,17 +972,27 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
|
||||
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
|
||||
"integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.0",
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-intrinsic": "^1.2.4",
|
||||
"set-function-length": "^1.2.1"
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"get-intrinsic": "^1.2.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@ -1103,6 +1136,7 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cron-parser": {
|
||||
@ -1126,23 +1160,6 @@
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/define-data-property": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.0",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/denque": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||
@ -1184,15 +1201,16 @@
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.4.5",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
|
||||
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
|
||||
"version": "16.4.7",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
||||
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@ -1201,6 +1219,20 @@
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ecdsa-sig-formatter": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||
@ -1288,13 +1320,10 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
|
||||
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-intrinsic": "^1.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
@ -1308,6 +1337,18 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
|
||||
"integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
@ -1324,9 +1365,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.21.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
|
||||
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
@ -1348,7 +1389,7 @@
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.10",
|
||||
"path-to-regexp": "0.1.12",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "6.13.0",
|
||||
"range-parser": "~1.2.1",
|
||||
@ -1363,6 +1404,10 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-redact": {
|
||||
@ -1447,16 +1492,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
|
||||
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz",
|
||||
"integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"dunder-proto": "^1.0.0",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.0.0",
|
||||
"function-bind": "^1.1.2",
|
||||
"has-proto": "^1.0.1",
|
||||
"has-symbols": "^1.0.3",
|
||||
"hasown": "^2.0.0"
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@ -1479,12 +1529,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
|
||||
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-intrinsic": "^1.1.3"
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@ -1500,34 +1550,10 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/has-property-descriptors": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
||||
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-proto": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
|
||||
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@ -1590,9 +1616,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ioredis": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.1.tgz",
|
||||
"integrity": "sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==",
|
||||
"version": "5.4.2",
|
||||
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.2.tgz",
|
||||
"integrity": "sha512-0SZXGNGZ+WzISQ67QDyZ2x0+wVxjjUndtD8oSeik/4ajifeiRufed8fCb8QW8VMyi4MXcS+UO1k/0NGhvq1PAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ioredis/commands": "^1.1.1",
|
||||
@ -1614,9 +1640,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ioredis/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
@ -1813,8 +1839,18 @@
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
@ -1895,9 +1931,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/msgpackr": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.0.tgz",
|
||||
"integrity": "sha512-I8qXuuALqJe5laEBYoFykChhSXLikZmUhccjGsPuSJ/7uPip2TJ7lwdIQwWSAi0jGZDXv4WOP8Qg65QZRuXxXw==",
|
||||
"version": "1.11.2",
|
||||
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.2.tgz",
|
||||
"integrity": "sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g==",
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"msgpackr-extract": "^3.0.2"
|
||||
@ -1955,10 +1991,19 @@
|
||||
"node-gyp-build-optional-packages-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "6.9.16",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz",
|
||||
"integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz",
|
||||
"integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==",
|
||||
"version": "3.1.9",
|
||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz",
|
||||
"integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -1985,9 +2030,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -2029,9 +2074,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
|
||||
"integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
|
||||
"version": "1.13.3",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
|
||||
"integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@ -2071,9 +2116,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.10",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
|
||||
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
@ -2127,9 +2172,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
|
||||
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
|
||||
"integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@ -2143,13 +2188,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prisma": {
|
||||
"version": "5.21.1",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.21.1.tgz",
|
||||
"integrity": "sha512-PB+Iqzld/uQBPaaw2UVIk84kb0ITsLajzsxzsadxxl54eaU5Gyl2/L02ysivHxK89t7YrfQJm+Ggk37uvM70oQ==",
|
||||
"version": "5.22.0",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz",
|
||||
"integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/engines": "5.21.1"
|
||||
"@prisma/engines": "5.22.0"
|
||||
},
|
||||
"bin": {
|
||||
"prisma": "build/index.js"
|
||||
@ -2376,23 +2421,6 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"define-data-property": "^1.1.4",
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-intrinsic": "^1.2.4",
|
||||
"gopd": "^1.0.1",
|
||||
"has-property-descriptors": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
@ -2439,15 +2467,69 @@
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
|
||||
"integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.7",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.4",
|
||||
"object-inspect": "^1.13.1"
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-list": "^1.0.0",
|
||||
"side-channel-map": "^1.0.1",
|
||||
"side-channel-weakmap": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-list": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-map": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-weakmap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-map": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@ -2679,6 +2761,7 @@
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
@ -2719,9 +2802,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz",
|
||||
"integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==",
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
@ -2738,9 +2821,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.6.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
|
||||
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
|
||||
"integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@ -2798,6 +2881,7 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vary": {
|
||||
@ -2834,15 +2918,16 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.23.8",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
|
||||
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
|
||||
"version": "3.24.1",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
|
||||
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
|
@ -15,11 +15,11 @@
|
||||
"express": "^4.19.2",
|
||||
"ioredis": "^5.4.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"nodemailer": "^6.9.15",
|
||||
"pino": "^9.3.2",
|
||||
"prisma": "^5.17.0",
|
||||
"sharp": "^0.33.4",
|
||||
"socket.io": "^4.7.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.5.3",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
@ -28,6 +28,8 @@
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/node": "^20.14.11",
|
||||
"@types/nodemailer": "^6.4.16",
|
||||
"ts-node": "^10.9.2",
|
||||
"nodemon": "^3.1.4",
|
||||
"prettier": "^3.3.3"
|
||||
}
|
||||
|
@ -1,10 +1,21 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE `World` (
|
||||
`date` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`isRainEnabled` BOOLEAN NOT NULL DEFAULT false,
|
||||
`rainPercentage` INTEGER NOT NULL DEFAULT 0,
|
||||
`isFogEnabled` BOOLEAN NOT NULL DEFAULT false,
|
||||
`fogDensity` INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
UNIQUE INDEX `World_date_key`(`date`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `Chat` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`characterId` INTEGER NOT NULL,
|
||||
`zoneId` INTEGER NOT NULL,
|
||||
`message` VARCHAR(191) NOT NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
@ -36,14 +47,41 @@ CREATE TABLE `SpriteAction` (
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `Item` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`name` VARCHAR(191) NOT NULL,
|
||||
`description` VARCHAR(191) NULL,
|
||||
`itemType` ENUM('WEAPON', 'HELMET', 'CHEST', 'LEGS', 'BOOTS', 'GLOVES', 'RING', 'NECKLACE') NOT NULL,
|
||||
`stackable` BOOLEAN NOT NULL DEFAULT false,
|
||||
`rarity` ENUM('COMMON', 'UNCOMMON', 'RARE', 'EPIC', 'LEGENDARY') NOT NULL DEFAULT 'COMMON',
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `User` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`username` VARCHAR(191) NOT NULL,
|
||||
`email` VARCHAR(191) NOT NULL,
|
||||
`password` VARCHAR(191) NOT NULL,
|
||||
`online` BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
UNIQUE INDEX `User_username_key`(`username`),
|
||||
UNIQUE INDEX `User_email_key`(`email`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `PasswordResetToken` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`userId` INTEGER NOT NULL,
|
||||
`token` VARCHAR(191) NOT NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
|
||||
UNIQUE INDEX `PasswordResetToken_token_key`(`token`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
@ -53,6 +91,7 @@ CREATE TABLE `CharacterType` (
|
||||
`name` VARCHAR(191) NOT NULL,
|
||||
`gender` ENUM('MALE', 'FEMALE') NOT NULL,
|
||||
`race` ENUM('HUMAN', 'ELF', 'DWARF', 'ORC', 'GOBLIN') NOT NULL,
|
||||
`isEnabledForCharCreation` BOOLEAN NOT NULL DEFAULT false,
|
||||
`spriteId` VARCHAR(191) NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
@ -60,23 +99,39 @@ CREATE TABLE `CharacterType` (
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `CharacterHair` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`name` VARCHAR(191) NOT NULL,
|
||||
`gender` ENUM('MALE', 'FEMALE') NOT NULL DEFAULT 'MALE',
|
||||
`isEnabledForCharCreation` BOOLEAN NOT NULL DEFAULT false,
|
||||
`spriteId` VARCHAR(191) NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `Character` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`userId` INTEGER NOT NULL,
|
||||
`name` VARCHAR(191) NOT NULL,
|
||||
`online` BOOLEAN NOT NULL DEFAULT false,
|
||||
`role` VARCHAR(191) NOT NULL DEFAULT 'player',
|
||||
`zoneId` INTEGER NOT NULL DEFAULT 1,
|
||||
`positionX` INTEGER NOT NULL DEFAULT 0,
|
||||
`positionY` INTEGER NOT NULL DEFAULT 0,
|
||||
`rotation` INTEGER NOT NULL DEFAULT 0,
|
||||
`characterTypeId` INTEGER NULL,
|
||||
`characterHairId` INTEGER NULL,
|
||||
`alignment` INTEGER NOT NULL DEFAULT 50,
|
||||
`hitpoints` INTEGER NOT NULL DEFAULT 100,
|
||||
`mana` INTEGER NOT NULL DEFAULT 100,
|
||||
`level` INTEGER NOT NULL DEFAULT 1,
|
||||
`experience` INTEGER NOT NULL DEFAULT 0,
|
||||
`alignment` INTEGER NOT NULL DEFAULT 50,
|
||||
`role` VARCHAR(191) NOT NULL DEFAULT 'player',
|
||||
`positionX` INTEGER NOT NULL DEFAULT 0,
|
||||
`positionY` INTEGER NOT NULL DEFAULT 0,
|
||||
`rotation` INTEGER NOT NULL DEFAULT 0,
|
||||
`zoneId` INTEGER NOT NULL DEFAULT 1,
|
||||
`characterTypeId` INTEGER NULL,
|
||||
`strength` INTEGER NOT NULL DEFAULT 10,
|
||||
`dexterity` INTEGER NOT NULL DEFAULT 10,
|
||||
`intelligence` INTEGER NOT NULL DEFAULT 10,
|
||||
`wisdom` INTEGER NOT NULL DEFAULT 10,
|
||||
|
||||
UNIQUE INDEX `Character_name_key`(`name`),
|
||||
PRIMARY KEY (`id`)
|
||||
@ -92,6 +147,17 @@ CREATE TABLE `CharacterItem` (
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `CharacterEquipment` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`characterId` INTEGER NOT NULL,
|
||||
`itemId` VARCHAR(191) NOT NULL,
|
||||
`quantity` INTEGER NOT NULL,
|
||||
`slot` ENUM('HEAD', 'BODY', 'ARMS', 'LEGS', 'NECK', 'RING') NOT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `Tile` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
@ -120,18 +186,6 @@ CREATE TABLE `Object` (
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `Item` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`name` VARCHAR(191) NOT NULL,
|
||||
`description` VARCHAR(191) NULL,
|
||||
`stackable` BOOLEAN NOT NULL DEFAULT false,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `Zone` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
@ -202,9 +256,15 @@ ALTER TABLE `Chat` ADD CONSTRAINT `Chat_zoneId_fkey` FOREIGN KEY (`zoneId`) REFE
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `SpriteAction` ADD CONSTRAINT `SpriteAction_spriteId_fkey` FOREIGN KEY (`spriteId`) REFERENCES `Sprite`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `PasswordResetToken` ADD CONSTRAINT `PasswordResetToken_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `CharacterType` ADD CONSTRAINT `CharacterType_spriteId_fkey` FOREIGN KEY (`spriteId`) REFERENCES `Sprite`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `CharacterHair` ADD CONSTRAINT `CharacterHair_spriteId_fkey` FOREIGN KEY (`spriteId`) REFERENCES `Sprite`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `Character` ADD CONSTRAINT `Character_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
@ -214,12 +274,21 @@ ALTER TABLE `Character` ADD CONSTRAINT `Character_zoneId_fkey` FOREIGN KEY (`zon
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `Character` ADD CONSTRAINT `Character_characterTypeId_fkey` FOREIGN KEY (`characterTypeId`) REFERENCES `CharacterType`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `Character` ADD CONSTRAINT `Character_characterHairId_fkey` FOREIGN KEY (`characterHairId`) REFERENCES `CharacterHair`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `CharacterItem` ADD CONSTRAINT `CharacterItem_characterId_fkey` FOREIGN KEY (`characterId`) REFERENCES `Character`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `CharacterItem` ADD CONSTRAINT `CharacterItem_itemId_fkey` FOREIGN KEY (`itemId`) REFERENCES `Item`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `CharacterEquipment` ADD CONSTRAINT `CharacterEquipment_characterId_fkey` FOREIGN KEY (`characterId`) REFERENCES `Character`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `CharacterEquipment` ADD CONSTRAINT `CharacterEquipment_itemId_fkey` FOREIGN KEY (`itemId`) REFERENCES `Item`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `ZoneEffect` ADD CONSTRAINT `ZoneEffect_zoneId_fkey` FOREIGN KEY (`zoneId`) REFERENCES `Zone`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
74
prisma/schema/game.prisma
Normal file
74
prisma/schema/game.prisma
Normal file
@ -0,0 +1,74 @@
|
||||
model World {
|
||||
date DateTime @unique @default(now())
|
||||
isRainEnabled Boolean @default(false)
|
||||
rainPercentage Int @default(0)
|
||||
isFogEnabled Boolean @default(false)
|
||||
fogDensity Int @default(0)
|
||||
}
|
||||
|
||||
model Chat {
|
||||
id Int @id @default(autoincrement())
|
||||
characterId Int
|
||||
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
|
||||
zoneId Int
|
||||
zone Zone @relation(fields: [zoneId], references: [id], onDelete: Cascade)
|
||||
message String
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model Sprite {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
spriteActions SpriteAction[]
|
||||
characterTypes CharacterType[]
|
||||
characterHairs CharacterHair[]
|
||||
}
|
||||
|
||||
model SpriteAction {
|
||||
id String @id @default(uuid())
|
||||
spriteId String
|
||||
sprite Sprite @relation(fields: [spriteId], references: [id], onDelete: Cascade)
|
||||
action String
|
||||
sprites Json?
|
||||
originX Decimal @default(0)
|
||||
originY Decimal @default(0)
|
||||
isAnimated Boolean @default(false)
|
||||
isLooping Boolean @default(false)
|
||||
frameWidth Int @default(0)
|
||||
frameHeight Int @default(0)
|
||||
frameSpeed Int @default(0)
|
||||
}
|
||||
|
||||
model Item {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
description String?
|
||||
itemType ItemType
|
||||
stackable Boolean @default(false)
|
||||
rarity ItemRarity @default(COMMON)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
characters CharacterItem[]
|
||||
equipment CharacterEquipment[]
|
||||
}
|
||||
|
||||
enum ItemType {
|
||||
WEAPON
|
||||
HELMET
|
||||
CHEST
|
||||
LEGS
|
||||
BOOTS
|
||||
GLOVES
|
||||
RING
|
||||
NECKLACE
|
||||
}
|
||||
|
||||
enum ItemRarity {
|
||||
COMMON
|
||||
UNCOMMON
|
||||
RARE
|
||||
EPIC
|
||||
LEGENDARY
|
||||
}
|
@ -11,7 +11,7 @@
|
||||
// npx prisma migrate deploy
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
provider = "prisma-client-js"
|
||||
previewFeatures = ["prismaSchemaFolder"]
|
||||
}
|
||||
|
||||
@ -19,13 +19,3 @@ datasource db {
|
||||
provider = "mysql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Chat {
|
||||
id Int @id @default(autoincrement())
|
||||
characterId Int
|
||||
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
|
||||
zoneId Int
|
||||
zone Zone @relation(fields: [zoneId], references: [id], onDelete: Cascade)
|
||||
message String
|
||||
createdAt DateTime
|
||||
}
|
||||
|
@ -1,23 +0,0 @@
|
||||
model Sprite {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
spriteActions SpriteAction[]
|
||||
characterTypes CharacterType[]
|
||||
}
|
||||
|
||||
model SpriteAction {
|
||||
id String @id @default(uuid())
|
||||
spriteId String
|
||||
sprite Sprite @relation(fields: [spriteId], references: [id], onDelete: Cascade)
|
||||
action String
|
||||
sprites Json?
|
||||
originX Decimal @default(0)
|
||||
originY Decimal @default(0)
|
||||
isAnimated Boolean @default(false)
|
||||
isLooping Boolean @default(false)
|
||||
frameWidth Int @default(0)
|
||||
frameHeight Int @default(0)
|
||||
frameSpeed Int @default(0)
|
||||
}
|
@ -1,11 +1,3 @@
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
password String
|
||||
online Boolean @default(false)
|
||||
characters Character[]
|
||||
}
|
||||
|
||||
enum CharacterGender {
|
||||
MALE
|
||||
FEMALE
|
||||
@ -19,39 +11,83 @@ enum CharacterRace {
|
||||
GOBLIN
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
email String @unique
|
||||
password String
|
||||
online Boolean @default(false)
|
||||
characters Character[]
|
||||
passwordResetTokens PasswordResetToken[]
|
||||
}
|
||||
|
||||
model PasswordResetToken {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
token String @unique
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model CharacterType {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
gender CharacterGender
|
||||
race CharacterRace
|
||||
characters Character[]
|
||||
spriteId String?
|
||||
sprite Sprite? @relation(fields: [spriteId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
gender CharacterGender
|
||||
race CharacterRace
|
||||
isEnabledForCharCreation Boolean @default(false)
|
||||
characters Character[]
|
||||
spriteId String?
|
||||
sprite Sprite? @relation(fields: [spriteId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model CharacterHair {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
gender CharacterGender @default(MALE)
|
||||
isEnabledForCharCreation Boolean @default(false)
|
||||
sprite Sprite? @relation(fields: [spriteId], references: [id], onDelete: Cascade)
|
||||
spriteId String?
|
||||
characters Character[]
|
||||
}
|
||||
|
||||
model Character {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
name String @unique
|
||||
online Boolean @default(false)
|
||||
hitpoints Int @default(100)
|
||||
mana Int @default(100)
|
||||
level Int @default(1)
|
||||
experience Int @default(0)
|
||||
alignment Int @default(50)
|
||||
role String @default("player")
|
||||
positionX Int @default(0)
|
||||
positionY Int @default(0)
|
||||
rotation Int @default(0)
|
||||
zoneId Int @default(1)
|
||||
zone Zone @relation(fields: [zoneId], references: [id], onDelete: Cascade)
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
name String @unique
|
||||
online Boolean @default(false)
|
||||
role String @default("player")
|
||||
chats Chat[]
|
||||
|
||||
// Position
|
||||
zoneId Int @default(1)
|
||||
zone Zone @relation(fields: [zoneId], references: [id], onDelete: Cascade)
|
||||
positionX Int @default(0)
|
||||
positionY Int @default(0)
|
||||
rotation Int @default(0)
|
||||
|
||||
// Customization
|
||||
characterTypeId Int?
|
||||
characterType CharacterType? @relation(fields: [characterTypeId], references: [id], onDelete: Cascade)
|
||||
chats Chat[]
|
||||
items CharacterItem[]
|
||||
characterType CharacterType? @relation(fields: [characterTypeId], references: [id], onDelete: Cascade)
|
||||
characterHairId Int?
|
||||
characterHair CharacterHair? @relation(fields: [characterHairId], references: [id], onDelete: Cascade)
|
||||
|
||||
// Inventory
|
||||
items CharacterItem[]
|
||||
equipment CharacterEquipment[]
|
||||
|
||||
// Stats
|
||||
alignment Int @default(50)
|
||||
hitpoints Int @default(100)
|
||||
mana Int @default(100)
|
||||
level Int @default(1)
|
||||
experience Int @default(0)
|
||||
strength Int @default(10)
|
||||
dexterity Int @default(10)
|
||||
intelligence Int @default(10)
|
||||
wisdom Int @default(10)
|
||||
}
|
||||
|
||||
model CharacterItem {
|
||||
@ -62,3 +98,22 @@ model CharacterItem {
|
||||
item Item @relation(fields: [itemId], references: [id], onDelete: Cascade)
|
||||
quantity Int
|
||||
}
|
||||
|
||||
model CharacterEquipment {
|
||||
id Int @id @default(autoincrement())
|
||||
characterId Int
|
||||
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
|
||||
itemId String
|
||||
item Item @relation(fields: [itemId], references: [id], onDelete: Cascade)
|
||||
quantity Int
|
||||
slot CharacterEquipmentSlotType
|
||||
}
|
||||
|
||||
enum CharacterEquipmentSlotType {
|
||||
HEAD
|
||||
BODY
|
||||
ARMS
|
||||
LEGS
|
||||
NECK
|
||||
RING
|
||||
}
|
||||
|
@ -21,16 +21,6 @@ model Object {
|
||||
ZoneObject ZoneObject[]
|
||||
}
|
||||
|
||||
model Item {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
description String?
|
||||
stackable Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
characters CharacterItem[]
|
||||
}
|
||||
|
||||
model Zone {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
|
@ -1,42 +0,0 @@
|
||||
import { ExtendedCharacter, TSocket } from '../utilities/types'
|
||||
import { Zone } from '@prisma/client'
|
||||
import prisma from '../utilities/prisma'
|
||||
|
||||
class CharacterManager {
|
||||
private characters!: ExtendedCharacter[]
|
||||
|
||||
public async boot() {
|
||||
this.characters = []
|
||||
}
|
||||
|
||||
public initCharacter(character: ExtendedCharacter) {
|
||||
this.characters = [...this.characters, character]
|
||||
}
|
||||
|
||||
public async removeCharacter(character: ExtendedCharacter) {
|
||||
await prisma.character.update({
|
||||
where: { id: character.id },
|
||||
data: {
|
||||
positionX: character.positionX,
|
||||
positionY: character.positionY,
|
||||
rotation: character.rotation,
|
||||
zoneId: character.zoneId
|
||||
}
|
||||
})
|
||||
this.characters = this.characters.filter((x) => x.id !== character.id)
|
||||
}
|
||||
|
||||
public getCharacterFromSocket(socket: TSocket) {
|
||||
return this.characters.find((x) => x.id === socket?.characterId)
|
||||
}
|
||||
|
||||
public hasResetMovement(character: ExtendedCharacter) {
|
||||
return this.characters.find((x) => x.id === character.id)?.resetMovement
|
||||
}
|
||||
|
||||
public getCharactersInZone(zone: Zone) {
|
||||
return this.characters.filter((x) => x.zoneId === zone.id)
|
||||
}
|
||||
}
|
||||
|
||||
export default new CharacterManager()
|
@ -1,7 +1,7 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { appLogger } from '../utilities/logger'
|
||||
import { getRootPath } from '../utilities/storage'
|
||||
import { readJsonValue, setJsonValue } from '../utilities/json'
|
||||
import worldService from '../services/worldService'
|
||||
import worldRepository from '../repositories/worldRepository'
|
||||
|
||||
class DateManager {
|
||||
private static readonly GAME_SPEED = 8 // 24 game hours / 3 real hours
|
||||
@ -18,17 +18,37 @@ class DateManager {
|
||||
appLogger.info('Date manager loaded')
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId)
|
||||
this.intervalId = null
|
||||
public async setTime(time: string): Promise<void> {
|
||||
try {
|
||||
let newDate: Date
|
||||
|
||||
// Check if it's just a time (HH:mm or HH:mm:ss format)
|
||||
if (/^\d{1,2}:\d{2}(:\d{2})?$/.test(time)) {
|
||||
const [hours, minutes] = time.split(':').map(Number)
|
||||
newDate = new Date(this.currentDate) // Clone current date
|
||||
newDate.setHours(hours, minutes)
|
||||
} else {
|
||||
// Treat as full datetime string
|
||||
newDate = new Date(time)
|
||||
if (isNaN(newDate.getTime())) return
|
||||
}
|
||||
|
||||
this.currentDate = newDate
|
||||
this.emitDate()
|
||||
await this.saveDate()
|
||||
} catch (error) {
|
||||
appLogger.error(`Failed to set time: ${error instanceof Error ? error.message : String(error)}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private async loadDate(): Promise<void> {
|
||||
try {
|
||||
const dateString = await readJsonValue<string>(this.getWorldFilePath(), 'date')
|
||||
this.currentDate = new Date(dateString)
|
||||
const world = await worldRepository.getFirst()
|
||||
|
||||
if (world) {
|
||||
this.currentDate = world.date
|
||||
}
|
||||
} catch (error) {
|
||||
appLogger.error(`Failed to load date: ${error instanceof Error ? error.message : String(error)}`)
|
||||
this.currentDate = new Date() // Use current date as fallback
|
||||
@ -39,7 +59,7 @@ class DateManager {
|
||||
this.intervalId = setInterval(() => {
|
||||
this.advanceGameTime()
|
||||
this.emitDate()
|
||||
this.saveDate()
|
||||
void this.saveDate()
|
||||
}, DateManager.UPDATE_INTERVAL)
|
||||
}
|
||||
|
||||
@ -54,14 +74,22 @@ class DateManager {
|
||||
|
||||
private async saveDate(): Promise<void> {
|
||||
try {
|
||||
await setJsonValue(this.getWorldFilePath(), 'date', this.currentDate)
|
||||
await worldService.update({
|
||||
date: this.currentDate
|
||||
})
|
||||
} catch (error) {
|
||||
appLogger.error(`Failed to save date: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
}
|
||||
|
||||
private getWorldFilePath(): string {
|
||||
return getRootPath('data', 'world.json')
|
||||
public cleanup(): void {
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId)
|
||||
}
|
||||
}
|
||||
|
||||
public getCurrentDate(): Date {
|
||||
return this.currentDate
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,127 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { appLogger } from '../utilities/logger'
|
||||
import worldService from '../services/worldService'
|
||||
import worldRepository from '../repositories/worldRepository'
|
||||
|
||||
interface WeatherState {
|
||||
isRainEnabled: boolean
|
||||
rainPercentage: number
|
||||
isFogEnabled: boolean
|
||||
fogDensity: number
|
||||
}
|
||||
|
||||
class WeatherManager {
|
||||
private static readonly UPDATE_INTERVAL = 60000 // Check weather every minute
|
||||
private static readonly RAIN_CHANCE = 0.2 // 20% chance of rain
|
||||
private static readonly FOG_CHANCE = 0.15 // 15% chance of fog
|
||||
|
||||
private io: Server | null = null
|
||||
private intervalId: NodeJS.Timeout | null = null
|
||||
private weatherState: WeatherState = {
|
||||
isRainEnabled: false,
|
||||
rainPercentage: 0,
|
||||
isFogEnabled: false,
|
||||
fogDensity: 0
|
||||
}
|
||||
|
||||
public async boot(io: Server): Promise<void> {
|
||||
this.io = io
|
||||
await this.loadWeather()
|
||||
this.startWeatherLoop()
|
||||
appLogger.info('Weather manager loaded')
|
||||
}
|
||||
|
||||
public async toggleRain(): Promise<void> {
|
||||
this.weatherState.isRainEnabled = !this.weatherState.isRainEnabled
|
||||
this.weatherState.rainPercentage = this.weatherState.isRainEnabled
|
||||
? Math.floor(Math.random() * 50) + 50 // 50-100%
|
||||
: 0
|
||||
|
||||
await this.saveWeather()
|
||||
this.emitWeather()
|
||||
}
|
||||
|
||||
public async toggleFog(): Promise<void> {
|
||||
this.weatherState.isFogEnabled = !this.weatherState.isFogEnabled
|
||||
this.weatherState.fogDensity = this.weatherState.isFogEnabled
|
||||
? Math.floor((Math.random() * 0.7 + 0.3) * 100) // Convert 0.3-1.0 to 30-100
|
||||
: 0
|
||||
|
||||
await this.saveWeather()
|
||||
this.emitWeather()
|
||||
}
|
||||
|
||||
private async loadWeather(): Promise<void> {
|
||||
try {
|
||||
const world = await worldRepository.getFirst()
|
||||
|
||||
if (world) {
|
||||
this.weatherState = {
|
||||
isRainEnabled: world.isRainEnabled,
|
||||
rainPercentage: world.rainPercentage,
|
||||
isFogEnabled: world.isFogEnabled,
|
||||
fogDensity: world.fogDensity
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
appLogger.error(`Failed to load weather: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
}
|
||||
|
||||
public getWeatherState(): WeatherState {
|
||||
return this.weatherState
|
||||
}
|
||||
|
||||
private startWeatherLoop(): void {
|
||||
this.intervalId = setInterval(async () => {
|
||||
this.updateWeather()
|
||||
this.emitWeather()
|
||||
await this.saveWeather().catch((error) => {
|
||||
appLogger.error(`Failed to save weather: ${error instanceof Error ? error.message : String(error)}`)
|
||||
})
|
||||
}, WeatherManager.UPDATE_INTERVAL)
|
||||
}
|
||||
|
||||
private updateWeather(): void {
|
||||
// Update rain
|
||||
if (Math.random() < WeatherManager.RAIN_CHANCE) {
|
||||
this.weatherState.isRainEnabled = !this.weatherState.isRainEnabled
|
||||
this.weatherState.rainPercentage = this.weatherState.isRainEnabled
|
||||
? Math.floor(Math.random() * 50) + 50 // 50-100%
|
||||
: 0
|
||||
}
|
||||
|
||||
// Update fog
|
||||
if (Math.random() < WeatherManager.FOG_CHANCE) {
|
||||
this.weatherState.isFogEnabled = !this.weatherState.isFogEnabled
|
||||
this.weatherState.fogDensity = this.weatherState.isFogEnabled
|
||||
? Math.floor((Math.random() * 0.7 + 0.3) * 100) // Convert 0.3-1.0 to 30-100
|
||||
: 0
|
||||
}
|
||||
}
|
||||
|
||||
private emitWeather(): void {
|
||||
this.io?.emit('weather', this.weatherState)
|
||||
}
|
||||
|
||||
private async saveWeather(): Promise<void> {
|
||||
try {
|
||||
await worldService.update({
|
||||
isRainEnabled: this.weatherState.isRainEnabled,
|
||||
rainPercentage: this.weatherState.rainPercentage,
|
||||
isFogEnabled: this.weatherState.isFogEnabled,
|
||||
fogDensity: this.weatherState.fogDensity
|
||||
})
|
||||
} catch (error) {
|
||||
appLogger.error(`Failed to save weather: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
}
|
||||
|
||||
public cleanup(): void {
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new WeatherManager()
|
||||
|
@ -3,53 +3,53 @@ import ZoneRepository from '../repositories/zoneRepository'
|
||||
import ZoneService from '../services/zoneService'
|
||||
import LoadedZone from '../models/loadedZone'
|
||||
import { gameLogger } from '../utilities/logger'
|
||||
import ZoneCharacter from '../models/zoneCharacter'
|
||||
|
||||
class ZoneManager {
|
||||
private loadedZones: LoadedZone[] = []
|
||||
private readonly zones = new Map<number, LoadedZone>()
|
||||
|
||||
// Method to initialize zoneEditor manager
|
||||
public async boot() {
|
||||
public async boot(): Promise<void> {
|
||||
// Create first zone if it doesn't exist
|
||||
if (!(await ZoneRepository.getById(1))) {
|
||||
const zoneService = new ZoneService()
|
||||
await zoneService.createDemoZone()
|
||||
await new ZoneService().createDemoZone()
|
||||
}
|
||||
|
||||
const zones = await ZoneRepository.getAll()
|
||||
await Promise.all(zones.map((zone) => this.loadZone(zone)))
|
||||
|
||||
for (const zone of zones) {
|
||||
await this.loadZone(zone)
|
||||
}
|
||||
|
||||
gameLogger.info('Zone manager loaded')
|
||||
gameLogger.info(`Zone manager loaded with ${this.zones.size} zones`)
|
||||
}
|
||||
|
||||
// Method to handle individual zoneEditor loading
|
||||
public async loadZone(zone: Zone) {
|
||||
public async loadZone(zone: Zone): Promise<void> {
|
||||
const loadedZone = new LoadedZone(zone)
|
||||
this.loadedZones.push(loadedZone)
|
||||
this.zones.set(zone.id, loadedZone)
|
||||
gameLogger.info(`Zone ID ${zone.id} loaded`)
|
||||
}
|
||||
|
||||
// Method to handle individual zoneEditor unloading
|
||||
public unloadZone(zoneId: number) {
|
||||
this.loadedZones = this.loadedZones.filter((loadedZone) => loadedZone.getZone().id !== zoneId)
|
||||
public unloadZone(zoneId: number): void {
|
||||
this.zones.delete(zoneId)
|
||||
gameLogger.info(`Zone ID ${zoneId} unloaded`)
|
||||
}
|
||||
|
||||
// Getter for loaded zones
|
||||
public getLoadedZones(): LoadedZone[] {
|
||||
return this.loadedZones
|
||||
return Array.from(this.zones.values())
|
||||
}
|
||||
|
||||
// Getter for zone by id
|
||||
public getZoneById(zoneId: number): LoadedZone | undefined {
|
||||
return this.loadedZones.find((loadedZone) => loadedZone.getZone().id === zoneId)
|
||||
return this.zones.get(zoneId)
|
||||
}
|
||||
}
|
||||
|
||||
export interface ZoneAssets {
|
||||
tiles: string[]
|
||||
objects: string[]
|
||||
public getCharacter(characterId: number): ZoneCharacter | undefined {
|
||||
for (const zone of this.zones.values()) {
|
||||
const character = zone.getCharactersInZone().find((char) => char.character.id === characterId)
|
||||
if (character) return character
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
public removeCharacter(characterId: number): void {
|
||||
this.zones.forEach((zone) => zone.removeCharacter(characterId))
|
||||
}
|
||||
}
|
||||
|
||||
export default new ZoneManager()
|
||||
|
@ -37,7 +37,7 @@ export async function Authentication(socket: TSocket, next: any) {
|
||||
return next(new Error('Authentication error'))
|
||||
}
|
||||
|
||||
socket.user = (await UserRepository.getById(decoded.id)) as User
|
||||
socket.userId = decoded.id
|
||||
next()
|
||||
})
|
||||
} else {
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { Zone } from '@prisma/client'
|
||||
import zoneRepository from '../repositories/zoneRepository'
|
||||
import { Character, Zone } from '@prisma/client'
|
||||
import zoneEventTileRepository from '../repositories/zoneEventTileRepository'
|
||||
import ZoneCharacter from './zoneCharacter'
|
||||
|
||||
class LoadedZone {
|
||||
private readonly zone: Zone
|
||||
// private readonly npcs: ZoneNPC[] = []
|
||||
private characters: ZoneCharacter[] = []
|
||||
|
||||
constructor(zone: Zone) {
|
||||
this.zone = zone
|
||||
@ -13,10 +14,31 @@ class LoadedZone {
|
||||
return this.zone
|
||||
}
|
||||
|
||||
public addCharacter(character: Character) {
|
||||
const zoneCharacter = new ZoneCharacter(character)
|
||||
this.characters.push(zoneCharacter)
|
||||
}
|
||||
|
||||
public async removeCharacter(id: number) {
|
||||
const zoneCharacter = this.getCharacterById(id)
|
||||
if (zoneCharacter) {
|
||||
await zoneCharacter.savePosition()
|
||||
this.characters = this.characters.filter((c) => c.character.id !== id)
|
||||
}
|
||||
}
|
||||
|
||||
public getCharacterById(id: number): ZoneCharacter | undefined {
|
||||
return this.characters.find((c) => c.character.id === id)
|
||||
}
|
||||
|
||||
public getCharactersInZone(): ZoneCharacter[] {
|
||||
return this.characters
|
||||
}
|
||||
|
||||
public async getGrid(): Promise<number[][]> {
|
||||
let grid: number[][] = Array.from({ length: this.zone.height }, () => Array.from({ length: this.zone.width }, () => 0))
|
||||
|
||||
const eventTiles = await zoneRepository.getEventTiles(this.zone.id)
|
||||
const eventTiles = await zoneEventTileRepository.getAll(this.zone.id)
|
||||
|
||||
// Set the grid values based on the event tiles, these are strings
|
||||
eventTiles.forEach((eventTile) => {
|
||||
@ -27,20 +49,6 @@ class LoadedZone {
|
||||
|
||||
return grid
|
||||
}
|
||||
|
||||
/**
|
||||
* @TODO: Implement this
|
||||
* @param position
|
||||
*/
|
||||
public async isPositionWalkable(position: { x: number; y: number }): Promise<boolean> {
|
||||
const grid = await this.getGrid()
|
||||
if (!grid?.length) return false
|
||||
|
||||
const gridX = Math.floor(position.x)
|
||||
const gridY = Math.floor(position.y)
|
||||
|
||||
return grid[gridY]?.[gridX] === 1 || grid[gridY]?.[Math.ceil(position.x)] === 1 || grid[Math.ceil(position.y)]?.[gridX] === 1 || grid[Math.ceil(position.y)]?.[Math.ceil(position.x)] === 1
|
||||
}
|
||||
}
|
||||
|
||||
export default LoadedZone
|
||||
|
19
src/models/zoneCharacter.ts
Normal file
19
src/models/zoneCharacter.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Character } from '@prisma/client'
|
||||
import { CharacterService } from '../services/characterService'
|
||||
|
||||
class ZoneCharacter {
|
||||
public readonly character: Character
|
||||
public isMoving: boolean = false
|
||||
public currentPath: Array<{ x: number; y: number }> | null = null
|
||||
|
||||
constructor(character: Character) {
|
||||
this.character = character
|
||||
}
|
||||
|
||||
public async savePosition() {
|
||||
const characterService = new CharacterService()
|
||||
await characterService.updateCharacterPosition(this.character.id, this.character.positionX, this.character.positionY, this.character.rotation, this.character.zoneId)
|
||||
}
|
||||
}
|
||||
|
||||
export default ZoneCharacter
|
17
src/repositories/characterHairRepository.ts
Normal file
17
src/repositories/characterHairRepository.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import prisma from '../utilities/prisma' // Import the global Prisma instance
|
||||
import { CharacterHair } from '@prisma/client'
|
||||
|
||||
class CharacterHairRepository {
|
||||
async getAll(): Promise<CharacterHair[]> {
|
||||
return prisma.characterHair.findMany()
|
||||
}
|
||||
async getIsEnabledForCharCreationHair(): Promise<CharacterHair[]> {
|
||||
return prisma.characterHair.findMany({
|
||||
where: {
|
||||
isEnabledForCharCreation: true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default new CharacterHairRepository()
|
@ -1,8 +1,8 @@
|
||||
import prisma from '../utilities/prisma' // Import the global Prisma instance
|
||||
import { Character } from '@prisma/client'
|
||||
import { appLogger } from '../utilities/logger'
|
||||
|
||||
class CharacterRepository {
|
||||
async getByUserId(userId: number): Promise<Character[] | null> {
|
||||
async getByUserId(userId: number) {
|
||||
try {
|
||||
return await prisma.character.findMany({
|
||||
where: {
|
||||
@ -14,16 +14,22 @@ class CharacterRepository {
|
||||
include: {
|
||||
sprite: true
|
||||
}
|
||||
},
|
||||
characterHair: {
|
||||
include: {
|
||||
sprite: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
// Handle error
|
||||
throw new Error(`Failed to get character by user ID: ${error.message}`)
|
||||
appLogger.error(`Failed to get character by user ID: ${error instanceof Error ? error.message : String(error)}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async getByUserAndId(userId: number, characterId: number): Promise<Character | null> {
|
||||
async getByUserAndId(userId: number, characterId: number) {
|
||||
try {
|
||||
return await prisma.character.findFirst({
|
||||
where: {
|
||||
@ -36,16 +42,22 @@ class CharacterRepository {
|
||||
include: {
|
||||
sprite: true
|
||||
}
|
||||
},
|
||||
characterHair: {
|
||||
include: {
|
||||
sprite: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
// Handle error
|
||||
throw new Error(`Failed to get character by user ID and character ID: ${error.message}`)
|
||||
appLogger.error(`Failed to get character by user ID and character ID: ${error instanceof Error ? error.message : String(error)}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async getById(id: number): Promise<Character | null> {
|
||||
async getById(id: number) {
|
||||
try {
|
||||
return await prisma.character.findUnique({
|
||||
where: {
|
||||
@ -57,47 +69,22 @@ class CharacterRepository {
|
||||
include: {
|
||||
sprite: true
|
||||
}
|
||||
},
|
||||
characterHair: {
|
||||
include: {
|
||||
sprite: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
// Handle error
|
||||
throw new Error(`Failed to get character by ID: ${error.message}`)
|
||||
appLogger.error(`Failed to get character by ID: ${error instanceof Error ? error.message : String(error)}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async updatePosition(id: number, positionX: number, positionY: number): Promise<Character | null> {
|
||||
try {
|
||||
return await prisma.character.update({
|
||||
where: {
|
||||
id: id
|
||||
},
|
||||
data: {
|
||||
positionX,
|
||||
positionY
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
// Handle error
|
||||
throw new Error(`Failed to update character: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
async deleteByUserIdAndId(userId: number, characterId: number): Promise<Character | null> {
|
||||
try {
|
||||
return await prisma.character.delete({
|
||||
where: {
|
||||
userId,
|
||||
id: characterId
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
// Handle error
|
||||
throw new Error(`Failed to delete character by user ID and character ID: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
async getByName(name: string): Promise<Character | null> {
|
||||
async getByName(name: string) {
|
||||
try {
|
||||
return await prisma.character.findFirst({
|
||||
where: {
|
||||
@ -109,12 +96,18 @@ class CharacterRepository {
|
||||
include: {
|
||||
sprite: true
|
||||
}
|
||||
},
|
||||
characterHair: {
|
||||
include: {
|
||||
sprite: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
// Handle error
|
||||
throw new Error(`Failed to get character by name: ${error.message}`)
|
||||
appLogger.error(`Failed to get character by name: ${error instanceof Error ? error.message : String(error)}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
49
src/repositories/chatRepository.ts
Normal file
49
src/repositories/chatRepository.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import prisma from '../utilities/prisma'
|
||||
import { Chat } from '@prisma/client'
|
||||
|
||||
class ChatRepository {
|
||||
async getById(id: number): Promise<Chat | null> {
|
||||
return prisma.chat.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
character: true,
|
||||
zone: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async getAll(): Promise<Chat[]> {
|
||||
return prisma.chat.findMany({
|
||||
include: {
|
||||
character: true,
|
||||
zone: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async getByCharacterId(characterId: number): Promise<Chat[]> {
|
||||
return prisma.chat.findMany({
|
||||
where: {
|
||||
characterId
|
||||
},
|
||||
include: {
|
||||
character: true,
|
||||
zone: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async getByZoneId(zoneId: number): Promise<Chat[]> {
|
||||
return prisma.chat.findMany({
|
||||
where: {
|
||||
zoneId
|
||||
},
|
||||
include: {
|
||||
character: true,
|
||||
zone: true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default new ChatRepository()
|
45
src/repositories/passwordResetTokenRepository.ts
Normal file
45
src/repositories/passwordResetTokenRepository.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import prisma from '../utilities/prisma'
|
||||
import { appLogger } from '../utilities/logger' // Import the global Prisma instance
|
||||
|
||||
class PasswordResetTokenRepository {
|
||||
async getById(id: number): Promise<any> {
|
||||
try {
|
||||
return await prisma.passwordResetToken.findUnique({
|
||||
where: {
|
||||
id
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
// Handle error
|
||||
appLogger.error(`Failed to get password reset token by ID: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
}
|
||||
|
||||
async getByUserId(userId: number): Promise<any> {
|
||||
try {
|
||||
return await prisma.passwordResetToken.findFirst({
|
||||
where: {
|
||||
userId
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
// Handle error
|
||||
appLogger.error(`Failed to get password reset token by user ID: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
}
|
||||
|
||||
async getByToken(token: string): Promise<any> {
|
||||
try {
|
||||
return await prisma.passwordResetToken.findFirst({
|
||||
where: {
|
||||
token
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
// Handle error
|
||||
appLogger.error(`Failed to get password reset token by token: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new PasswordResetTokenRepository()
|
@ -1,5 +1,8 @@
|
||||
import prisma from '../utilities/prisma' // Import the global Prisma instance
|
||||
import { Tile } from '@prisma/client'
|
||||
import zoneRepository from './zoneRepository'
|
||||
import { unduplicateArray } from '../utilities/utilities'
|
||||
import { FlattenZoneArray } from '../utilities/zone'
|
||||
|
||||
class TileRepository {
|
||||
async getById(id: string): Promise<Tile | null> {
|
||||
@ -8,9 +11,28 @@ class TileRepository {
|
||||
})
|
||||
}
|
||||
|
||||
async getByIds(ids: string[]): Promise<Tile[]> {
|
||||
return prisma.tile.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: ids
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async getAll(): Promise<Tile[]> {
|
||||
return prisma.tile.findMany()
|
||||
}
|
||||
|
||||
async getByZoneId(zoneId: number): Promise<Tile[]> {
|
||||
const zone = await zoneRepository.getById(zoneId)
|
||||
if (!zone) return []
|
||||
|
||||
const zoneTileArray = unduplicateArray(FlattenZoneArray(JSON.parse(JSON.stringify(zone.tiles))))
|
||||
|
||||
return this.getByIds(zoneTileArray)
|
||||
}
|
||||
}
|
||||
|
||||
export default new TileRepository()
|
||||
|
@ -1,5 +1,6 @@
|
||||
import prisma from '../utilities/prisma' // Import the global Prisma instance
|
||||
import { User } from '@prisma/client'
|
||||
import { appLogger } from '../utilities/logger'
|
||||
|
||||
class UserRepository {
|
||||
async getById(id: number): Promise<User | null> {
|
||||
@ -11,7 +12,8 @@ class UserRepository {
|
||||
})
|
||||
} catch (error: any) {
|
||||
// Handle error
|
||||
throw new Error(`Failed to get user by ID: ${error.message}`)
|
||||
appLogger.error(`Failed to get user by ID: ${error instanceof Error ? error.message : String(error)}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,7 +26,22 @@ class UserRepository {
|
||||
})
|
||||
} catch (error: any) {
|
||||
// Handle error
|
||||
throw new Error(`Failed to get user by username: ${error.message}`)
|
||||
appLogger.error(`Failed to get user by username: ${error instanceof Error ? error.message : String(error)}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async getByEmail(email: string): Promise<User | null> {
|
||||
try {
|
||||
return await prisma.user.findUnique({
|
||||
where: {
|
||||
email
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
// Handle error
|
||||
appLogger.error(`Failed to get user by email: ${error instanceof Error ? error.message : String(error)}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
19
src/repositories/worldRepository.ts
Normal file
19
src/repositories/worldRepository.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import prisma from '../utilities/prisma' // Import the global Prisma instance
|
||||
import { World } from '@prisma/client'
|
||||
import { gameLogger } from '../utilities/logger'
|
||||
|
||||
class WorldRepository {
|
||||
async getFirst(): Promise<World | null> {
|
||||
try {
|
||||
return await prisma.world.findFirst({
|
||||
orderBy: { date: 'desc' }
|
||||
})
|
||||
} catch (error: any) {
|
||||
// Handle error
|
||||
gameLogger.error(`Failed to get first world: ${error instanceof Error ? error.message : String(error)}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new WorldRepository()
|
36
src/repositories/zoneEventTileRepository.ts
Normal file
36
src/repositories/zoneEventTileRepository.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { ZoneEventTile } from '@prisma/client'
|
||||
import prisma from '../utilities/prisma'
|
||||
import { appLogger } from '../utilities/logger'
|
||||
|
||||
class ZoneEventTileRepository {
|
||||
async getAll(id: number): Promise<ZoneEventTile[]> {
|
||||
try {
|
||||
return await prisma.zoneEventTile.findMany({
|
||||
where: {
|
||||
zoneId: id
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
appLogger.error(`Failed to get zone event tiles: ${error.message}`)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
async getEventTileByZoneIdAndPosition(zoneId: number, positionX: number, positionY: number) {
|
||||
try {
|
||||
return await prisma.zoneEventTile.findFirst({
|
||||
where: {
|
||||
zoneId: zoneId,
|
||||
positionX: positionX,
|
||||
positionY: positionY
|
||||
},
|
||||
include: { teleport: true }
|
||||
})
|
||||
} catch (error: any) {
|
||||
appLogger.error(`Failed to get zone event tile: ${error.message}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new ZoneEventTileRepository()
|
@ -1,20 +1,9 @@
|
||||
import { Zone, ZoneEventTile, ZoneEventTileType, ZoneObject } from '@prisma/client'
|
||||
import prisma from '../utilities/prisma'
|
||||
import { ZoneEventTileWithTeleport } from '../socketEvents/zone/characterMove'
|
||||
import { ZoneEventTileWithTeleport } from '../utilities/types'
|
||||
import { appLogger } from '../utilities/logger'
|
||||
import { TAsset } from '../utilities/types'
|
||||
import tileRepository from './tileRepository'
|
||||
|
||||
class ZoneRepository {
|
||||
async getFirst(): Promise<Zone | null> {
|
||||
try {
|
||||
return await prisma.zone.findFirst()
|
||||
} catch (error: any) {
|
||||
appLogger.error(`Failed to get first zone: ${error.message}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async getAll(): Promise<Zone[]> {
|
||||
try {
|
||||
return await prisma.zone.findMany()
|
||||
@ -64,18 +53,19 @@ class ZoneRepository {
|
||||
}
|
||||
}
|
||||
|
||||
async getEventTeleportTiles(id: number): Promise<ZoneEventTileWithTeleport[]> {
|
||||
async getFirstEventTile(zoneId: number, positionX: number, positionY: number): Promise<ZoneEventTile | null> {
|
||||
try {
|
||||
return (await prisma.zoneEventTile.findMany({
|
||||
return await prisma.zoneEventTile.findFirst({
|
||||
where: {
|
||||
zoneId: id,
|
||||
type: ZoneEventTileType.TELEPORT
|
||||
zoneId: zoneId,
|
||||
positionX: positionX,
|
||||
positionY: positionY
|
||||
},
|
||||
include: { teleport: true }
|
||||
})) as unknown as ZoneEventTileWithTeleport[]
|
||||
})
|
||||
} catch (error: any) {
|
||||
appLogger.error(`Failed to get zone event tiles: ${error.message}`)
|
||||
return []
|
||||
appLogger.error(`Failed to get zone event tile: ${error.message}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,52 +81,6 @@ class ZoneRepository {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
async getZoneAssets(zone_id: number): Promise<TAsset[]> {
|
||||
const zone = await this.getById(zone_id)
|
||||
if (!zone) return []
|
||||
|
||||
const assets: TAsset[] = []
|
||||
|
||||
// zone.tiles is prisma jsonvalue
|
||||
let tiles = JSON.parse(JSON.stringify(zone.tiles))
|
||||
tiles = [...new Set(tiles.flat())]
|
||||
|
||||
// Add tile assets
|
||||
for (const tile of tiles) {
|
||||
const tileInfo = await tileRepository.getById(tile)
|
||||
if (!tileInfo) continue
|
||||
|
||||
assets.push({
|
||||
key: tileInfo.id,
|
||||
url: '/assets/tiles/' + tileInfo.id + '.png',
|
||||
group: 'tiles',
|
||||
updatedAt: tileInfo?.updatedAt || new Date()
|
||||
})
|
||||
}
|
||||
|
||||
// Add object assets
|
||||
for (const zoneObject of zone.zoneObjects) {
|
||||
if (!zoneObject.object) continue
|
||||
|
||||
assets.push({
|
||||
key: zoneObject.object.id,
|
||||
url: '/assets/objects/' + zoneObject.object.id + '.png',
|
||||
group: 'objects',
|
||||
updatedAt: zoneObject.object.updatedAt || new Date()
|
||||
})
|
||||
}
|
||||
|
||||
// Filter out duplicate assets
|
||||
return assets.reduce((acc: TAsset[], current) => {
|
||||
const x = acc.find((item) => item.key === current.key && item.group === current.group)
|
||||
if (!x) {
|
||||
return acc.concat([current])
|
||||
} else {
|
||||
return acc
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
}
|
||||
|
||||
export default new ZoneRepository()
|
||||
|
@ -13,9 +13,9 @@ import { appLogger, watchLogs } from './utilities/logger'
|
||||
import ZoneManager from './managers/zoneManager'
|
||||
import UserManager from './managers/userManager'
|
||||
import CommandManager from './managers/commandManager'
|
||||
import CharacterManager from './managers/characterManager'
|
||||
import QueueManager from './managers/queueManager'
|
||||
import DateManager from './managers/dateManager'
|
||||
import WeatherManager from './managers/weatherManager'
|
||||
|
||||
export class Server {
|
||||
private readonly app: Application
|
||||
@ -27,7 +27,14 @@ export class Server {
|
||||
*/
|
||||
constructor() {
|
||||
this.app = express()
|
||||
this.app.use(cors())
|
||||
this.app.use(
|
||||
cors({
|
||||
origin: config.CLIENT_URL,
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // Add supported methods
|
||||
allowedHeaders: ['Content-Type', 'Authorization'], // Add allowed headers
|
||||
credentials: true
|
||||
})
|
||||
)
|
||||
this.app.use(express.json())
|
||||
this.app.use(express.urlencoded({ extended: true }))
|
||||
this.http = httpServer(this.app)
|
||||
@ -70,12 +77,12 @@ export class Server {
|
||||
// Load date manager
|
||||
await DateManager.boot(this.io)
|
||||
|
||||
// Load weather manager
|
||||
await WeatherManager.boot(this.io)
|
||||
|
||||
// Load zoneEditor manager
|
||||
await ZoneManager.boot()
|
||||
|
||||
// Load character manager
|
||||
await CharacterManager.boot()
|
||||
|
||||
// Load command manager
|
||||
await CommandManager.boot(this.io)
|
||||
|
||||
|
@ -1,43 +0,0 @@
|
||||
import { ExtendedCharacter } from '../../utilities/types'
|
||||
import { AStar } from '../../utilities/character/aStar'
|
||||
import ZoneManager from '../../managers/zoneManager'
|
||||
import Rotation from '../../utilities/character/rotation'
|
||||
import { gameLogger } from '../../utilities/logger'
|
||||
|
||||
export class CharacterMoveService {
|
||||
public updatePosition(character: ExtendedCharacter, position: { x: number; y: number }, newZoneId?: number) {
|
||||
Object.assign(character, {
|
||||
positionX: position.x,
|
||||
positionY: position.y,
|
||||
rotation: Rotation.calculate(character.positionX, character.positionY, position.x, position.y),
|
||||
zoneId: newZoneId || character.zoneId
|
||||
})
|
||||
|
||||
// await prisma.character.update({
|
||||
// where: { id: character.id },
|
||||
// data: {
|
||||
// positionX: position.x,
|
||||
// positionY: position.y,
|
||||
// rotation: character.rotation,
|
||||
// zoneId: newZoneId
|
||||
// }
|
||||
// })
|
||||
}
|
||||
|
||||
public async calculatePath(character: ExtendedCharacter, targetX: number, targetY: number): Promise<Array<{ x: number; y: number }> | null> {
|
||||
const grid = await ZoneManager.getZoneById(character.zoneId)?.getGrid()
|
||||
if (!grid?.length) {
|
||||
gameLogger.error('character:move error', 'Grid not found or empty')
|
||||
return null
|
||||
}
|
||||
|
||||
const start = { x: Math.floor(character.positionX), y: Math.floor(character.positionY) }
|
||||
const end = { x: Math.floor(targetX), y: Math.floor(targetY) }
|
||||
|
||||
return AStar.findPath(start, end, grid)
|
||||
}
|
||||
|
||||
public async applyMovementDelay(): Promise<void> {
|
||||
await new Promise((resolve) => setTimeout(resolve, 250)) // 250ms delay between steps
|
||||
}
|
||||
}
|
104
src/services/characterService.ts
Normal file
104
src/services/characterService.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { AStar } from '../utilities/character/aStar'
|
||||
import ZoneManager from '../managers/zoneManager'
|
||||
import prisma from '../utilities/prisma'
|
||||
import Rotation from '../utilities/character/rotation'
|
||||
import { appLogger, gameLogger } from '../utilities/logger'
|
||||
import { Character } from '@prisma/client'
|
||||
|
||||
interface Position {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export class CharacterService {
|
||||
private readonly MOVEMENT_DELAY_MS = 250
|
||||
|
||||
async create(name: string, userId: number) {
|
||||
return prisma.character.create({
|
||||
data: {
|
||||
name,
|
||||
userId
|
||||
// characterTypeId: 1 // @TODO set to chosen character type
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async updateHair(characterId: number, characterHairId: number | null) {
|
||||
await prisma.character.update({
|
||||
where: { id: characterId },
|
||||
data: {
|
||||
characterHairId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async deleteByUserIdAndId(userId: number, characterId: number): Promise<Character | null> {
|
||||
try {
|
||||
return await prisma.character.delete({
|
||||
where: {
|
||||
userId,
|
||||
id: characterId
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
// Handle error
|
||||
appLogger.error(`Failed to delete character by user ID and character ID: ${error instanceof Error ? error.message : String(error)}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async updateCharacterPosition(id: number, positionX: number, positionY: number, rotation: number, zoneId: number) {
|
||||
await prisma.character.update({
|
||||
where: { id },
|
||||
data: {
|
||||
positionX,
|
||||
positionY,
|
||||
rotation,
|
||||
zoneId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public updatePosition(character: Character, position: Position, newZoneId?: number): void {
|
||||
if (!this.isValidPosition(position)) {
|
||||
gameLogger.error(`Invalid position coordinates: ${position.x}, ${position.y}`)
|
||||
}
|
||||
|
||||
Object.assign(character, {
|
||||
positionX: position.x,
|
||||
positionY: position.y,
|
||||
rotation: Rotation.calculate(character.positionX, character.positionY, position.x, position.y),
|
||||
zoneId: newZoneId ?? character.zoneId
|
||||
})
|
||||
}
|
||||
|
||||
public async calculatePath(character: Character, targetX: number, targetY: number): Promise<Position[] | null> {
|
||||
const zone = ZoneManager.getZoneById(character.zoneId)
|
||||
const grid = await zone?.getGrid()
|
||||
|
||||
if (!grid?.length) {
|
||||
gameLogger.error('character:move error', 'Grid not found or empty')
|
||||
return null
|
||||
}
|
||||
|
||||
const start: Position = {
|
||||
x: Math.floor(character.positionX),
|
||||
y: Math.floor(character.positionY)
|
||||
}
|
||||
|
||||
const end: Position = {
|
||||
x: Math.floor(targetX),
|
||||
y: Math.floor(targetY)
|
||||
}
|
||||
|
||||
return AStar.findPath(start, end, grid)
|
||||
}
|
||||
|
||||
public async applyMovementDelay(): Promise<void> {
|
||||
await new Promise((resolve) => setTimeout(resolve, this.MOVEMENT_DELAY_MS))
|
||||
}
|
||||
|
||||
private isValidPosition(position: Position): boolean {
|
||||
return Number.isFinite(position.x) && Number.isFinite(position.y) && position.x >= 0 && position.y >= 0
|
||||
}
|
||||
}
|
30
src/services/chatService.ts
Normal file
30
src/services/chatService.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import prisma from '../utilities/prisma'
|
||||
import { gameLogger } from '../utilities/logger'
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../utilities/types'
|
||||
import ChatRepository from '../repositories/chatRepository'
|
||||
|
||||
class ChatService {
|
||||
async sendZoneMessage(io: Server, socket: TSocket, message: string, characterId: number, zoneId: number): Promise<boolean> {
|
||||
try {
|
||||
const newChat = await prisma.chat.create({
|
||||
data: {
|
||||
characterId,
|
||||
zoneId,
|
||||
message
|
||||
}
|
||||
})
|
||||
|
||||
const chat = await ChatRepository.getById(newChat.id)
|
||||
if (!chat) return false
|
||||
|
||||
io.to(zoneId.toString()).emit('chat:message', chat)
|
||||
return true
|
||||
} catch (error: any) {
|
||||
gameLogger.error(`Failed to save chat message: ${error instanceof Error ? error.message : String(error)}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ChatService
|
31
src/services/passwordResetTokenService.ts
Normal file
31
src/services/passwordResetTokenService.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import prisma from '../utilities/prisma'
|
||||
import passwordResetTokenRepository from '../repositories/passwordResetTokenRepository'
|
||||
import { appLogger } from '../utilities/logger'
|
||||
|
||||
class PasswordResetTokenService {
|
||||
/**
|
||||
* Delete token
|
||||
* @param token
|
||||
*/
|
||||
public async delete(token: string): Promise<boolean> {
|
||||
try {
|
||||
const tokenData = await passwordResetTokenRepository.getByToken(token)
|
||||
if (!tokenData) {
|
||||
return false
|
||||
}
|
||||
|
||||
await prisma.passwordResetToken.delete({
|
||||
where: {
|
||||
token
|
||||
}
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (error: any) {
|
||||
appLogger.error(`Error deleting password reset token: ${error instanceof Error ? error.message : String(error)}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default PasswordResetTokenService
|
@ -1,7 +1,12 @@
|
||||
import bcrypt from 'bcryptjs'
|
||||
import UserRepository from '../repositories/userRepository'
|
||||
import PasswordResetTokenRepository from '../repositories/passwordResetTokenRepository'
|
||||
import prisma from '../utilities/prisma'
|
||||
import { User } from '@prisma/client'
|
||||
import config from '../utilities/config'
|
||||
import NodeMailer from 'nodemailer'
|
||||
import { httpLogger } from '../utilities/logger'
|
||||
import PasswordResetTokenService from './passwordResetTokenService'
|
||||
|
||||
/**
|
||||
* User service
|
||||
@ -15,37 +20,145 @@ class UserService {
|
||||
* @param password
|
||||
*/
|
||||
async login(username: string, password: string): Promise<boolean | User> {
|
||||
const user = await UserRepository.getByUsername(username)
|
||||
if (!user) {
|
||||
try {
|
||||
const user = await UserRepository.getByUsername(username)
|
||||
if (!user) {
|
||||
return false
|
||||
}
|
||||
|
||||
const passwordMatch = await bcrypt.compare(password, user.password)
|
||||
if (!passwordMatch) {
|
||||
httpLogger.error(`Failed to login user: ${username}`)
|
||||
return false
|
||||
}
|
||||
|
||||
return user
|
||||
} catch (error: any) {
|
||||
httpLogger.error(`Error logging in user: ${error instanceof Error ? error.message : String(error)}`)
|
||||
return false
|
||||
}
|
||||
|
||||
const passwordMatch = await bcrypt.compare(password, user.password)
|
||||
if (!passwordMatch) {
|
||||
return false
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
/**
|
||||
* Register user
|
||||
* @param username
|
||||
* @param email
|
||||
* @param password
|
||||
*/
|
||||
async register(username: string, password: string): Promise<boolean | User> {
|
||||
const user = await UserRepository.getByUsername(username)
|
||||
if (user) {
|
||||
async register(username: string, email: string, password: string): Promise<boolean | User> {
|
||||
try {
|
||||
const user = await UserRepository.getByUsername(username)
|
||||
if (user) {
|
||||
return false
|
||||
}
|
||||
|
||||
const userByEmail = await UserRepository.getByEmail(email)
|
||||
if (userByEmail) {
|
||||
httpLogger.error(`User already exists: ${email}`)
|
||||
return false
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10)
|
||||
return prisma.user.create({
|
||||
data: {
|
||||
username,
|
||||
email,
|
||||
password: hashedPassword
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
httpLogger.error(`Error registering user: ${error instanceof Error ? error.message : String(error)}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10)
|
||||
return prisma.user.create({
|
||||
data: {
|
||||
username,
|
||||
password: hashedPassword
|
||||
/**
|
||||
* Reset password
|
||||
* @param email
|
||||
*/
|
||||
async requestPasswordReset(email: string): Promise<boolean> {
|
||||
try {
|
||||
const user = await UserRepository.getByEmail(email)
|
||||
if (!user) return false
|
||||
|
||||
const token = await bcrypt.hash(new Date().getTime().toString(), 10)
|
||||
const latestToken = await PasswordResetTokenRepository.getByUserId(user.id)
|
||||
|
||||
// Check if password reset has been requested recently
|
||||
if (latestToken) {
|
||||
const tokenExpiryDate = new Date(Date.now() - 24 * 60 * 60 * 1000) // 24 hours
|
||||
const isTokenExpired = latestToken.createdAt < tokenExpiryDate
|
||||
|
||||
if (!isTokenExpired) return false
|
||||
|
||||
await prisma.passwordResetToken.delete({
|
||||
where: {
|
||||
id: latestToken.id
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
await prisma.passwordResetToken.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
token: token
|
||||
}
|
||||
})
|
||||
|
||||
const transporter = NodeMailer.createTransport({
|
||||
host: config.SMTP_HOST,
|
||||
port: config.SMTP_PORT,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: config.SMTP_USER,
|
||||
pass: config.SMTP_PASSWORD
|
||||
}
|
||||
})
|
||||
|
||||
await transporter.sendMail({
|
||||
from: config.SMTP_USER,
|
||||
to: email,
|
||||
subject: 'Reset your password',
|
||||
text: 'A password reset has been requested, reset your password here: ' + config.CLIENT_URL + '#' + token, // Plain text body
|
||||
html: "<p>A password reset has been requested, reset your password here: <a href='" + config.CLIENT_URL + '#' + token + "'>" + config.CLIENT_URL + '#' + token + '</a></p>' // Html body
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (error: any) {
|
||||
httpLogger.error(`Error sending password reset email: ${error instanceof Error ? error.message : String(error)}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set new password
|
||||
* @param urlToken
|
||||
* @param password
|
||||
*/
|
||||
async resetPassword(urlToken: string, password: string): Promise<boolean> {
|
||||
try {
|
||||
const tokenData = await PasswordResetTokenRepository.getByToken(urlToken)
|
||||
if (!tokenData) {
|
||||
return false
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10)
|
||||
await prisma.user.update({
|
||||
where: { id: tokenData.userId },
|
||||
data: {
|
||||
password: hashedPassword
|
||||
}
|
||||
})
|
||||
|
||||
// Delete the token
|
||||
const passwordResetTokenService = new PasswordResetTokenService()
|
||||
await passwordResetTokenService.delete(urlToken)
|
||||
|
||||
return true
|
||||
} catch (error: any) {
|
||||
httpLogger.error(`Error setting new password: ${error instanceof Error ? error.message : String(error)}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
37
src/services/worldService.ts
Normal file
37
src/services/worldService.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import prisma from '../utilities/prisma'
|
||||
import { gameLogger } from '../utilities/logger'
|
||||
import { World } from '@prisma/client'
|
||||
import WorldRepository from '../repositories/worldRepository'
|
||||
|
||||
class WorldService {
|
||||
async update(worldData: Partial<World>): Promise<boolean> {
|
||||
try {
|
||||
const currentWorld = await WorldRepository.getFirst()
|
||||
if (!currentWorld) {
|
||||
// If no world exists, create first record
|
||||
await prisma.world.create({
|
||||
data: {
|
||||
...worldData,
|
||||
date: worldData.date || new Date()
|
||||
}
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
// Update existing world using its date as unique identifier
|
||||
await prisma.world.update({
|
||||
where: {
|
||||
date: currentWorld.date
|
||||
},
|
||||
data: worldData
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (error: any) {
|
||||
gameLogger.error(`Failed to update world: ${error instanceof Error ? error.message : String(error)}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new WorldService()
|
@ -1,18 +1,21 @@
|
||||
import { ExtendedCharacter, TSocket } from '../utilities/types'
|
||||
import prisma from '../utilities/prisma'
|
||||
import ZoneRepository from '../repositories/zoneRepository'
|
||||
import { ZoneEventTileTeleport } from '@prisma/client'
|
||||
import { Server } from 'socket.io'
|
||||
import CharacterManager from '../managers/characterManager'
|
||||
import ZoneManager from '../managers/zoneManager'
|
||||
import { gameLogger } from '../utilities/logger'
|
||||
|
||||
export class ZoneEventTileService {
|
||||
public async handleTeleport(io: Server, socket: TSocket, character: ExtendedCharacter, teleport: ZoneEventTileTeleport): Promise<void> {
|
||||
if (teleport.toZoneId === character.zoneId) return
|
||||
|
||||
const zone = await ZoneRepository.getById(teleport.toZoneId)
|
||||
if (!zone) return
|
||||
const loadedZone = ZoneManager.getZoneById(teleport.toZoneId)
|
||||
if (!loadedZone) {
|
||||
gameLogger.error('zone:character:join error', 'Loaded zone not found')
|
||||
return
|
||||
}
|
||||
|
||||
// CharacterManager.moveCharacterBetweenZones(character, zone)
|
||||
const zone = loadedZone.getZone()
|
||||
|
||||
const oldZoneId = character.zoneId
|
||||
const newZoneId = teleport.toZoneId
|
||||
@ -35,6 +38,10 @@ export class ZoneEventTileService {
|
||||
character.positionY = teleport.toPositionY
|
||||
character.isMoving = false
|
||||
|
||||
// Remove and add character to new zone
|
||||
await loadedZone.removeCharacter(character.id)
|
||||
loadedZone.addCharacter(character)
|
||||
|
||||
// Emit events
|
||||
io.to(oldZoneId.toString()).emit('zone:character:leave', character.id)
|
||||
io.to(newZoneId.toString()).emit('zone:character:join', character)
|
||||
@ -46,7 +53,7 @@ export class ZoneEventTileService {
|
||||
// Send teleport information to the client
|
||||
socket.emit('zone:character:teleport', {
|
||||
zone,
|
||||
characters: CharacterManager.getCharactersInZone(zone)
|
||||
characters: loadedZone.getCharactersInZone()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,31 +1,38 @@
|
||||
import prisma from '../utilities/prisma'
|
||||
import { gameLogger } from '../utilities/logger'
|
||||
|
||||
class ZoneService {
|
||||
async createDemoZone(): Promise<boolean> {
|
||||
const tiles = [
|
||||
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
||||
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
||||
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
||||
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
||||
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
||||
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
||||
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
||||
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
||||
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
||||
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile']
|
||||
]
|
||||
try {
|
||||
const tiles = [
|
||||
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
||||
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
||||
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
||||
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
||||
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
||||
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
||||
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
||||
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
||||
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
||||
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile']
|
||||
]
|
||||
|
||||
await prisma.zone.create({
|
||||
data: {
|
||||
name: 'Demo zone',
|
||||
width: 10,
|
||||
height: 10,
|
||||
tiles
|
||||
}
|
||||
})
|
||||
await prisma.zone.create({
|
||||
data: {
|
||||
name: 'Demo zone',
|
||||
width: 10,
|
||||
height: 10,
|
||||
tiles
|
||||
}
|
||||
})
|
||||
|
||||
console.log('Demo zone created.')
|
||||
return true
|
||||
gameLogger.info('Demo zone created.')
|
||||
|
||||
return true
|
||||
} catch (error: any) {
|
||||
gameLogger.error(`Failed to create demo zone: ${error instanceof Error ? error.message : String(error)}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,22 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { CharacterHair } from '@prisma/client'
|
||||
import { TSocket } from '../../../utilities/types'
|
||||
import characterHairRepository from '../../../repositories/characterHairRepository'
|
||||
|
||||
interface IPayload {}
|
||||
|
||||
export default class characterHairListEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('character:hair:list', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: IPayload, callback: (response: CharacterHair[]) => void): Promise<void> {
|
||||
const items = await characterHairRepository.getIsEnabledForCharCreationHair()
|
||||
callback(items)
|
||||
}
|
||||
}
|
@ -1,9 +1,13 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../utilities/types'
|
||||
import CharacterRepository from '../../repositories/characterRepository'
|
||||
import { gameLogger } from '../../utilities/logger'
|
||||
import ZoneManager from '../../managers/zoneManager'
|
||||
import { CharacterService } from '../../services/characterService'
|
||||
|
||||
type SocketResponseT = {
|
||||
character_id: number
|
||||
interface CharacterConnectPayload {
|
||||
characterId: number
|
||||
characterHairId?: number
|
||||
}
|
||||
|
||||
export default class CharacterConnectEvent {
|
||||
@ -16,16 +20,48 @@ export default class CharacterConnectEvent {
|
||||
this.socket.on('character:connect', this.handleCharacterConnect.bind(this))
|
||||
}
|
||||
|
||||
private async handleCharacterConnect(data: SocketResponseT): Promise<void> {
|
||||
console.log('character:connect requested', data)
|
||||
private async handleCharacterConnect({ characterId, characterHairId }: CharacterConnectPayload): Promise<void> {
|
||||
if (!this.socket.userId) {
|
||||
this.emitError('User not authenticated')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const character = await CharacterRepository.getByUserAndId(this.socket?.user?.id as number, data.character_id)
|
||||
if (!character) return
|
||||
if (await this.hasActiveCharacter()) {
|
||||
this.emitError('You are already connected to another character')
|
||||
return
|
||||
}
|
||||
|
||||
// Update hair
|
||||
const characterService = new CharacterService()
|
||||
await characterService.updateHair(characterId, characterHairId ?? null)
|
||||
|
||||
const character = await CharacterRepository.getByUserAndId(this.socket.userId!, characterId)
|
||||
if (!character) {
|
||||
this.emitError('Character not found or does not belong to this user')
|
||||
return
|
||||
}
|
||||
|
||||
this.socket.characterId = character.id
|
||||
this.socket.emit('character:connect', character)
|
||||
} catch (error: any) {
|
||||
console.log('character:connect error', error)
|
||||
} catch (error) {
|
||||
this.handleError('Failed to connect character', error) // @TODO : Make global error handler
|
||||
}
|
||||
}
|
||||
|
||||
private async hasActiveCharacter(): Promise<boolean> {
|
||||
const characters = await CharacterRepository.getByUserId(this.socket.userId!)
|
||||
return characters?.some((char) => ZoneManager.getCharacter(char.id)) ?? false
|
||||
}
|
||||
|
||||
private emitError(message: string): void {
|
||||
this.socket.emit('notification', { title: 'Server message', message })
|
||||
gameLogger.error('character:connect error', `Player ${this.socket.userId}: ${message}`)
|
||||
}
|
||||
|
||||
private handleError(context: string, error: unknown): void {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
this.emitError(`${context}: ${errorMessage}`)
|
||||
gameLogger.error('character:connect error', errorMessage)
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,8 @@ import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../utilities/types'
|
||||
import { Character } from '@prisma/client'
|
||||
import CharacterRepository from '../../repositories/characterRepository'
|
||||
import { CharacterService } from '../../services/characterService'
|
||||
import { ZCharacterCreate } from '../../utilities/zodTypes'
|
||||
import prisma from '../../utilities/prisma'
|
||||
import { gameLogger } from '../../utilities/logger'
|
||||
import { ZodError } from 'zod'
|
||||
|
||||
@ -23,7 +23,7 @@ export default class CharacterCreateEvent {
|
||||
try {
|
||||
data = ZCharacterCreate.parse(data)
|
||||
|
||||
const user_id = this.socket.user?.id as number
|
||||
const user_id = this.socket.userId!
|
||||
|
||||
// Check if character name already exists
|
||||
const characterExists = await CharacterRepository.getByName(data.name)
|
||||
@ -38,13 +38,8 @@ export default class CharacterCreateEvent {
|
||||
return this.socket.emit('notification', { message: 'You can only have 4 characters' })
|
||||
}
|
||||
|
||||
const character: Character = await prisma.character.create({
|
||||
data: {
|
||||
name: data.name,
|
||||
userId: user_id
|
||||
// characterTypeId: 1 // @TODO set to chosen character type
|
||||
}
|
||||
})
|
||||
const characterService = new CharacterService()
|
||||
const character: Character = await characterService.create(data.name, user_id)
|
||||
|
||||
characters = [...characters, character]
|
||||
|
||||
|
@ -2,9 +2,10 @@ import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../utilities/types'
|
||||
import { Character, Zone } from '@prisma/client'
|
||||
import CharacterRepository from '../../repositories/characterRepository'
|
||||
import { CharacterService } from '../../services/characterService'
|
||||
|
||||
type TypePayload = {
|
||||
character_id: number
|
||||
characterId: number
|
||||
}
|
||||
|
||||
type TypeResponse = {
|
||||
@ -23,16 +24,14 @@ export default class CharacterDeleteEvent {
|
||||
}
|
||||
|
||||
private async handleCharacterDelete(data: TypePayload, callback: (response: TypeResponse) => void): Promise<any> {
|
||||
// zod validate
|
||||
try {
|
||||
await CharacterRepository.deleteByUserIdAndId(this.socket.user?.id as number, data.character_id as number)
|
||||
const characterService = new CharacterService()
|
||||
await characterService.deleteByUserIdAndId(this.socket.userId!, data.characterId!)
|
||||
|
||||
const user_id = this.socket.user?.id as number
|
||||
const characters: Character[] = (await CharacterRepository.getByUserId(user_id)) as Character[]
|
||||
const characters: Character[] = (await CharacterRepository.getByUserId(this.socket.userId!)) as Character[]
|
||||
|
||||
this.socket.emit('character:list', characters)
|
||||
} catch (error: any) {
|
||||
console.log(error)
|
||||
return this.socket.emit('notification', { message: 'Character delete failed. Please try again.' })
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { Socket, Server } from 'socket.io'
|
||||
import { TSocket } from '../../utilities/types'
|
||||
import { Character } from '@prisma/client'
|
||||
import CharacterRepository from '../../repositories/characterRepository'
|
||||
import { gameLogger } from '../../utilities/logger'
|
||||
|
||||
export default class CharacterListEvent {
|
||||
constructor(
|
||||
@ -15,12 +16,10 @@ export default class CharacterListEvent {
|
||||
|
||||
private async handleCharacterList(data: any): Promise<void> {
|
||||
try {
|
||||
console.log('character:list requested')
|
||||
const user_id = this.socket.user?.id as number
|
||||
const characters: Character[] = (await CharacterRepository.getByUserId(user_id)) as Character[]
|
||||
const characters: Character[] = (await CharacterRepository.getByUserId(this.socket.userId!)) as Character[]
|
||||
this.socket.emit('character:list', characters)
|
||||
} catch (error: any) {
|
||||
console.log('character:list error', error)
|
||||
gameLogger.error('character:list error', error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ export default class AlertCommandEvent {
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('chat:send_message', this.handleAlertCommand.bind(this))
|
||||
this.socket.on('chat:message', this.handleAlertCommand.bind(this))
|
||||
}
|
||||
|
||||
private async handleAlertCommand(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
|
||||
@ -24,22 +24,27 @@ export default class AlertCommandEvent {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if character exists
|
||||
const character = await CharacterRepository.getByUserAndId(this.socket.userId!, this.socket.characterId!)
|
||||
if (!character) {
|
||||
gameLogger.error('chat:alert_command error', 'Character not found')
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
// Check if the user is the GM
|
||||
if (character.role !== 'gm') {
|
||||
gameLogger.info(`User ${character.id} tried to set time but is not a game master.`)
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
const args = getArgs('alert', data.message)
|
||||
|
||||
if (!args) {
|
||||
callback(false)
|
||||
return
|
||||
}
|
||||
|
||||
const character = await CharacterRepository.getByUserAndId(this.socket.user?.id as number, this.socket.characterId as number)
|
||||
if (!character) {
|
||||
gameLogger.error('chat:alert_command error', 'Character not found')
|
||||
callback(false)
|
||||
return
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
this.io.emit('notification', { title: 'Message from GM', message: args.join(' ') })
|
||||
callback(true)
|
||||
return callback(true)
|
||||
} catch (error: any) {
|
||||
gameLogger.error('chat:alert_command error', error.message)
|
||||
callback(false)
|
||||
|
60
src/socketEvents/chat/gameMaster/setTimeCommand.ts
Normal file
60
src/socketEvents/chat/gameMaster/setTimeCommand.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../utilities/types'
|
||||
import { getArgs, isCommand } from '../../../utilities/chat'
|
||||
import CharacterRepository from '../../../repositories/characterRepository'
|
||||
import { gameLogger } from '../../../utilities/logger'
|
||||
import DateManager from '../../../managers/dateManager'
|
||||
|
||||
type TypePayload = {
|
||||
message: string
|
||||
}
|
||||
|
||||
export default class SetTimeCommand {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('chat:message', this.handleAlertCommand.bind(this))
|
||||
}
|
||||
|
||||
private async handleAlertCommand(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
|
||||
try {
|
||||
if (!isCommand(data.message, 'time')) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if character exists
|
||||
const character = await CharacterRepository.getByUserAndId(this.socket.userId!, this.socket.characterId!)
|
||||
if (!character) {
|
||||
gameLogger.error('chat:alert_command error', 'Character not found')
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the user is the GM
|
||||
if (character.role !== 'gm') {
|
||||
gameLogger.info(`User ${character.id} tried to set time but is not a game master.`)
|
||||
return
|
||||
}
|
||||
|
||||
// Get arguments
|
||||
const args = getArgs('time', data.message)
|
||||
|
||||
if (!args) {
|
||||
return
|
||||
}
|
||||
|
||||
const time = args[0] // 24h time, e.g. 17:34
|
||||
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
await DateManager.setTime(time)
|
||||
} catch (error: any) {
|
||||
gameLogger.error('command error', error.message)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
@ -2,8 +2,10 @@ import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../utilities/types'
|
||||
import { getArgs, isCommand } from '../../../utilities/chat'
|
||||
import ZoneRepository from '../../../repositories/zoneRepository'
|
||||
import CharacterManager from '../../../managers/characterManager'
|
||||
import { gameMasterLogger } from '../../../utilities/logger'
|
||||
import { gameLogger, gameMasterLogger } from '../../../utilities/logger'
|
||||
import ZoneManager from '../../../managers/zoneManager'
|
||||
import ZoneCharacter from '../../../models/zoneCharacter'
|
||||
import zoneManager from '../../../managers/zoneManager'
|
||||
|
||||
type TypePayload = {
|
||||
message: string
|
||||
@ -16,14 +18,23 @@ export default class TeleportCommandEvent {
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('chat:send_message', this.handleTeleportCommand.bind(this))
|
||||
this.socket.on('chat:message', this.handleTeleportCommand.bind(this))
|
||||
}
|
||||
|
||||
private async handleTeleportCommand(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
|
||||
try {
|
||||
const character = CharacterManager.getCharacterFromSocket(this.socket)
|
||||
if (!character) {
|
||||
this.socket.emit('notification', { title: 'Server message', message: 'Character not found' })
|
||||
// Check if character exists
|
||||
const zoneCharacter = ZoneManager.getCharacter(this.socket.characterId!)
|
||||
if (!zoneCharacter) {
|
||||
gameLogger.error('chat:message error', 'Character not found')
|
||||
return
|
||||
}
|
||||
|
||||
const character = zoneCharacter.character
|
||||
|
||||
// Check if the user is the GM
|
||||
if (character.role !== 'gm') {
|
||||
gameLogger.info(`User ${character.id} tried to set time but is not a game master.`)
|
||||
return
|
||||
}
|
||||
|
||||
@ -54,10 +65,12 @@ export default class TeleportCommandEvent {
|
||||
}
|
||||
|
||||
// Remove character from current zone
|
||||
zoneManager.removeCharacter(character.id)
|
||||
this.io.to(character.zoneId.toString()).emit('zone:character:leave', character.id)
|
||||
this.socket.leave(character.zoneId.toString())
|
||||
|
||||
// Add character to new zone
|
||||
zoneManager.getZoneById(zone.id)?.addCharacter(character)
|
||||
this.io.to(zone.id.toString()).emit('zone:character:join', character)
|
||||
this.socket.join(zone.id.toString())
|
||||
|
||||
@ -65,17 +78,15 @@ export default class TeleportCommandEvent {
|
||||
character.positionX = 0
|
||||
character.positionY = 0
|
||||
|
||||
character.resetMovement = true
|
||||
zoneCharacter.isMoving = false
|
||||
|
||||
this.socket.emit('zone:character:teleport', {
|
||||
zone,
|
||||
characters: CharacterManager.getCharactersInZone(zone)
|
||||
characters: ZoneManager.getZoneById(zone.id)?.getCharactersInZone()
|
||||
})
|
||||
|
||||
this.socket.emit('notification', { title: 'Server message', message: `You have been teleported to ${zone.name}` })
|
||||
gameMasterLogger.info('teleport', `Character ${character.id} teleported to zone ${zone.id}`)
|
||||
|
||||
callback(true)
|
||||
} catch (error: any) {
|
||||
gameMasterLogger.error(`Error in teleport command: ${error.message}`)
|
||||
this.socket.emit('notification', { title: 'Server message', message: 'An error occurred while teleporting' })
|
||||
|
47
src/socketEvents/chat/gameMaster/toggleFogCommand.ts
Normal file
47
src/socketEvents/chat/gameMaster/toggleFogCommand.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../utilities/types'
|
||||
import { isCommand } from '../../../utilities/chat'
|
||||
import CharacterRepository from '../../../repositories/characterRepository'
|
||||
import { gameLogger } from '../../../utilities/logger'
|
||||
import WeatherManager from '../../../managers/weatherManager'
|
||||
|
||||
type TypePayload = {
|
||||
message: string
|
||||
}
|
||||
|
||||
export default class ToggleFogCommand {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('chat:message', this.handleAlertCommand.bind(this))
|
||||
}
|
||||
|
||||
private async handleAlertCommand(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
|
||||
try {
|
||||
if (!isCommand(data.message, 'fog')) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if character exists
|
||||
const character = await CharacterRepository.getByUserAndId(this.socket.userId!, this.socket.characterId!)
|
||||
if (!character) {
|
||||
gameLogger.error('chat:alert_command error', 'Character not found')
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the user is the GM
|
||||
if (character.role !== 'gm') {
|
||||
gameLogger.info(`User ${character.id} tried to set time but is not a game master.`)
|
||||
return
|
||||
}
|
||||
|
||||
await WeatherManager.toggleFog()
|
||||
} catch (error: any) {
|
||||
gameLogger.error('command error', error.message)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
47
src/socketEvents/chat/gameMaster/toggleRainCommand.ts
Normal file
47
src/socketEvents/chat/gameMaster/toggleRainCommand.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../utilities/types'
|
||||
import { isCommand } from '../../../utilities/chat'
|
||||
import CharacterRepository from '../../../repositories/characterRepository'
|
||||
import { gameLogger } from '../../../utilities/logger'
|
||||
import WeatherManager from '../../../managers/weatherManager'
|
||||
|
||||
type TypePayload = {
|
||||
message: string
|
||||
}
|
||||
|
||||
export default class ToggleRainCommand {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('chat:message', this.handleAlertCommand.bind(this))
|
||||
}
|
||||
|
||||
private async handleAlertCommand(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
|
||||
try {
|
||||
if (!isCommand(data.message, 'rain')) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if character exists
|
||||
const character = await CharacterRepository.getByUserAndId(this.socket.userId!, this.socket.characterId!)
|
||||
if (!character) {
|
||||
gameLogger.error('chat:alert_command error', 'Character not found')
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the user is the GM
|
||||
if (character.role !== 'gm') {
|
||||
gameLogger.info(`User ${character.id} tried to set time but is not a game master.`)
|
||||
return
|
||||
}
|
||||
|
||||
await WeatherManager.toggleRain()
|
||||
} catch (error: any) {
|
||||
gameLogger.error('command error', error.message)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
54
src/socketEvents/chat/message.ts
Normal file
54
src/socketEvents/chat/message.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../utilities/types'
|
||||
import ZoneRepository from '../../repositories/zoneRepository'
|
||||
import { isCommand } from '../../utilities/chat'
|
||||
import { gameLogger } from '../../utilities/logger'
|
||||
import ZoneManager from '../../managers/zoneManager'
|
||||
import ChatService from '../../services/chatService'
|
||||
|
||||
type TypePayload = {
|
||||
message: string
|
||||
}
|
||||
|
||||
export default class ChatMessageEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('chat:message', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
|
||||
try {
|
||||
if (!data.message || isCommand(data.message)) {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
const zoneCharacter = ZoneManager.getCharacter(this.socket.characterId!)
|
||||
if (!zoneCharacter) {
|
||||
gameLogger.error('chat:message error', 'Character not found')
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
const character = zoneCharacter.character
|
||||
|
||||
const zone = await ZoneRepository.getById(character.zoneId)
|
||||
if (!zone) {
|
||||
gameLogger.error('chat:message error', 'Zone not found')
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
const chatService = new ChatService()
|
||||
if (await chatService.sendZoneMessage(this.io, this.socket, data.message, character.id, zone.id)) {
|
||||
return callback(true)
|
||||
}
|
||||
|
||||
callback(false)
|
||||
} catch (error: any) {
|
||||
gameLogger.error('chat:message error', error.message)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../utilities/types'
|
||||
import ZoneRepository from '../../repositories/zoneRepository'
|
||||
import { isCommand } from '../../utilities/chat'
|
||||
import { gameLogger } from '../../utilities/logger'
|
||||
import CharacterManager from '../../managers/characterManager'
|
||||
|
||||
type TypePayload = {
|
||||
message: string
|
||||
}
|
||||
|
||||
export default class ChatMessageEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('chat:send_message', this.handleChatMessage.bind(this))
|
||||
}
|
||||
|
||||
private async handleChatMessage(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
|
||||
try {
|
||||
if (!data.message || isCommand(data.message)) {
|
||||
callback(false)
|
||||
return
|
||||
}
|
||||
|
||||
const character = CharacterManager.getCharacterFromSocket(this.socket)
|
||||
if (!character) {
|
||||
gameLogger.error('chat:send_message error', 'Character not found')
|
||||
callback(false)
|
||||
return
|
||||
}
|
||||
|
||||
const zone = await ZoneRepository.getById(character.zoneId)
|
||||
if (!zone) {
|
||||
gameLogger.error('chat:send_message error', 'Zone not found')
|
||||
callback(false)
|
||||
return
|
||||
}
|
||||
|
||||
callback(true)
|
||||
|
||||
this.io.to(zone.id.toString()).emit('chat:message', {
|
||||
character: character,
|
||||
message: data.message
|
||||
})
|
||||
} catch (error: any) {
|
||||
gameLogger.error('chat:send_message error', error.message)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../utilities/types'
|
||||
import CharacterManager from '../managers/characterManager'
|
||||
import { gameLogger } from '../utilities/logger'
|
||||
import ZoneManager from '../managers/zoneManager'
|
||||
|
||||
export default class DisconnectEvent {
|
||||
constructor(
|
||||
@ -10,31 +10,34 @@ export default class DisconnectEvent {
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('disconnect', this.handleDisconnect.bind(this))
|
||||
this.socket.on('disconnect', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleDisconnect(data: any): Promise<void> {
|
||||
private async handleEvent(data: any): Promise<void> {
|
||||
try {
|
||||
if (!this.socket.user) {
|
||||
if (!this.socket.userId) {
|
||||
gameLogger.info('User disconnected but had no user set')
|
||||
return
|
||||
}
|
||||
|
||||
this.io.emit('user:disconnect', this.socket.user.id)
|
||||
this.io.emit('user:disconnect', this.socket.userId)
|
||||
|
||||
const character = CharacterManager.getCharacterFromSocket(this.socket)
|
||||
|
||||
if (!character) {
|
||||
const zoneCharacter = ZoneManager.getCharacter(this.socket.characterId!)
|
||||
if (!zoneCharacter) {
|
||||
gameLogger.info('User disconnected but had no character set')
|
||||
return
|
||||
}
|
||||
|
||||
character.resetMovement = true
|
||||
const character = zoneCharacter.character
|
||||
|
||||
// Save character position and remove from zone
|
||||
zoneCharacter.isMoving = false
|
||||
await zoneCharacter.savePosition()
|
||||
ZoneManager.removeCharacter(this.socket.characterId!)
|
||||
|
||||
gameLogger.info('User disconnected along with their character')
|
||||
|
||||
await CharacterManager.removeCharacter(character)
|
||||
|
||||
// Inform other clients that the character has left
|
||||
this.io.in(character.zoneId.toString()).emit('zone:character:leave', character.id)
|
||||
this.io.emit('character:disconnect', character.id)
|
||||
} catch (error: any) {
|
||||
|
@ -0,0 +1,37 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import prisma from '../../../../utilities/prisma'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
|
||||
export default class CharacterHairCreateEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:characterHair:create', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: undefined, callback: (response: boolean, characterType?: any) => void): Promise<void> {
|
||||
try {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
const newCharacterHair = await prisma.characterHair.create({
|
||||
data: {
|
||||
name: 'New hair'
|
||||
}
|
||||
})
|
||||
|
||||
callback(true, newCharacterHair)
|
||||
} catch (error) {
|
||||
console.error('Error creating character hair:', error)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import prisma from '../../../../utilities/prisma'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
import { gameMasterLogger } from '../../../../utilities/logger'
|
||||
|
||||
interface IPayload {
|
||||
id: number
|
||||
}
|
||||
|
||||
export default class characterHairDeleteEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:characterHair:remove', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: IPayload, callback: (response: boolean) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.characterHair.delete({
|
||||
where: { id: data.id }
|
||||
})
|
||||
|
||||
callback(true)
|
||||
} catch (error) {
|
||||
gameMasterLogger.error(`Error deleting character type ${data.id}: ${error instanceof Error ? error.message : String(error)}`)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import { CharacterHair } from '@prisma/client'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
import { gameMasterLogger } from '../../../../utilities/logger'
|
||||
import characterHairRepository from '../../../../repositories/characterHairRepository'
|
||||
|
||||
interface IPayload {}
|
||||
|
||||
export default class characterHairListEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:characterHair:list', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: IPayload, callback: (response: CharacterHair[]) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) {
|
||||
gameMasterLogger.error('gm:characterHair:list error', 'Character not found')
|
||||
return callback([])
|
||||
}
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
gameMasterLogger.info(`User ${character.id} tried to list character hair but is not a game master.`)
|
||||
return callback([])
|
||||
}
|
||||
|
||||
// get all objects
|
||||
const items = await characterHairRepository.getAll()
|
||||
callback(items)
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import prisma from '../../../../utilities/prisma'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
import { CharacterGender, CharacterRace } from '@prisma/client'
|
||||
import { gameMasterLogger } from '../../../../utilities/logger'
|
||||
|
||||
type Payload = {
|
||||
id: number
|
||||
name: string
|
||||
gender: CharacterGender
|
||||
isEnabledForCharCreation: boolean
|
||||
spriteId: string
|
||||
}
|
||||
|
||||
export default class CharacterHairUpdateEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:characterHair:update', this.handleObjectUpdate.bind(this))
|
||||
}
|
||||
|
||||
private async handleObjectUpdate(data: Payload, callback: (success: boolean) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.characterHair.update({
|
||||
where: { id: data.id },
|
||||
data: {
|
||||
name: data.name,
|
||||
gender: data.gender,
|
||||
isEnabledForCharCreation: data.isEnabledForCharCreation,
|
||||
spriteId: data.spriteId
|
||||
}
|
||||
})
|
||||
|
||||
return callback(true)
|
||||
} catch (error) {
|
||||
gameMasterLogger.error(`Error updating character hair: ${error instanceof Error ? error.message : String(error)}`)
|
||||
return callback(false)
|
||||
}
|
||||
}
|
||||
}
|
@ -11,10 +11,10 @@ export default class CharacterTypeCreateEvent {
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:characterType:create', this.handleCharacterTypeCreate.bind(this))
|
||||
this.socket.on('gm:characterType:create', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleCharacterTypeCreate(data: undefined, callback: (response: boolean, characterType?: any) => void): Promise<void> {
|
||||
private async handleEvent(data: undefined, callback: (response: boolean, characterType?: any) => void): Promise<void> {
|
||||
try {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
|
@ -0,0 +1,40 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import prisma from '../../../../utilities/prisma'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
import { gameMasterLogger } from '../../../../utilities/logger'
|
||||
|
||||
interface IPayload {
|
||||
id: number
|
||||
}
|
||||
|
||||
export default class CharacterTypeDeleteEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:characterType:remove', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: IPayload, callback: (response: boolean) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.characterType.delete({
|
||||
where: { id: data.id }
|
||||
})
|
||||
|
||||
callback(true)
|
||||
} catch (error) {
|
||||
gameMasterLogger.error(`Error deleting character type ${data.id}: ${error instanceof Error ? error.message : String(error)}`)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
@ -14,10 +14,10 @@ export default class CharacterTypeListEvent {
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:characterType:list', this.handleCharacterTypeList.bind(this))
|
||||
this.socket.on('gm:characterType:list', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleCharacterTypeList(data: IPayload, callback: (response: CharacterType[]) => void): Promise<void> {
|
||||
private async handleEvent(data: IPayload, callback: (response: CharacterType[]) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) {
|
||||
gameMasterLogger.error('gm:characterType:list error', 'Character not found')
|
||||
|
@ -1,56 +0,0 @@
|
||||
import fs from 'fs'
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import prisma from '../../../../utilities/prisma'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
import { getPublicPath } from '../../../../utilities/storage'
|
||||
|
||||
interface IPayload {
|
||||
object: string
|
||||
}
|
||||
|
||||
export default class ObjectRemoveEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:object:remove', this.handleObjectRemove.bind(this))
|
||||
}
|
||||
|
||||
private async handleObjectRemove(data: IPayload, callback: (response: boolean) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.object.delete({
|
||||
where: {
|
||||
id: data.object
|
||||
}
|
||||
})
|
||||
|
||||
// get root path
|
||||
const public_folder = getPublicPath('objects')
|
||||
|
||||
// remove the tile from the disk
|
||||
const finalFilePath = getPublicPath('objects', data.object + '.png')
|
||||
fs.unlink(finalFilePath, (err) => {
|
||||
if (err) {
|
||||
console.log(err)
|
||||
callback(false)
|
||||
return
|
||||
}
|
||||
|
||||
callback(true)
|
||||
})
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
@ -2,27 +2,25 @@ import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import prisma from '../../../../utilities/prisma'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
import { CharacterGender, CharacterRace } from '@prisma/client'
|
||||
|
||||
type Payload = {
|
||||
id: string
|
||||
id: number
|
||||
name: string
|
||||
tags: string[]
|
||||
originX: number
|
||||
originY: number
|
||||
isAnimated: boolean
|
||||
frameSpeed: number
|
||||
frameWidth: number
|
||||
frameHeight: number
|
||||
gender: CharacterGender
|
||||
race: CharacterRace
|
||||
isEnabledForCharCreation: boolean
|
||||
spriteId: string
|
||||
}
|
||||
|
||||
export default class ObjectUpdateEvent {
|
||||
export default class CharacterTypeUpdateEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:object:update', this.handleObjectUpdate.bind(this))
|
||||
this.socket.on('gm:characterType:update', this.handleObjectUpdate.bind(this))
|
||||
}
|
||||
|
||||
private async handleObjectUpdate(data: Payload, callback: (success: boolean) => void): Promise<void> {
|
||||
@ -34,21 +32,17 @@ export default class ObjectUpdateEvent {
|
||||
}
|
||||
|
||||
try {
|
||||
const object = await prisma.object.update({
|
||||
where: {
|
||||
id: data.id
|
||||
},
|
||||
await prisma.characterType.update({
|
||||
where: { id: data.id },
|
||||
data: {
|
||||
name: data.name,
|
||||
tags: data.tags,
|
||||
originX: data.originX,
|
||||
originY: data.originY,
|
||||
isAnimated: data.isAnimated,
|
||||
frameSpeed: data.frameSpeed,
|
||||
frameWidth: data.frameWidth,
|
||||
frameHeight: data.frameHeight
|
||||
gender: data.gender,
|
||||
race: data.race,
|
||||
isEnabledForCharCreation: data.isEnabledForCharCreation,
|
||||
spriteId: data.spriteId
|
||||
}
|
||||
})
|
||||
|
||||
callback(true)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
@ -4,6 +4,7 @@ import { TSocket } from '../../../../utilities/types'
|
||||
import prisma from '../../../../utilities/prisma'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
import { getPublicPath } from '../../../../utilities/storage'
|
||||
import { gameLogger, gameMasterLogger } from '../../../../utilities/logger'
|
||||
|
||||
interface IPayload {
|
||||
object: string
|
||||
@ -41,15 +42,15 @@ export default class ObjectRemoveEvent {
|
||||
const finalFilePath = getPublicPath('objects', data.object + '.png')
|
||||
fs.unlink(finalFilePath, (err) => {
|
||||
if (err) {
|
||||
console.log(err)
|
||||
gameMasterLogger.error(`Error deleting object ${data.object}: ${err.message}`)
|
||||
callback(false)
|
||||
return
|
||||
}
|
||||
|
||||
callback(true)
|
||||
})
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} catch (error) {
|
||||
gameLogger.error(`Error deleting object ${data.object}: ${error instanceof Error ? error.message : String(error)}`)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ export default class SpriteCreateEvent {
|
||||
|
||||
private async handleSpriteCreate(data: undefined, callback: (response: boolean) => void): Promise<void> {
|
||||
try {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
const character = await characterRepository.getById(this.socket.characterId!)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
|
@ -2,9 +2,9 @@ import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import fs from 'fs'
|
||||
import prisma from '../../../../utilities/prisma'
|
||||
import CharacterManager from '../../../../managers/characterManager'
|
||||
import { gameMasterLogger } from '../../../../utilities/logger'
|
||||
import { getPublicPath } from '../../../../utilities/storage'
|
||||
import CharacterRepository from '../../../../repositories/characterRepository'
|
||||
|
||||
type Payload = {
|
||||
id: string
|
||||
@ -21,11 +21,11 @@ export default class GMSpriteDeleteEvent {
|
||||
}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:sprite:delete', this.handleSpriteDelete.bind(this))
|
||||
this.socket.on('gm:sprite:delete', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleSpriteDelete(data: Payload, callback: (response: boolean) => void): Promise<void> {
|
||||
const character = CharacterManager.getCharacterFromSocket(this.socket)
|
||||
private async handleEvent(data: Payload, callback: (response: boolean) => void): Promise<void> {
|
||||
const character = await CharacterRepository.getById(this.socket.characterId!)
|
||||
if (character?.role !== 'gm') {
|
||||
return callback(false)
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ export default class SpriteListEvent {
|
||||
}
|
||||
|
||||
private async handleSpriteList(data: any, callback: (response: Sprite[]) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
const character = await characterRepository.getById(this.socket.characterId!)
|
||||
if (!character) return callback([])
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
|
@ -4,14 +4,38 @@ import prisma from '../../../../utilities/prisma'
|
||||
import type { Prisma, SpriteAction } from '@prisma/client'
|
||||
import { writeFile, mkdir } from 'node:fs/promises'
|
||||
import sharp from 'sharp'
|
||||
import CharacterManager from '../../../../managers/characterManager'
|
||||
import { getPublicPath } from '../../../../utilities/storage'
|
||||
import CharacterRepository from '../../../../repositories/characterRepository'
|
||||
import { gameMasterLogger } from '../../../../utilities/logger'
|
||||
|
||||
type SpriteActionInput = Omit<SpriteAction, 'id' | 'spriteId' | 'frameWidth' | 'frameHeight'> & {
|
||||
// Constants
|
||||
const ISOMETRIC_CONFIG = {
|
||||
tileWidth: 64,
|
||||
tileHeight: 32,
|
||||
centerOffset: 32,
|
||||
bodyRatios: {
|
||||
topStart: 0.15,
|
||||
topEnd: 0.45,
|
||||
weightUpper: 0.7,
|
||||
weightLower: 0.3
|
||||
}
|
||||
} as const
|
||||
|
||||
// Types
|
||||
interface ContentBounds {
|
||||
left: number
|
||||
right: number
|
||||
top: number
|
||||
bottom: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
interface SpriteActionInput extends Omit<SpriteAction, 'id' | 'spriteId' | 'frameWidth' | 'frameHeight'> {
|
||||
sprites: string[]
|
||||
}
|
||||
|
||||
type Payload = {
|
||||
interface UpdatePayload {
|
||||
id: string
|
||||
name: string
|
||||
spriteActions: Prisma.JsonValue
|
||||
@ -20,11 +44,19 @@ type Payload = {
|
||||
interface ProcessedSpriteAction extends SpriteActionInput {
|
||||
frameWidth: number
|
||||
frameHeight: number
|
||||
buffersWithDimensions: Array<{
|
||||
buffer: Buffer
|
||||
width: number | undefined
|
||||
height: number | undefined
|
||||
}>
|
||||
buffersWithDimensions: ProcessedFrame[]
|
||||
}
|
||||
|
||||
interface ProcessedFrame {
|
||||
buffer: Buffer
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
interface SpriteAnalysis {
|
||||
massCenter: number
|
||||
spinePosition: number
|
||||
contentBounds: ContentBounds
|
||||
}
|
||||
|
||||
export default class SpriteUpdateEvent {
|
||||
@ -37,119 +69,322 @@ export default class SpriteUpdateEvent {
|
||||
this.socket.on('gm:sprite:update', this.handleSpriteUpdate.bind(this))
|
||||
}
|
||||
|
||||
private async handleSpriteUpdate(data: Payload, callback: (success: boolean) => void): Promise<void> {
|
||||
const character = CharacterManager.getCharacterFromSocket(this.socket)
|
||||
if (character?.role !== 'gm') {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
private async handleSpriteUpdate(payload: UpdatePayload, callback: (success: boolean) => void): Promise<void> {
|
||||
try {
|
||||
const parsedSpriteActions = validateSpriteActions(data.spriteActions)
|
||||
const processedActions = await processSprites(parsedSpriteActions)
|
||||
if (!await this.validateGameMasterAccess()) {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
await updateDatabase(data.id, data.name, processedActions)
|
||||
await saveSpritesToDisk(data.id, processedActions)
|
||||
const parsedActions = this.validateSpriteActions(payload.spriteActions)
|
||||
const processedActions = await this.processSprites(parsedActions)
|
||||
|
||||
await Promise.all([
|
||||
this.updateDatabase(payload.id, payload.name, processedActions),
|
||||
this.saveSpritesToDisk(payload.id, processedActions)
|
||||
])
|
||||
|
||||
callback(true)
|
||||
} catch (error) {
|
||||
console.error('Error updating sprite:', error)
|
||||
callback(false)
|
||||
this.handleError(error, payload.id, callback)
|
||||
}
|
||||
}
|
||||
|
||||
function validateSpriteActions(spriteActions: Prisma.JsonValue): SpriteActionInput[] {
|
||||
try {
|
||||
const parsed = JSON.parse(JSON.stringify(spriteActions)) as SpriteActionInput[]
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new Error('spriteActions is not an array')
|
||||
private async validateGameMasterAccess(): Promise<boolean> {
|
||||
const character = await CharacterRepository.getById(this.socket.characterId!)
|
||||
return character?.role === 'gm'
|
||||
}
|
||||
|
||||
private validateSpriteActions(actions: Prisma.JsonValue): SpriteActionInput[] {
|
||||
try {
|
||||
const parsed = JSON.parse(JSON.stringify(actions)) as SpriteActionInput[]
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new Error('Sprite actions must be an array')
|
||||
}
|
||||
return parsed
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid sprite actions format: ${this.getErrorMessage(error)}`)
|
||||
}
|
||||
}
|
||||
|
||||
private async processSprites(actions: SpriteActionInput[]): Promise<ProcessedSpriteAction[]> {
|
||||
return Promise.all(actions.map(async (action) => {
|
||||
const spriteBuffers = await this.convertBase64ToBuffers(action.sprites)
|
||||
const frameWidth = ISOMETRIC_CONFIG.tileWidth
|
||||
const frameHeight = await this.calculateOptimalHeight(spriteBuffers)
|
||||
const processedFrames = await this.normalizeFrames(spriteBuffers, frameWidth, frameHeight)
|
||||
|
||||
return {
|
||||
...action,
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
buffersWithDimensions: processedFrames
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
private async convertBase64ToBuffers(sprites: string[]): Promise<Buffer[]> {
|
||||
return sprites.map(sprite => Buffer.from(sprite.split(',')[1], 'base64'))
|
||||
}
|
||||
|
||||
private async normalizeFrames(buffers: Buffer[], frameWidth: number, frameHeight: number): Promise<ProcessedFrame[]> {
|
||||
return Promise.all(
|
||||
buffers.map(async (buffer) => {
|
||||
const normalizedBuffer = await this.normalizeIsometricSprite(buffer, frameWidth, frameHeight)
|
||||
return {
|
||||
buffer: normalizedBuffer,
|
||||
width: frameWidth,
|
||||
height: frameHeight
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
private async calculateOptimalHeight(buffers: Buffer[]): Promise<number> {
|
||||
const heights = await Promise.all(
|
||||
buffers.map(async buffer => {
|
||||
const bounds = await this.findContentBounds(buffer)
|
||||
return bounds.height
|
||||
})
|
||||
)
|
||||
return Math.ceil(Math.max(...heights) / 2) * 2
|
||||
}
|
||||
|
||||
private async normalizeIsometricSprite(buffer: Buffer, frameWidth: number, frameHeight: number): Promise<Buffer> {
|
||||
const analysis = await this.analyzeIsometricSprite(buffer)
|
||||
const idealCenter = Math.floor(frameWidth / 2)
|
||||
const offset = Math.round(idealCenter - analysis.massCenter)
|
||||
|
||||
// Process the input sprite
|
||||
const processedInput = await sharp(buffer)
|
||||
.ensureAlpha()
|
||||
.resize({
|
||||
kernel: sharp.kernel.nearest,
|
||||
fit: 'contain',
|
||||
position: 'center'
|
||||
})
|
||||
.png({
|
||||
compressionLevel: 9,
|
||||
adaptiveFiltering: false,
|
||||
palette: true,
|
||||
quality: 100,
|
||||
colors: 256
|
||||
})
|
||||
.toBuffer()
|
||||
|
||||
// Create the final composition
|
||||
return sharp({
|
||||
create: {
|
||||
width: frameWidth,
|
||||
height: frameHeight,
|
||||
channels: 4,
|
||||
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
||||
}
|
||||
})
|
||||
.composite([{
|
||||
input: processedInput,
|
||||
left: offset,
|
||||
top: 0,
|
||||
blend: 'over'
|
||||
}])
|
||||
.png({
|
||||
compressionLevel: 9,
|
||||
adaptiveFiltering: false,
|
||||
palette: true,
|
||||
quality: 100,
|
||||
colors: 256
|
||||
})
|
||||
.toBuffer()
|
||||
}
|
||||
|
||||
private async analyzeIsometricSprite(buffer: Buffer): Promise<SpriteAnalysis> {
|
||||
const { data, info } = await sharp(buffer).raw().ensureAlpha().toBuffer({ resolveWithObject: true })
|
||||
const { width, height } = info
|
||||
const upperStart = Math.floor(height * ISOMETRIC_CONFIG.bodyRatios.topStart)
|
||||
const upperEnd = Math.floor(height * ISOMETRIC_CONFIG.bodyRatios.topEnd)
|
||||
|
||||
const { columnDensity, upperBodyDensity, bounds } = this.calculatePixelDistribution(data, width, height, upperStart, upperEnd)
|
||||
const spinePosition = this.findSpinePosition(upperBodyDensity)
|
||||
const massCenter = this.calculateWeightedMassCenter(columnDensity, upperBodyDensity)
|
||||
|
||||
return {
|
||||
massCenter,
|
||||
spinePosition,
|
||||
contentBounds: bounds
|
||||
}
|
||||
}
|
||||
|
||||
private calculatePixelDistribution(data: Buffer, width: number, height: number, upperStart: number, upperEnd: number) {
|
||||
const columnDensity = new Array(width).fill(0)
|
||||
const upperBodyDensity = new Array(width).fill(0)
|
||||
const bounds = { left: width, right: 0, top: height, bottom: 0 }
|
||||
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
if (data[(y * width + x) * 4 + 3] > 0) {
|
||||
columnDensity[x]++
|
||||
if (y >= upperStart && y <= upperEnd) {
|
||||
upperBodyDensity[x]++
|
||||
}
|
||||
this.updateBounds(bounds, x, y)
|
||||
}
|
||||
return parsed
|
||||
} catch (error) {
|
||||
console.error('Error parsing spriteActions:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function processSprites(spriteActions: SpriteActionInput[]): Promise<ProcessedSpriteAction[]> {
|
||||
return Promise.all(
|
||||
spriteActions.map(async (spriteAction) => {
|
||||
const { action, sprites } = spriteAction
|
||||
|
||||
if (!Array.isArray(sprites) || sprites.length === 0) {
|
||||
throw new Error(`Invalid sprites array for action: ${action}`)
|
||||
}
|
||||
|
||||
const buffersWithDimensions = await Promise.all(
|
||||
sprites.map(async (sprite: string) => {
|
||||
const buffer = Buffer.from(sprite.split(',')[1], 'base64')
|
||||
const { width, height } = await sharp(buffer).metadata()
|
||||
return { buffer, width, height }
|
||||
})
|
||||
)
|
||||
|
||||
const frameWidth = Math.max(...buffersWithDimensions.map((b) => b.width || 0))
|
||||
const frameHeight = Math.max(...buffersWithDimensions.map((b) => b.height || 0))
|
||||
|
||||
return {
|
||||
...spriteAction,
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
buffersWithDimensions
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
async function updateDatabase(id: string, name: string, processedActions: ProcessedSpriteAction[]) {
|
||||
await prisma.sprite.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name,
|
||||
spriteActions: {
|
||||
deleteMany: { spriteId: id },
|
||||
create: processedActions.map(({ action, sprites, originX, originY, isAnimated, isLooping, frameWidth, frameHeight, frameSpeed }) => ({
|
||||
action,
|
||||
sprites,
|
||||
originX,
|
||||
originY,
|
||||
isAnimated,
|
||||
isLooping,
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
frameSpeed
|
||||
}))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function saveSpritesToDisk(id: string, processedActions: ProcessedSpriteAction[]) {
|
||||
const publicFolder = getPublicPath('sprites', id)
|
||||
await mkdir(publicFolder, { recursive: true })
|
||||
|
||||
await Promise.all(
|
||||
processedActions.map(async ({ action, buffersWithDimensions, frameWidth, frameHeight }) => {
|
||||
const combinedImage = await sharp({
|
||||
create: {
|
||||
width: frameWidth * buffersWithDimensions.length,
|
||||
height: frameHeight,
|
||||
channels: 4,
|
||||
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
||||
}
|
||||
})
|
||||
.composite(
|
||||
buffersWithDimensions.map(({ buffer }, index) => ({
|
||||
input: buffer,
|
||||
left: index * frameWidth,
|
||||
top: 0
|
||||
}))
|
||||
)
|
||||
.png()
|
||||
.toBuffer()
|
||||
|
||||
const filename = getPublicPath('sprites', id, `${action}.png`)
|
||||
await writeFile(filename, combinedImage)
|
||||
})
|
||||
)
|
||||
return {
|
||||
columnDensity,
|
||||
upperBodyDensity,
|
||||
bounds: {
|
||||
...bounds,
|
||||
width: bounds.right - bounds.left + 1,
|
||||
height: bounds.bottom - bounds.top + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateBounds(bounds: { left: number; right: number; top: number; bottom: number }, x: number, y: number): void {
|
||||
bounds.left = Math.min(bounds.left, x)
|
||||
bounds.right = Math.max(bounds.right, x)
|
||||
bounds.top = Math.min(bounds.top, y)
|
||||
bounds.bottom = Math.max(bounds.bottom, y)
|
||||
}
|
||||
|
||||
private findSpinePosition(density: number[]): number {
|
||||
return density.reduce((maxIdx, curr, idx, arr) => curr > arr[maxIdx] ? idx : maxIdx, 0)
|
||||
}
|
||||
|
||||
private calculateWeightedMassCenter(columnDensity: number[], upperBodyDensity: number[]): number {
|
||||
const upperMassCenter = this.calculateMassCenter(upperBodyDensity)
|
||||
const lowerMassCenter = this.calculateMassCenter(columnDensity)
|
||||
|
||||
return Math.round(
|
||||
upperMassCenter * ISOMETRIC_CONFIG.bodyRatios.weightUpper +
|
||||
lowerMassCenter * ISOMETRIC_CONFIG.bodyRatios.weightLower
|
||||
)
|
||||
}
|
||||
|
||||
private calculateMassCenter(density: number[]): number {
|
||||
const totalMass = density.reduce((sum, mass) => sum + mass, 0)
|
||||
if (!totalMass) return 0
|
||||
|
||||
const weightedSum = density.reduce((sum, mass, position) => sum + position * mass, 0)
|
||||
return Math.round(weightedSum / totalMass)
|
||||
}
|
||||
|
||||
private async findContentBounds(buffer: Buffer) {
|
||||
const { data, info } = await sharp(buffer)
|
||||
.raw()
|
||||
.ensureAlpha()
|
||||
.toBuffer({ resolveWithObject: true })
|
||||
|
||||
const width = info.width
|
||||
const height = info.height
|
||||
|
||||
let left = width
|
||||
let right = 0
|
||||
let top = height
|
||||
let bottom = 0
|
||||
|
||||
// Find actual content boundaries by checking alpha channel
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const idx = (y * width + x) * 4
|
||||
if (data[idx + 3] > 0) { // If pixel is not transparent
|
||||
left = Math.min(left, x)
|
||||
right = Math.max(right, x)
|
||||
top = Math.min(top, y)
|
||||
bottom = Math.max(bottom, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
width: right - left + 1,
|
||||
height: bottom - top + 1,
|
||||
leftOffset: left,
|
||||
topOffset: top
|
||||
}
|
||||
}
|
||||
|
||||
private async saveSpritesToDisk(id: string, actions: ProcessedSpriteAction[]): Promise<void> {
|
||||
const publicFolder = getPublicPath('sprites', id)
|
||||
await mkdir(publicFolder, { recursive: true })
|
||||
|
||||
await Promise.all(actions.map(async (action) => {
|
||||
const spritesheet = await this.createSpritesheet(action.buffersWithDimensions)
|
||||
await writeFile(getPublicPath('sprites', id, `${action.action}.png`), spritesheet)
|
||||
}))
|
||||
}
|
||||
|
||||
private async createSpritesheet(frames: ProcessedFrame[]): Promise<Buffer> {
|
||||
const background = await sharp({
|
||||
create: {
|
||||
width: ISOMETRIC_CONFIG.tileWidth * frames.length,
|
||||
height: frames[0].height,
|
||||
channels: 4,
|
||||
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
||||
}
|
||||
})
|
||||
.png({
|
||||
compressionLevel: 9,
|
||||
adaptiveFiltering: false,
|
||||
palette: true,
|
||||
quality: 100,
|
||||
colors: 256,
|
||||
dither: 0
|
||||
})
|
||||
.toBuffer()
|
||||
|
||||
return sharp(background)
|
||||
.composite(frames.map((frame, index) => ({
|
||||
input: frame.buffer,
|
||||
left: index * ISOMETRIC_CONFIG.tileWidth,
|
||||
top: 0,
|
||||
blend: 'over'
|
||||
})))
|
||||
.png({
|
||||
compressionLevel: 9,
|
||||
adaptiveFiltering: false,
|
||||
palette: true,
|
||||
quality: 100,
|
||||
colors: 256,
|
||||
dither: 0
|
||||
})
|
||||
.toBuffer()
|
||||
}
|
||||
|
||||
private async updateDatabase(id: string, name: string, actions: ProcessedSpriteAction[]): Promise<void> {
|
||||
await prisma.sprite.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name,
|
||||
spriteActions: {
|
||||
deleteMany: { spriteId: id },
|
||||
create: actions.map(this.mapActionToDatabase)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private mapActionToDatabase(action: ProcessedSpriteAction) {
|
||||
return {
|
||||
action: action.action,
|
||||
sprites: action.sprites,
|
||||
originX: action.originX,
|
||||
originY: action.originY,
|
||||
isAnimated: action.isAnimated,
|
||||
isLooping: action.isLooping,
|
||||
frameWidth: action.frameWidth,
|
||||
frameHeight: action.frameHeight,
|
||||
frameSpeed: action.frameSpeed
|
||||
}
|
||||
}
|
||||
|
||||
private handleError(error: unknown, spriteId: string, callback: (success: boolean) => void): void {
|
||||
gameMasterLogger.error(`Error updating sprite ${spriteId}: ${this.getErrorMessage(error)}`)
|
||||
callback(false)
|
||||
}
|
||||
|
||||
private getErrorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
}
|
@ -39,40 +39,52 @@ export default class ZoneUpdateEvent {
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:zone_editor:zone:update', this.handleZoneUpdate.bind(this))
|
||||
this.socket.on('gm:zone_editor:zone:update', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleZoneUpdate(data: IPayload, callback: (response: Zone | null) => void): Promise<void> {
|
||||
private async handleEvent(data: IPayload, callback: (response: Zone | null) => void): Promise<void> {
|
||||
try {
|
||||
const character = await CharacterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) {
|
||||
gameMasterLogger.error('gm:zone_editor:zone:update error', 'Character not found')
|
||||
callback(null)
|
||||
return
|
||||
return callback(null)
|
||||
}
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
gameMasterLogger.info(`User ${character.id} tried to update zone but is not a game master.`)
|
||||
callback(null)
|
||||
return
|
||||
return callback(null)
|
||||
}
|
||||
|
||||
gameMasterLogger.info(`User ${character.id} has updated zone via zone editor.`)
|
||||
|
||||
if (!data.zoneId) {
|
||||
gameMasterLogger.info(`User ${character.id} tried to update zone but did not provide a zone id.`)
|
||||
callback(null)
|
||||
return
|
||||
return callback(null)
|
||||
}
|
||||
|
||||
let zone = await ZoneRepository.getById(data.zoneId)
|
||||
|
||||
if (!zone) {
|
||||
gameMasterLogger.info(`User ${character.id} tried to update zone ${data.zoneId} but it does not exist.`)
|
||||
callback(null)
|
||||
return
|
||||
return callback(null)
|
||||
}
|
||||
|
||||
// If tiles are larger than the zone, remove the extra tiles
|
||||
if (data.tiles.length > data.height) {
|
||||
data.tiles = data.tiles.slice(0, data.height)
|
||||
}
|
||||
for (let i = 0; i < data.tiles.length; i++) {
|
||||
if (data.tiles[i].length > data.width) {
|
||||
data.tiles[i] = data.tiles[i].slice(0, data.width)
|
||||
}
|
||||
}
|
||||
|
||||
// If zone event tiles are placed outside the zone's bounds, remove these
|
||||
data.zoneEventTiles = data.zoneEventTiles.filter((tile) => tile.positionX >= 0 && tile.positionX < data.width && tile.positionY >= 0 && tile.positionY < data.height)
|
||||
|
||||
// If zone objects are placed outside the zone's bounds, remove these
|
||||
data.zoneObjects = data.zoneObjects.filter((obj) => obj.positionX >= 0 && obj.positionX < data.width && obj.positionY >= 0 && obj.positionY < data.height)
|
||||
|
||||
await prisma.zone.update({
|
||||
where: { id: data.zoneId },
|
||||
data: {
|
||||
@ -134,10 +146,13 @@ export default class ZoneUpdateEvent {
|
||||
|
||||
callback(zone)
|
||||
|
||||
/**
|
||||
* @TODO #246: Reload zone for players who are currently in the zone
|
||||
*/
|
||||
zoneManager.unloadZone(data.zoneId)
|
||||
await zoneManager.loadZone(zone)
|
||||
} catch (error: any) {
|
||||
gameMasterLogger.error('gm:zone_editor:zone:update error', error.message)
|
||||
gameMasterLogger.error(`gm:zone_editor:zone:update error: ${error instanceof Error ? error.message : String(error)}`)
|
||||
callback(null)
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../utilities/types'
|
||||
import { gameLogger } from '../utilities/logger'
|
||||
import UserRepository from '../repositories/userRepository'
|
||||
|
||||
export default class LoginEvent {
|
||||
constructor(
|
||||
@ -14,13 +15,13 @@ export default class LoginEvent {
|
||||
|
||||
private handleLogin(): void {
|
||||
try {
|
||||
if (!this.socket.user) {
|
||||
if (!this.socket.userId) {
|
||||
gameLogger.warn('Login attempt without user data')
|
||||
return
|
||||
}
|
||||
|
||||
this.socket.emit('logged_in', { user: this.socket.user })
|
||||
gameLogger.info(`User logged in: ${this.socket.user.id}`)
|
||||
this.socket.emit('logged_in', { user: UserRepository.getById(this.socket.userId) })
|
||||
gameLogger.info(`User logged in: ${this.socket.userId}`)
|
||||
} catch (error: any) {
|
||||
gameLogger.error('login error', error.message)
|
||||
}
|
||||
|
@ -1,14 +1,16 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { ExtendedCharacter, TSocket } from '../../utilities/types'
|
||||
import { TSocket } from '../../utilities/types'
|
||||
import ZoneRepository from '../../repositories/zoneRepository'
|
||||
import { Character, Zone } from '@prisma/client'
|
||||
import CharacterManager from '../../managers/characterManager'
|
||||
import { Zone } from '@prisma/client'
|
||||
import { gameLogger } from '../../utilities/logger'
|
||||
import CharacterRepository from '../../repositories/characterRepository'
|
||||
import ZoneManager from '../../managers/zoneManager'
|
||||
import zoneCharacter from '../../models/zoneCharacter'
|
||||
import zoneManager from '../../managers/zoneManager'
|
||||
|
||||
interface IResponse {
|
||||
zone: Zone
|
||||
characters: Character[]
|
||||
characters: zoneCharacter[]
|
||||
}
|
||||
|
||||
export default class CharacterJoinEvent {
|
||||
@ -28,30 +30,42 @@ export default class CharacterJoinEvent {
|
||||
return
|
||||
}
|
||||
|
||||
const character = await CharacterRepository.getById(this.socket.characterId as number)
|
||||
const character = await CharacterRepository.getById(this.socket.characterId)
|
||||
if (!character) {
|
||||
gameLogger.error('zone:character:join error', 'Character not found')
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* @TODO: If zone is not found, spawn back to the start
|
||||
*/
|
||||
const zone = await ZoneRepository.getById(character.zoneId)
|
||||
if (!zone) {
|
||||
gameLogger.error('zone:character:join error', 'Zone not found')
|
||||
return
|
||||
}
|
||||
|
||||
CharacterManager.initCharacter(character as ExtendedCharacter)
|
||||
/**
|
||||
* @TODO: If zone is not found, spawn back to the start
|
||||
*/
|
||||
const loadedZone = ZoneManager.getZoneById(zone.id)
|
||||
if (!loadedZone) {
|
||||
gameLogger.error('zone:character:join error', 'Loaded zone not found')
|
||||
return
|
||||
}
|
||||
|
||||
loadedZone.addCharacter(character)
|
||||
|
||||
this.socket.join(zone.id.toString())
|
||||
|
||||
// let other clients know of new character
|
||||
this.io.to(zone.id.toString()).emit('zone:character:join', character)
|
||||
// Let other clients know of new character
|
||||
this.io.to(zone.id.toString()).emit('zone:character:join', zoneManager.getCharacter(character.id))
|
||||
|
||||
// Log
|
||||
gameLogger.info(`User ${character.id} joined zone ${zone.id}`)
|
||||
|
||||
// send over zone and characters to socket
|
||||
callback({ zone, characters: CharacterManager.getCharactersInZone(zone) })
|
||||
// Send over zone and characters to socket
|
||||
callback({ zone, characters: loadedZone.getCharactersInZone() })
|
||||
} catch (error: any) {
|
||||
gameLogger.error('zone:character:join error', error.message)
|
||||
this.socket.disconnect()
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../utilities/types'
|
||||
import ZoneRepository from '../../repositories/zoneRepository'
|
||||
import CharacterManager from '../../managers/characterManager'
|
||||
import { gameLogger } from '../../utilities/logger'
|
||||
import ZoneManager from '../../managers/zoneManager'
|
||||
import CharacterRepository from '../../repositories/characterRepository'
|
||||
|
||||
export default class ZoneLeaveEvent {
|
||||
constructor(
|
||||
@ -16,21 +17,29 @@ export default class ZoneLeaveEvent {
|
||||
|
||||
private async handleZoneLeave(): Promise<void> {
|
||||
try {
|
||||
const character = CharacterManager.getCharacterFromSocket(this.socket)
|
||||
if (!this.socket.characterId) {
|
||||
gameLogger.error('zone:character:join error', 'Zone requested but no character id set')
|
||||
return
|
||||
}
|
||||
|
||||
const character = await CharacterRepository.getById(this.socket.characterId)
|
||||
if (!character) {
|
||||
gameLogger.error('zone:character:leave error', 'Character not found')
|
||||
return
|
||||
}
|
||||
|
||||
if (!character.zoneId) {
|
||||
gameLogger.error('zone:character:leave error', 'Character not in a zone')
|
||||
gameLogger.error('zone:character:join error', 'Character not found')
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* @TODO: If zone is not found, spawn back to the start
|
||||
*/
|
||||
const zone = await ZoneRepository.getById(character.zoneId)
|
||||
|
||||
if (!zone) {
|
||||
gameLogger.error('zone:character:leave error', 'Zone not found')
|
||||
gameLogger.error('zone:character:join error', 'Zone not found')
|
||||
return
|
||||
}
|
||||
|
||||
const loadedZone = ZoneManager.getZoneById(zone.id)
|
||||
if (!loadedZone) {
|
||||
gameLogger.error('zone:character:join error', 'Loaded zone not found')
|
||||
return
|
||||
}
|
||||
|
||||
@ -40,7 +49,7 @@ export default class ZoneLeaveEvent {
|
||||
this.io.to(zone.id.toString()).emit('zone:character:leave', character.id)
|
||||
|
||||
// remove character from zone manager
|
||||
await CharacterManager.removeCharacter(character)
|
||||
await loadedZone.removeCharacter(character.id)
|
||||
|
||||
gameLogger.info('zone:character:leave', `Character ${character.id} left zone ${zone.id}`)
|
||||
} catch (error: any) {
|
||||
|
@ -1,144 +1,96 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket, ExtendedCharacter } from '../../utilities/types'
|
||||
import { CharacterMoveService } from '../../services/character/characterMoveService'
|
||||
import { TSocket, ZoneEventTileWithTeleport } from '../../utilities/types'
|
||||
import { CharacterService } from '../../services/characterService'
|
||||
import { ZoneEventTileService } from '../../services/zoneEventTileService'
|
||||
import prisma from '../../utilities/prisma'
|
||||
import { ZoneEventTile, ZoneEventTileTeleport } from '@prisma/client'
|
||||
import Rotation from '../../utilities/character/rotation'
|
||||
import CharacterManager from '../../managers/characterManager'
|
||||
import { gameLogger } from '../../utilities/logger'
|
||||
import QueueManager from '../../managers/queueManager'
|
||||
|
||||
export type ZoneEventTileWithTeleport = ZoneEventTile & {
|
||||
teleport: ZoneEventTileTeleport
|
||||
}
|
||||
import ZoneManager from '../../managers/zoneManager'
|
||||
import ZoneCharacter from '../../models/zoneCharacter'
|
||||
import zoneEventTileRepository from '../../repositories/zoneEventTileRepository'
|
||||
|
||||
export default class CharacterMove {
|
||||
private characterMoveService: CharacterMoveService
|
||||
private zoneEventTileService: ZoneEventTileService
|
||||
private nextPath: { [index: number]: { x: number; y: number }[] } = []
|
||||
private currentZoneId: { [index: number]: number } = []
|
||||
private readonly characterService = new CharacterService()
|
||||
private readonly zoneEventTileService = new ZoneEventTileService()
|
||||
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {
|
||||
this.characterMoveService = new CharacterMoveService()
|
||||
this.zoneEventTileService = new ZoneEventTileService()
|
||||
}
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('character:initMove', this.handleCharacterMove.bind(this))
|
||||
this.socket.on('character:move', this.handleCharacterMove.bind(this))
|
||||
}
|
||||
|
||||
private async handleCharacterMove({ positionX, positionY }: { positionX: number; positionY: number }): Promise<void> {
|
||||
let character = CharacterManager.getCharacterFromSocket(this.socket)
|
||||
if (!character) {
|
||||
gameLogger.error('character:move error', 'Character not found')
|
||||
const zoneCharacter = ZoneManager.getCharacter(this.socket.characterId!)
|
||||
if (!zoneCharacter?.character) {
|
||||
gameLogger.error('character:move error', 'Character not found or not initialized')
|
||||
return
|
||||
}
|
||||
|
||||
if (!character) {
|
||||
gameLogger.error('character:move error', 'character has not been initialized?')
|
||||
return
|
||||
// If already moving, cancel current movement and wait for it to fully stop
|
||||
if (zoneCharacter.isMoving) {
|
||||
zoneCharacter.isMoving = false
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
}
|
||||
|
||||
const path = await this.characterMoveService.calculatePath(character, positionX, positionY)
|
||||
const path = await this.characterService.calculatePath(zoneCharacter.character, positionX, positionY)
|
||||
if (!path) {
|
||||
this.io.in(character.zoneId.toString()).emit('character:moveError', 'No valid path found')
|
||||
this.io.in(zoneCharacter.character.zoneId.toString()).emit('character:moveError', 'No valid path found')
|
||||
return
|
||||
}
|
||||
|
||||
if (!character.isMoving && character.resetMovement) {
|
||||
character.resetMovement = false
|
||||
}
|
||||
if (character.isMoving && !character.resetMovement) {
|
||||
character.resetMovement = true
|
||||
this.nextPath[character.id] = path
|
||||
}
|
||||
if (!character.isMoving && !character.resetMovement) {
|
||||
character.isMoving = true
|
||||
this.currentZoneId[character.id] = character.zoneId
|
||||
await this.moveAlongPath(character, path)
|
||||
}
|
||||
// Start new movement
|
||||
zoneCharacter.isMoving = true
|
||||
zoneCharacter.currentPath = path // Add this property to ZoneCharacter class
|
||||
await this.moveAlongPath(zoneCharacter, path)
|
||||
}
|
||||
|
||||
private async moveAlongPath(character: ExtendedCharacter, path: Array<{ x: number; y: number }>): Promise<void> {
|
||||
private async moveAlongPath(zoneCharacter: ZoneCharacter, path: Array<{ x: number; y: number }>): Promise<void> {
|
||||
const { character } = zoneCharacter
|
||||
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
const start = path[i]
|
||||
const end = path[i + 1]
|
||||
// Exit if movement was cancelled or interrupted
|
||||
if (!zoneCharacter.isMoving || zoneCharacter.currentPath !== path) {
|
||||
return
|
||||
}
|
||||
|
||||
// if (!(await this.movementValidator.isValidMove(character, end))) {
|
||||
// break
|
||||
// }
|
||||
const [start, end] = [path[i], path[i + 1]]
|
||||
character.rotation = Rotation.calculate(start.x, start.y, end.x, end.y)
|
||||
|
||||
if (CharacterManager.hasResetMovement(character)) {
|
||||
const zoneEventTile = await zoneEventTileRepository.getEventTileByZoneIdAndPosition(character.zoneId, Math.floor(end.x), Math.floor(end.y))
|
||||
|
||||
if (zoneEventTile?.type === 'BLOCK') break
|
||||
if (zoneEventTile?.type === 'TELEPORT' && zoneEventTile.teleport) {
|
||||
await this.handleZoneEventTile(zoneEventTile as ZoneEventTileWithTeleport)
|
||||
break
|
||||
}
|
||||
|
||||
character.rotation = Rotation.calculate(start.x, start.y, end.x, end.y)
|
||||
|
||||
const zoneEventTile = await prisma.zoneEventTile.findFirst({
|
||||
where: {
|
||||
zoneId: character.zoneId,
|
||||
positionX: Math.floor(end.x),
|
||||
positionY: Math.floor(end.y)
|
||||
}
|
||||
})
|
||||
|
||||
if (zoneEventTile) {
|
||||
if (zoneEventTile.type === 'BLOCK') {
|
||||
break
|
||||
}
|
||||
|
||||
if (zoneEventTile.type === 'TELEPORT') {
|
||||
const teleportTile = (await prisma.zoneEventTile.findFirst({
|
||||
where: { id: zoneEventTile.id },
|
||||
include: { teleport: true }
|
||||
})) as ZoneEventTileWithTeleport
|
||||
|
||||
if (teleportTile) {
|
||||
await this.handleZoneEventTile(teleportTile)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.characterMoveService.updatePosition(character, end)
|
||||
this.io.in(character.zoneId.toString()).emit('character:move', character)
|
||||
|
||||
await this.characterMoveService.applyMovementDelay()
|
||||
this.characterService.updatePosition(character, end)
|
||||
this.io.in(character.zoneId.toString()).emit('character:move', zoneCharacter)
|
||||
await this.characterService.applyMovementDelay()
|
||||
}
|
||||
|
||||
if (CharacterManager.hasResetMovement(character)) {
|
||||
character.resetMovement = false
|
||||
if (this.currentZoneId[character.id] === character.zoneId) {
|
||||
await this.moveAlongPath(character, this.nextPath[character.id])
|
||||
} else {
|
||||
delete this.currentZoneId[character.id]
|
||||
character.isMoving = false
|
||||
}
|
||||
} else {
|
||||
this.finalizeMovement(character)
|
||||
// Only finalize if this path wasn't interrupted
|
||||
if (zoneCharacter.isMoving && zoneCharacter.currentPath === path) {
|
||||
this.finalizeMovement(zoneCharacter)
|
||||
}
|
||||
}
|
||||
|
||||
private async handleZoneEventTile(zoneEventTile: ZoneEventTileWithTeleport): Promise<void> {
|
||||
const character = CharacterManager.getCharacterFromSocket(this.socket)
|
||||
if (!character) {
|
||||
const zoneCharacter = ZoneManager.getCharacter(this.socket.characterId!)
|
||||
if (!zoneCharacter) {
|
||||
gameLogger.error('character:move error', 'Character not found')
|
||||
return
|
||||
}
|
||||
|
||||
const teleport = zoneEventTile.teleport
|
||||
if (teleport) {
|
||||
await this.zoneEventTileService.handleTeleport(this.io, this.socket, character, teleport)
|
||||
return
|
||||
if (zoneEventTile.teleport) {
|
||||
await this.zoneEventTileService.handleTeleport(this.io, this.socket, zoneCharacter.character, zoneEventTile.teleport)
|
||||
}
|
||||
}
|
||||
|
||||
private finalizeMovement(character: ExtendedCharacter): void {
|
||||
character.isMoving = false
|
||||
this.io.in(character.zoneId.toString()).emit('character:move', character)
|
||||
private finalizeMovement(zoneCharacter: ZoneCharacter): void {
|
||||
zoneCharacter.isMoving = false
|
||||
this.io.in(zoneCharacter.character.zoneId.toString()).emit('character:move', zoneCharacter)
|
||||
}
|
||||
}
|
||||
|
24
src/socketEvents/zone/weather.ts
Normal file
24
src/socketEvents/zone/weather.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../utilities/types'
|
||||
import { gameLogger } from '../../utilities/logger'
|
||||
import WeatherManager from '../../managers/weatherManager'
|
||||
|
||||
export default class Weather {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('weather', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(): Promise<void> {
|
||||
try {
|
||||
const weather = await WeatherManager.getWeatherState()
|
||||
this.socket.emit('weather', weather)
|
||||
} catch (error: any) {
|
||||
gameLogger.error('error', error.message)
|
||||
}
|
||||
}
|
||||
}
|
@ -2,33 +2,31 @@ import config from '../config'
|
||||
|
||||
class Rotation {
|
||||
static calculate(X1: number, Y1: number, X2: number, Y2: number): number {
|
||||
let rotation = 0
|
||||
|
||||
if (config.ALLOW_DIAGONAL_MOVEMENT) {
|
||||
// Check diagonal movements
|
||||
if (X1 > X2 && Y1 > Y2) {
|
||||
rotation = 7
|
||||
return 7
|
||||
} else if (X1 < X2 && Y1 < Y2) {
|
||||
rotation = 3
|
||||
return 3
|
||||
} else if (X1 > X2 && Y1 < Y2) {
|
||||
rotation = 5
|
||||
return 5
|
||||
} else if (X1 < X2 && Y1 > Y2) {
|
||||
rotation = 1
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
if (rotation === 0) {
|
||||
if (X1 > X2) {
|
||||
rotation = 6
|
||||
} else if (X1 < X2) {
|
||||
rotation = 2
|
||||
} else if (Y1 < Y2) {
|
||||
rotation = 4
|
||||
} else if (Y1 > Y2) {
|
||||
rotation = 0
|
||||
}
|
||||
// Non-diagonal movements
|
||||
if (X1 > X2) {
|
||||
return 6
|
||||
} else if (X1 < X2) {
|
||||
return 2
|
||||
} else if (Y1 < Y2) {
|
||||
return 4
|
||||
} else if (Y1 > Y2) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return rotation
|
||||
return 0 // Default case
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
export function isCommand(message: string, command?: string) {
|
||||
if (command) {
|
||||
return message.startsWith(`/${command} `)
|
||||
return message === `/${command}` || message.startsWith(`/${command} `)
|
||||
}
|
||||
return message.startsWith('/')
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ class config {
|
||||
static REDIS_URL: string = process.env.REDIS_URL || 'redis://@127.0.0.1:6379/4'
|
||||
static HOST: string = process.env.HOST || '0.0.0.0'
|
||||
static PORT: number = process.env.PORT ? parseInt(process.env.PORT) : 6969
|
||||
static CLIENT_URL: string = process.env.CLIENT_URL ? process.env.CLIENT_URL : 'https://sylvan.quest'
|
||||
static JWT_SECRET: string = process.env.JWT_SECRET || 'secret'
|
||||
|
||||
static ALLOW_DIAGONAL_MOVEMENT: boolean = process.env.ALLOW_DIAGONAL_MOVEMENT === 'true'
|
||||
@ -14,6 +15,11 @@ class config {
|
||||
static DEFAULT_CHARACTER_ZONE: number = parseInt(process.env.DEFAULT_CHARACTER_ZONE || '1')
|
||||
static DEFAULT_CHARACTER_X: number = parseInt(process.env.DEFAULT_CHARACTER_POS_X || '0')
|
||||
static DEFAULT_CHARACTER_Y: number = parseInt(process.env.DEFAULT_CHARACTER_POS_Y || '0')
|
||||
|
||||
static SMTP_HOST: string = process.env.SMTP_HOST || 'my.directonline.io'
|
||||
static SMTP_PORT: number = process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT) : 587
|
||||
static SMTP_USER: string = process.env.SMTP_USER || 'no-reply@sylvan.quest'
|
||||
static SMTP_PASSWORD: string = process.env.SMTP_PASSWORD || 'password'
|
||||
}
|
||||
|
||||
export default config
|
||||
|
@ -2,11 +2,14 @@ import { Application, Request, Response } from 'express'
|
||||
import UserService from '../services/userService'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import config from './config'
|
||||
import { loginAccountSchema, registerAccountSchema } from './zodTypes'
|
||||
import { loginAccountSchema, registerAccountSchema, resetPasswordSchema, newPasswordSchema } from './zodTypes'
|
||||
import fs from 'fs'
|
||||
import { httpLogger } from './logger'
|
||||
import { getPublicPath } from './storage'
|
||||
import zoneRepository from '../repositories/zoneRepository'
|
||||
import TileRepository from '../repositories/tileRepository'
|
||||
import { AssetData } from './types'
|
||||
import ZoneRepository from '../repositories/zoneRepository'
|
||||
import SpriteRepository from '../repositories/spriteRepository'
|
||||
|
||||
async function addHttpRoutes(app: Application) {
|
||||
/**
|
||||
@ -40,16 +43,16 @@ async function addHttpRoutes(app: Application) {
|
||||
* @param res
|
||||
*/
|
||||
app.post('/register', async (req: Request, res: Response) => {
|
||||
const { username, password } = req.body
|
||||
const { username, email, password } = req.body
|
||||
|
||||
try {
|
||||
registerAccountSchema.parse({ username, password })
|
||||
registerAccountSchema.parse({ username, email, password })
|
||||
} catch (error: any) {
|
||||
return res.status(400).json({ message: error.errors[0]?.message })
|
||||
}
|
||||
|
||||
const userService = new UserService()
|
||||
const user = await userService.register(username, password)
|
||||
const user = await userService.register(username, email, password)
|
||||
|
||||
if (user) {
|
||||
return res.status(200).json({ message: 'User registered' })
|
||||
@ -58,12 +61,82 @@ async function addHttpRoutes(app: Application) {
|
||||
return res.status(400).json({ message: 'Failed to register user' })
|
||||
})
|
||||
|
||||
/**
|
||||
* Reset password
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
app.post('/reset-password', async (req: Request, res: Response) => {
|
||||
const { email } = req.body
|
||||
|
||||
try {
|
||||
resetPasswordSchema.parse({ email })
|
||||
} catch (error: any) {
|
||||
return res.status(400).json({ message: error.errors[0]?.message })
|
||||
}
|
||||
|
||||
const userService = new UserService()
|
||||
const sentEmail = await userService.requestPasswordReset(email)
|
||||
|
||||
if (sentEmail) {
|
||||
return res.status(200).json({ message: 'Email has been sent' })
|
||||
}
|
||||
|
||||
return res.status(400).json({ message: 'Failed to send password reset request. Perhaps one has already been sent recently, check your spam folder.' })
|
||||
})
|
||||
|
||||
/**
|
||||
* New password
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
app.post('/new-password', async (req: Request, res: Response) => {
|
||||
const { urlToken, password } = req.body
|
||||
|
||||
try {
|
||||
newPasswordSchema.parse({ urlToken, password })
|
||||
} catch (error: any) {
|
||||
return res.status(400).json({ message: error.errors[0]?.message })
|
||||
}
|
||||
|
||||
const userService = new UserService()
|
||||
const resetPassword = await userService.resetPassword(urlToken, password)
|
||||
|
||||
if (resetPassword) {
|
||||
return res.status(200).json({ message: 'Password has been reset' })
|
||||
}
|
||||
|
||||
return res.status(400).json({ message: 'Failed to set new password' })
|
||||
})
|
||||
|
||||
/**
|
||||
* Get all tiles from a zone as an array of ids
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
app.get('/assets/tiles/:zoneId', async (req: Request, res: Response) => {
|
||||
app.get('/assets/list_tiles', async (req: Request, res: Response) => {
|
||||
// Get all tiles
|
||||
let assets: AssetData[] = []
|
||||
const tiles = await TileRepository.getAll()
|
||||
for (const tile of tiles) {
|
||||
assets.push({
|
||||
key: tile.id,
|
||||
data: '/assets/tiles/' + tile.id + '.png',
|
||||
group: 'tiles',
|
||||
updatedAt: tile.updatedAt
|
||||
} as AssetData)
|
||||
}
|
||||
|
||||
// Return the array
|
||||
res.json(assets)
|
||||
})
|
||||
|
||||
/**
|
||||
* Get all tiles from a zone and serve as AssetData array
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
app.get('/assets/list_tiles/:zoneId', async (req: Request, res: Response) => {
|
||||
const zoneId = req.params.zoneId
|
||||
|
||||
// Check if zoneId is valid number
|
||||
@ -72,28 +145,59 @@ async function addHttpRoutes(app: Application) {
|
||||
}
|
||||
|
||||
// Get zone by id
|
||||
const zone = await zoneRepository.getById(parseInt(zoneId))
|
||||
const zone = await ZoneRepository.getById(parseInt(zoneId))
|
||||
if (!zone) {
|
||||
return res.status(404).json({ message: 'Zone not found' })
|
||||
}
|
||||
|
||||
let tiles = zone.tiles;
|
||||
|
||||
// Convert to array
|
||||
tiles = JSON.parse(JSON.stringify(tiles)) as string[]
|
||||
|
||||
// Flatten the array
|
||||
tiles = [...new Set(tiles.flat())]
|
||||
|
||||
// Remove duplicates
|
||||
tiles = tiles.filter((value, index, self) => self.indexOf(value) === index);
|
||||
// Get all tiles
|
||||
let assets: AssetData[] = []
|
||||
const tiles = await TileRepository.getByZoneId(parseInt(zoneId))
|
||||
for (const tile of tiles) {
|
||||
assets.push({
|
||||
key: tile.id,
|
||||
data: '/assets/tiles/' + tile.id + '.png',
|
||||
group: 'tiles',
|
||||
updatedAt: tile.updatedAt
|
||||
} as AssetData)
|
||||
}
|
||||
|
||||
// Return the array
|
||||
res.json(tiles)
|
||||
res.json(assets)
|
||||
})
|
||||
|
||||
app.get('/assets/list_sprite_actions/:spriteId', async (req: Request, res: Response) => {
|
||||
const spriteId = req.params.spriteId
|
||||
// Check if spriteId is valid number
|
||||
if (!spriteId || parseInt(spriteId) === 0) {
|
||||
return res.status(400).json({ message: 'Invalid sprite ID' })
|
||||
}
|
||||
// Get sprite by id
|
||||
const sprite = await SpriteRepository.getById(spriteId)
|
||||
if (!sprite) {
|
||||
return res.status(404).json({ message: 'Sprite not found' })
|
||||
}
|
||||
|
||||
let assets: AssetData[] = []
|
||||
sprite.spriteActions.forEach((spriteAction) => {
|
||||
assets.push({
|
||||
key: sprite.id + '-' + spriteAction.action,
|
||||
data: '/assets/sprites/' + sprite.id + '/' + spriteAction.action + '.png',
|
||||
group: spriteAction.isAnimated ? 'sprite_animations' : 'sprites',
|
||||
updatedAt: sprite.updatedAt,
|
||||
isAnimated: spriteAction.isAnimated,
|
||||
frameCount: JSON.parse(JSON.stringify(spriteAction.sprites)).length,
|
||||
frameWidth: spriteAction.frameWidth,
|
||||
frameHeight: spriteAction.frameHeight
|
||||
})
|
||||
})
|
||||
|
||||
// Return the array
|
||||
res.json(assets)
|
||||
})
|
||||
|
||||
/**
|
||||
* Get a specific asset
|
||||
* Download asset file
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
|
@ -1,49 +0,0 @@
|
||||
import * as fs from 'fs/promises'
|
||||
import { appLogger } from './logger'
|
||||
|
||||
export async function readJsonFile<T>(filePath: string): Promise<T> {
|
||||
try {
|
||||
const fileContent = await fs.readFile(filePath, 'utf-8')
|
||||
return JSON.parse(fileContent) as T
|
||||
} catch (error) {
|
||||
appLogger.error(`Error reading JSON file: ${error instanceof Error ? error.message : String(error)}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeJsonFile<T>(filePath: string, data: T): Promise<void> {
|
||||
try {
|
||||
const jsonString = JSON.stringify(data, null, 2)
|
||||
await fs.writeFile(filePath, jsonString, 'utf-8')
|
||||
} catch (error) {
|
||||
appLogger.error(`Error writing JSON file: ${error instanceof Error ? error.message : String(error)}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function readJsonValue<T>(filePath: string, paramPath: string): Promise<T> {
|
||||
try {
|
||||
const jsonContent = await readJsonFile<any>(filePath)
|
||||
const paramValue = paramPath.split('.').reduce((obj, key) => obj && obj[key], jsonContent)
|
||||
|
||||
if (paramValue === undefined) {
|
||||
throw new Error(`Parameter ${paramPath} not found in the JSON file`)
|
||||
}
|
||||
|
||||
return paramValue as T
|
||||
} catch (error) {
|
||||
appLogger.error(`Error reading JSON parameter: ${error instanceof Error ? error.message : String(error)}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function setJsonValue<T>(filePath: string, key: string, value: any): Promise<void> {
|
||||
try {
|
||||
const data = await readJsonFile<T>(filePath)
|
||||
const updatedData = { ...data, [key]: value }
|
||||
await writeJsonFile(filePath, updatedData)
|
||||
} catch (error) {
|
||||
appLogger.error(`Error setting JSON value: ${error instanceof Error ? error.message : String(error)}`)
|
||||
throw error
|
||||
}
|
||||
}
|
@ -32,13 +32,31 @@ const watchLogs = () => {
|
||||
LOG_TYPES.forEach((type) => {
|
||||
const logFile = getRootPath('logs', `${type}.log`)
|
||||
|
||||
fs.watchFile(logFile, (curr, prev) => {
|
||||
if (curr.size > prev.size) {
|
||||
const stream = fs.createReadStream(logFile, { start: prev.size, end: curr.size })
|
||||
stream.on('data', (chunk) => {
|
||||
console.log(`[${type}]\n${chunk.toString()}`)
|
||||
})
|
||||
// Get initial file size
|
||||
const stats = fs.statSync(logFile)
|
||||
let lastPosition = stats.size
|
||||
|
||||
fs.watch(logFile, (eventType) => {
|
||||
if (eventType !== 'change') {
|
||||
return
|
||||
}
|
||||
|
||||
fs.stat(logFile, (err, stats) => {
|
||||
if (err) return
|
||||
|
||||
if (stats.size > lastPosition) {
|
||||
const stream = fs.createReadStream(logFile, {
|
||||
start: lastPosition,
|
||||
end: stats.size
|
||||
})
|
||||
|
||||
stream.on('data', (chunk) => {
|
||||
console.log(`[${type}]\n${chunk.toString()}`)
|
||||
})
|
||||
|
||||
lastPosition = stats.size
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Socket } from 'socket.io'
|
||||
import { Character, User } from '@prisma/client'
|
||||
import { Character, User, ZoneEventTile, ZoneEventTileTeleport } from '@prisma/client'
|
||||
|
||||
export type TSocket = Socket & {
|
||||
user?: User
|
||||
userId?: number
|
||||
characterId?: number
|
||||
handshake?: {
|
||||
query?: {
|
||||
@ -18,14 +18,19 @@ export type TSocket = Socket & {
|
||||
|
||||
export type ExtendedCharacter = Character & {
|
||||
isMoving?: boolean
|
||||
resetMovement: boolean
|
||||
resetMovement?: boolean
|
||||
}
|
||||
|
||||
export type TAsset = {
|
||||
export type ZoneEventTileWithTeleport = ZoneEventTile & {
|
||||
teleport: ZoneEventTileTeleport
|
||||
}
|
||||
|
||||
export type AssetData = {
|
||||
key: string
|
||||
url: string
|
||||
data: string
|
||||
group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
|
||||
updatedAt: Date
|
||||
isAnimated?: boolean
|
||||
frameCount?: number
|
||||
frameWidth?: number
|
||||
frameHeight?: number
|
||||
|
3
src/utilities/utilities.ts
Normal file
3
src/utilities/utilities.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function unduplicateArray(array: any[]) {
|
||||
return [...new Set(array.flat())]
|
||||
}
|
@ -20,6 +20,29 @@ export const registerAccountSchema = z.object({
|
||||
.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' }),
|
||||
email: z
|
||||
.string()
|
||||
.min(3, { message: 'Email must be at least 3 characters long' })
|
||||
.max(255, { message: 'Email must be at most 255 characters long' })
|
||||
.regex(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/, { message: 'Email must be valid' }),
|
||||
password: z
|
||||
.string()
|
||||
.min(8, {
|
||||
message: 'Password must be at least 8 characters long'
|
||||
})
|
||||
.max(255)
|
||||
})
|
||||
|
||||
export const resetPasswordSchema = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.min(3, { message: 'Email must be at least 3 characters long' })
|
||||
.max(255, { message: 'Email must be at most 255 characters long' })
|
||||
.regex(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/, { message: 'Email must be valid' })
|
||||
})
|
||||
|
||||
export const newPasswordSchema = z.object({
|
||||
urlToken: z.string().min(10, { message: 'Invalid request' }).max(255, { message: 'Invalid request' }),
|
||||
password: z
|
||||
.string()
|
||||
.min(8, {
|
||||
|
9
src/utilities/zone.ts
Normal file
9
src/utilities/zone.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export function FlattenZoneArray(tiles: string[][]) {
|
||||
const normalArray = []
|
||||
|
||||
for (const row of tiles) {
|
||||
normalArray.push(...row)
|
||||
}
|
||||
|
||||
return normalArray
|
||||
}
|
Reference in New Issue
Block a user