1
0
forked from noxious/server

Compare commits

...

47 Commits

Author SHA1 Message Date
9d08073fa8 npm update, http asset endpoint changes 2024-10-25 22:21:06 +02:00
5631930bf5 Typo fix¿ 2024-10-21 19:13:20 +02:00
b6e7a5d7fe Started working on Dexie support 2024-10-21 02:08:04 +02:00
63804336be Inform user about not meeting requirements upon character creation 2024-10-19 23:39:56 +02:00
0b62b4231b npm update 2024-10-19 21:15:26 +02:00
4e1e7d95ac Added logging, worked on character type management 2024-10-19 02:14:39 +02:00
d29420cbf3 ? 2024-10-18 23:56:53 +02:00
acc04daa27 New migration 2024-10-18 23:24:43 +02:00
8abf5acef3 #137 : ZoneEffects 2024-10-18 23:08:50 +02:00
780cac9644 npm update 2024-10-18 00:21:38 +02:00
44481e19a8 npm update 2024-10-17 02:32:17 +02:00
9075bfaad5 npm update 2024-10-16 15:45:49 +02:00
bfd941c091 Rnamed Datetime > Date 2024-10-14 20:11:08 +02:00
bb9f62a9c8 Renamed files to storage, re-worked datetimeManager, added json help utilities 2024-10-14 19:47:52 +02:00
049b9de2b3 Renamed utilities to files, added datetimeManager, npm update 2024-10-13 12:15:29 +02:00
2008646a3f npm update 2024-10-04 20:06:09 +02:00
075592702c Zone editor bug fix: set updatedAt at saving so it gets sent back to the client 2024-10-02 19:57:08 +02:00
d271efc1ec npm update 2024-10-02 16:17:43 +02:00
297d4742a4 #91 : Zone editor: allow objects to be rotated 2024-10-01 21:59:51 +02:00
ab649b9fa1 Removed debugging line 2024-10-01 00:34:58 +02:00
4f643269eb Replace fix for tiles command 2024-10-01 00:30:25 +02:00
ce1708a55e Path fixes for all environments, npm run format, removed redundant imports 2024-10-01 00:10:30 +02:00
4cbd62cbb0 Test #1 2024-09-30 23:31:00 +02:00
7b3c4b92a5 File loading fixes for prod. 2024-09-30 23:18:27 +02:00
da8ef9fa65 Uhm excuse me, but what the fuck 2024-09-30 22:56:17 +02:00
4f9a1bc879 npm run dev 2024-09-30 22:42:46 +02:00
3638e2a793 Fixed oopsies 2024-09-30 22:39:48 +02:00
6ac827630a Update command manager and commands to OOP 2024-09-30 22:29:58 +02:00
3ec4bc2557 Prod. command fix attempt #2 2024-09-30 22:20:07 +02:00
6a286590b4 Fix maybe 2024-09-30 22:17:14 +02:00
34ed2ba7cb TMUX? 2024-09-30 21:23:38 +02:00
72159cdc17 Add screen 2024-09-30 21:14:19 +02:00
70d8c43350 #169 : Fixed command for tile bleeding 2024-09-30 20:22:41 +02:00
3a83f2c1ff #169 : Re-enabled command manager, created extrudeTiles command for testing 2024-09-30 19:02:55 +02:00
ddeee356b4 Revert "#169 : Expand tiles to 68x64px and draw 1px line to each side-profile to"
This reverts commit e9fb277d63.
2024-09-29 21:56:03 +02:00
e9fb277d63 #169 : Expand tiles to 68x64px and draw 1px line to each side-profile to 2024-09-29 21:23:16 +02:00
e8aee51248 #95 : Replaced : with / for commands 2024-09-28 21:26:27 +02:00
46fdb3edb6 #160 : Fix for being unable to chat after teleporting into another zone 2024-09-28 03:17:15 +02:00
dec6b36699 #143 : Fix switching back to zone from zoneEditor 2024-09-28 02:52:16 +02:00
21a75f6cbe npm run format 2024-09-28 02:18:31 +02:00
10a231b54c Log when a character joins a zone 2024-09-28 02:18:06 +02:00
cc9eada654 Send frameCount for assets to client 2024-09-28 00:59:20 +02:00
6f057639c0 npm update 2024-09-28 00:39:05 +02:00
251a72aa97 npm update 2024-09-26 00:42:38 +02:00
0d7ed18b03 npm update 2024-09-24 16:09:40 +02:00
4a9b7987dc Added option to set rotation on teleport tiles, new base database migration (db reset needed) 2024-09-23 14:02:25 +02:00
a729371741 Converted socketEvents to new format
Still need to rework 'any' type Promises
2024-09-22 18:35:07 +02:00
60 changed files with 1108 additions and 710 deletions

5
.gitignore vendored
View File

@ -309,7 +309,4 @@ $RECYCLE.BIN/
# Windows shortcuts # Windows shortcuts
*.lnk *.lnk
# End of https://www.toptal.com/developers/gitignore/api/node,jetbrains+all,visualstudiocode,macos,windows # End of https://www.toptal.com/developers/gitignore/api/node,jetbrains+all,visualstudiocode,macos,windows
prisma/dev.db
prisma/dev.db-journal

View File

@ -1,8 +1,8 @@
# Use the official Node.js 22.4.1 image # Use the official Node.js 22.4.1 image
FROM node:22.4.1-alpine FROM node:22.4.1-alpine
# Install Redis # Install Redis and tmux
RUN apk add --no-cache redis RUN apk add --no-cache redis tmux
# Set the working directory in the container # Set the working directory in the container
WORKDIR /usr/src/ WORKDIR /usr/src/
@ -28,11 +28,13 @@ RUN npm run build
# Expose the ports your Node.js application and Redis will listen on # Expose the ports your Node.js application and Redis will listen on
EXPOSE 80 6379 EXPOSE 80 6379
# Create a shell script to run Redis, run migrations, and start the application # Create a shell script to run Redis, run migrations, and start the application in a tmux session
RUN echo '#!/bin/sh' > /usr/src/start.sh && \ RUN echo '#!/bin/sh' > /usr/src/start.sh && \
echo 'redis-server --daemonize yes' >> /usr/src/start.sh && \ echo 'redis-server --daemonize yes' >> /usr/src/start.sh && \
echo 'npx prisma migrate deploy' >> /usr/src/start.sh && \ echo 'npx prisma migrate deploy' >> /usr/src/start.sh && \
echo 'node dist/server.js' >> /usr/src/start.sh && \ echo 'tmux new-session -d -s nodeapp "node dist/server.js"' >> /usr/src/start.sh && \
echo 'echo "App is running in tmux session. Attach with: tmux attach-session -t nodeapp"' >> /usr/src/start.sh && \
echo 'tail -f /dev/null' >> /usr/src/start.sh && \
chmod +x /usr/src/start.sh chmod +x /usr/src/start.sh
# Use the shell script as the entry point # Use the shell script as the entry point

285
package-lock.json generated
View File

@ -44,9 +44,9 @@
} }
}, },
"node_modules/@emnapi/runtime": { "node_modules/@emnapi/runtime": {
"version": "1.2.0", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz",
"integrity": "sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==", "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@ -524,9 +524,9 @@
] ]
}, },
"node_modules/@prisma/client": { "node_modules/@prisma/client": {
"version": "5.19.1", "version": "5.21.1",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.19.1.tgz", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.21.1.tgz",
"integrity": "sha512-x30GFguInsgt+4z5I4WbkZP2CGpotJMUXy+Gl/aaUjHn2o1DnLYNTA+q9XdYmAQZM8fIIkvUiA2NpgosM3fneg==", "integrity": "sha512-3n+GgbAZYjaS/k0M03yQsQfR1APbr411r74foknnsGpmhNKBG49VuUkxIU6jORgvJPChoD4WC4PqoHImN1FP0w==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
@ -542,48 +542,48 @@
} }
}, },
"node_modules/@prisma/debug": { "node_modules/@prisma/debug": {
"version": "5.19.1", "version": "5.21.1",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.19.1.tgz", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.21.1.tgz",
"integrity": "sha512-lAG6A6QnG2AskAukIEucYJZxxcSqKsMK74ZFVfCTOM/7UiyJQi48v6TQ47d6qKG3LbMslqOvnTX25dj/qvclGg==", "integrity": "sha512-uY8SAhcnORhvgtOrNdvWS98Aq/nkQ9QDUxrWAgW8XrCZaI3j2X7zb7Xe6GQSh6xSesKffFbFlkw0c2luHQviZA==",
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@prisma/engines": { "node_modules/@prisma/engines": {
"version": "5.19.1", "version": "5.21.1",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.19.1.tgz", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.21.1.tgz",
"integrity": "sha512-kR/PoxZDrfUmbbXqqb8SlBBgCjvGaJYMCOe189PEYzq9rKqitQ2fvT/VJ8PDSe8tTNxhc2KzsCfCAL+Iwm/7Cg==", "integrity": "sha512-hGVTldUkIkTwoV8//hmnAAiAchi4oMEKD3aW5H2RrnI50tTdwza7VQbTTAyN3OIHWlK5DVg6xV7X8N/9dtOydA==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@prisma/debug": "5.19.1", "@prisma/debug": "5.21.1",
"@prisma/engines-version": "5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3", "@prisma/engines-version": "5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36",
"@prisma/fetch-engine": "5.19.1", "@prisma/fetch-engine": "5.21.1",
"@prisma/get-platform": "5.19.1" "@prisma/get-platform": "5.21.1"
} }
}, },
"node_modules/@prisma/engines-version": { "node_modules/@prisma/engines-version": {
"version": "5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3", "version": "5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3.tgz", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36.tgz",
"integrity": "sha512-xR6rt+z5LnNqTP5BBc+8+ySgf4WNMimOKXRn6xfNRDSpHvbOEmd7+qAOmzCrddEc4Cp8nFC0txU14dstjH7FXA==", "integrity": "sha512-qvnEflL0//lh44S/T9NcvTMxfyowNeUxTunPcDfKPjyJNrCNf2F1zQLcUv5UHAruECpX+zz21CzsC7V2xAeM7Q==",
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@prisma/fetch-engine": { "node_modules/@prisma/fetch-engine": {
"version": "5.19.1", "version": "5.21.1",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.19.1.tgz", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.21.1.tgz",
"integrity": "sha512-pCq74rtlOVJfn4pLmdJj+eI4P7w2dugOnnTXpRilP/6n5b2aZiA4ulJlE0ddCbTPkfHmOL9BfaRgA8o+1rfdHw==", "integrity": "sha512-70S31vgpCGcp9J+mh/wHtLCkVezLUqe/fGWk3J3JWZIN7prdYSlr1C0niaWUyNK2VflLXYi8kMjAmSxUVq6WGQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@prisma/debug": "5.19.1", "@prisma/debug": "5.21.1",
"@prisma/engines-version": "5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3", "@prisma/engines-version": "5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36",
"@prisma/get-platform": "5.19.1" "@prisma/get-platform": "5.21.1"
} }
}, },
"node_modules/@prisma/get-platform": { "node_modules/@prisma/get-platform": {
"version": "5.19.1", "version": "5.21.1",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.19.1.tgz", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.21.1.tgz",
"integrity": "sha512-sCeoJ+7yt0UjnR+AXZL7vXlg5eNxaFOwC23h0KvW1YIXUoa7+W2ZcAUhoEQBmJTW4GrFqCuZ8YSP0mkDa4k3Zg==", "integrity": "sha512-sRxjL3Igst3ct+e8ya/x//cDXmpLbZQ5vfps2N4tWl4VGKQAmym77C/IG/psSMsQKszc8uFC/q1dgmKFLUgXZQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@prisma/debug": "5.19.1" "@prisma/debug": "5.21.1"
} }
}, },
"node_modules/@socket.io/component-emitter": { "node_modules/@socket.io/component-emitter": {
@ -673,9 +673,9 @@
} }
}, },
"node_modules/@types/express-serve-static-core": { "node_modules/@types/express-serve-static-core": {
"version": "4.19.5", "version": "4.19.6",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
"integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -719,9 +719,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.16.5", "version": "20.17.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.1.tgz",
"integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==", "integrity": "sha512-j2VlPv1NnwPJbaCNv69FO/1z4lId0QmGvpT41YxitRtWlg96g/j8qcv2RKsLKe2F6OJgyXhupN1Xo17b2m139Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.19.2" "undici-types": "~6.19.2"
@ -764,18 +764,6 @@
"@types/send": "*" "@types/send": "*"
} }
}, },
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"license": "MIT",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/accepts": { "node_modules/accepts": {
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@ -790,9 +778,9 @@
} }
}, },
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.12.1", "version": "8.13.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz",
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==",
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
@ -855,26 +843,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/base64id": { "node_modules/base64id": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
@ -951,30 +919,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/buffer-equal-constant-time": { "node_modules/buffer-equal-constant-time": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@ -982,9 +926,9 @@
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/bullmq": { "node_modules/bullmq": {
"version": "5.13.2", "version": "5.21.2",
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.13.2.tgz", "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.21.2.tgz",
"integrity": "sha512-McGE8k3mrCvdUHdU0sHkTKDS1xr4pff+hbEKBY51wk5S6Za0gkuejYA620VQTo3Zz37E/NVWMgumwiXPQ3yZcA==", "integrity": "sha512-LPuNoGaDc5CON2X6h4cJ2iVfd+B+02xubFU+IB/fyJHd+/HqUZRqnlYryUCAuhVHBhUKtA6oyVdJxqSa62i+og==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cron-parser": "^4.6.0", "cron-parser": "^4.6.0",
@ -1128,9 +1072,9 @@
} }
}, },
"node_modules/cookie": { "node_modules/cookie": {
"version": "0.6.0", "version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
@ -1282,9 +1226,9 @@
} }
}, },
"node_modules/engine.io": { "node_modules/engine.io": {
"version": "6.6.1", "version": "6.6.2",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.1.tgz", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz",
"integrity": "sha512-NEpDCw9hrvBW+hVEOK4T7v0jFJ++KgtPl4jKFwsZVfG1XhS0dCrSb3VMb9gPAd7VAdW52VT1EnaNiU2vM8C0og==", "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/cookie": "^0.4.1", "@types/cookie": "^0.4.1",
@ -1292,7 +1236,7 @@
"@types/node": ">=10.0.0", "@types/node": ">=10.0.0",
"accepts": "~1.3.4", "accepts": "~1.3.4",
"base64id": "2.0.0", "base64id": "2.0.0",
"cookie": "~0.4.1", "cookie": "~0.7.2",
"cors": "~2.8.5", "cors": "~2.8.5",
"debug": "~4.3.1", "debug": "~4.3.1",
"engine.io-parser": "~5.2.1", "engine.io-parser": "~5.2.1",
@ -1312,9 +1256,9 @@
} }
}, },
"node_modules/engine.io/node_modules/cookie": { "node_modules/engine.io/node_modules/cookie": {
"version": "0.4.2", "version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
@ -1379,28 +1323,10 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"license": "MIT",
"engines": {
"node": ">=0.8.x"
}
},
"node_modules/express": { "node_modules/express": {
"version": "4.21.0", "version": "4.21.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
"integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"accepts": "~1.3.8", "accepts": "~1.3.8",
@ -1408,7 +1334,7 @@
"body-parser": "1.20.3", "body-parser": "1.20.3",
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"content-type": "~1.0.4", "content-type": "~1.0.4",
"cookie": "0.6.0", "cookie": "0.7.1",
"cookie-signature": "1.0.6", "cookie-signature": "1.0.6",
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
@ -1650,26 +1576,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/ignore-by-default": { "node_modules/ignore-by-default": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
@ -2184,15 +2090,15 @@
} }
}, },
"node_modules/pino": { "node_modules/pino": {
"version": "9.4.0", "version": "9.5.0",
"resolved": "https://registry.npmjs.org/pino/-/pino-9.4.0.tgz", "resolved": "https://registry.npmjs.org/pino/-/pino-9.5.0.tgz",
"integrity": "sha512-nbkQb5+9YPhQRz/BeQmrWpEknAaqjpAqRK8NwJpmrX/JHu7JuZC5G1CeAwJDJfGes4h+YihC6in3Q2nGb+Y09w==", "integrity": "sha512-xSEmD4pLnV54t0NOUN16yCl7RIB1c5UUOse5HSyEXtBp+FgFQyPeDutc+Q2ZO7/22vImV7VfEjH/1zV2QuqvYw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"atomic-sleep": "^1.0.0", "atomic-sleep": "^1.0.0",
"fast-redact": "^3.1.1", "fast-redact": "^3.1.1",
"on-exit-leak-free": "^2.1.0", "on-exit-leak-free": "^2.1.0",
"pino-abstract-transport": "^1.2.0", "pino-abstract-transport": "^2.0.0",
"pino-std-serializers": "^7.0.0", "pino-std-serializers": "^7.0.0",
"process-warning": "^4.0.0", "process-warning": "^4.0.0",
"quick-format-unescaped": "^4.0.3", "quick-format-unescaped": "^4.0.3",
@ -2206,12 +2112,11 @@
} }
}, },
"node_modules/pino-abstract-transport": { "node_modules/pino-abstract-transport": {
"version": "1.2.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz", "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz",
"integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==", "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"readable-stream": "^4.0.0",
"split2": "^4.0.0" "split2": "^4.0.0"
} }
}, },
@ -2238,13 +2143,13 @@
} }
}, },
"node_modules/prisma": { "node_modules/prisma": {
"version": "5.19.1", "version": "5.21.1",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.19.1.tgz", "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.21.1.tgz",
"integrity": "sha512-c5K9MiDaa+VAAyh1OiYk76PXOme9s3E992D7kvvIOhCrNsBQfy2mP2QAQtX0WNj140IgG++12kwZpYB9iIydNQ==", "integrity": "sha512-PB+Iqzld/uQBPaaw2UVIk84kb0ITsLajzsxzsadxxl54eaU5Gyl2/L02ysivHxK89t7YrfQJm+Ggk37uvM70oQ==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@prisma/engines": "5.19.1" "@prisma/engines": "5.21.1"
}, },
"bin": { "bin": {
"prisma": "build/index.js" "prisma": "build/index.js"
@ -2256,15 +2161,6 @@
"fsevents": "2.3.3" "fsevents": "2.3.3"
} }
}, },
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/process-warning": { "node_modules/process-warning": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.0.tgz", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.0.tgz",
@ -2336,22 +2232,6 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/readable-stream": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz",
"integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==",
"license": "MIT",
"dependencies": {
"abort-controller": "^3.0.0",
"buffer": "^6.0.3",
"events": "^3.3.0",
"process": "^0.11.10",
"string_decoder": "^1.3.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/readdirp": { "node_modules/readdirp": {
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@ -2599,9 +2479,9 @@
} }
}, },
"node_modules/socket.io": { "node_modules/socket.io": {
"version": "4.8.0", "version": "4.8.1",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
"integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==", "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"accepts": "~1.3.4", "accepts": "~1.3.4",
@ -2709,9 +2589,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/sonic-boom": { "node_modules/sonic-boom": {
"version": "4.1.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.1.0.tgz", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz",
"integrity": "sha512-NGipjjRicyJJ03rPiZCJYjwlsuP2d1/5QUviozRXC7S3WdVWNK5e3Ojieb9CCyfhq2UC+3+SRd9nG3I2lPRvUw==", "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"atomic-sleep": "^1.0.0" "atomic-sleep": "^1.0.0"
@ -2741,15 +2621,6 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/supports-color": { "node_modules/supports-color": {
"version": "5.5.0", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@ -2848,9 +2719,9 @@
} }
}, },
"node_modules/tslib": { "node_modules/tslib": {
"version": "2.7.0", "version": "2.8.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz",
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==",
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/type-is": { "node_modules/type-is": {
@ -2867,9 +2738,9 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.6.2", "version": "5.6.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
"integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",

View File

@ -1,7 +1,7 @@
{ {
"scripts": { "scripts": {
"start": "npx prisma migrate deploy && node dist/server.js", "start": "npx prisma migrate deploy && node dist/server.js",
"dev": "nodemon --exec ts-node src/server.ts", "dev": "nodemon --ignore 'data/*' --exec ts-node src/server.ts",
"build": "tsc", "build": "tsc",
"format": "prettier --write src/" "format": "prettier --write src/"
}, },

0
prisma/.gitignore vendored Normal file
View File

View File

@ -53,7 +53,7 @@ CREATE TABLE `CharacterType` (
`name` VARCHAR(191) NOT NULL, `name` VARCHAR(191) NOT NULL,
`gender` ENUM('MALE', 'FEMALE') NOT NULL, `gender` ENUM('MALE', 'FEMALE') NOT NULL,
`race` ENUM('HUMAN', 'ELF', 'DWARF', 'ORC', 'GOBLIN') NOT NULL, `race` ENUM('HUMAN', 'ELF', 'DWARF', 'ORC', 'GOBLIN') NOT NULL,
`spriteId` VARCHAR(191) NOT NULL, `spriteId` VARCHAR(191) NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL, `updatedAt` DATETIME(3) NOT NULL,
@ -146,12 +146,23 @@ CREATE TABLE `Zone` (
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `ZoneEffect` (
`id` VARCHAR(191) NOT NULL,
`zoneId` INTEGER NOT NULL,
`effect` VARCHAR(191) NOT NULL,
`strength` INTEGER NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable -- CreateTable
CREATE TABLE `ZoneObject` ( CREATE TABLE `ZoneObject` (
`id` VARCHAR(191) NOT NULL, `id` VARCHAR(191) NOT NULL,
`zoneId` INTEGER NOT NULL, `zoneId` INTEGER NOT NULL,
`objectId` VARCHAR(191) NOT NULL, `objectId` VARCHAR(191) NOT NULL,
`depth` INTEGER NOT NULL DEFAULT 0, `depth` INTEGER NOT NULL DEFAULT 0,
`isRotated` BOOLEAN NOT NULL DEFAULT false,
`positionX` INTEGER NOT NULL DEFAULT 0, `positionX` INTEGER NOT NULL DEFAULT 0,
`positionY` INTEGER NOT NULL DEFAULT 0, `positionY` INTEGER NOT NULL DEFAULT 0,
@ -174,6 +185,7 @@ CREATE TABLE `ZoneEventTileTeleport` (
`id` VARCHAR(191) NOT NULL, `id` VARCHAR(191) NOT NULL,
`zoneEventTileId` VARCHAR(191) NOT NULL, `zoneEventTileId` VARCHAR(191) NOT NULL,
`toZoneId` INTEGER NOT NULL, `toZoneId` INTEGER NOT NULL,
`toRotation` INTEGER NOT NULL,
`toPositionX` INTEGER NOT NULL, `toPositionX` INTEGER NOT NULL,
`toPositionY` INTEGER NOT NULL, `toPositionY` INTEGER NOT NULL,
@ -208,6 +220,9 @@ ALTER TABLE `CharacterItem` ADD CONSTRAINT `CharacterItem_characterId_fkey` FORE
-- AddForeignKey -- AddForeignKey
ALTER TABLE `CharacterItem` ADD CONSTRAINT `CharacterItem_itemId_fkey` FOREIGN KEY (`itemId`) REFERENCES `Item`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE `CharacterItem` ADD CONSTRAINT `CharacterItem_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;
-- AddForeignKey -- AddForeignKey
ALTER TABLE `ZoneObject` ADD CONSTRAINT `ZoneObject_zoneId_fkey` FOREIGN KEY (`zoneId`) REFERENCES `Zone`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE `ZoneObject` ADD CONSTRAINT `ZoneObject_zoneId_fkey` FOREIGN KEY (`zoneId`) REFERENCES `Zone`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -25,8 +25,8 @@ model CharacterType {
gender CharacterGender gender CharacterGender
race CharacterRace race CharacterRace
characters Character[] characters Character[]
spriteId String spriteId String?
sprite Sprite @relation(fields: [spriteId], references: [id], onDelete: Cascade) sprite Sprite? @relation(fields: [spriteId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }

View File

@ -38,6 +38,7 @@ model Zone {
height Int @default(10) height Int @default(10)
tiles Json? tiles Json?
pvp Boolean @default(false) pvp Boolean @default(false)
zoneEffects ZoneEffect[]
zoneEventTiles ZoneEventTile[] zoneEventTiles ZoneEventTile[]
zoneEventTileTeleports ZoneEventTileTeleport[] zoneEventTileTeleports ZoneEventTileTeleport[]
zoneObjects ZoneObject[] zoneObjects ZoneObject[]
@ -47,15 +48,24 @@ model Zone {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
model ZoneEffect {
id String @id @default(uuid())
zoneId Int
zone Zone @relation(fields: [zoneId], references: [id], onDelete: Cascade)
effect String
strength Int
}
model ZoneObject { model ZoneObject {
id String @id @default(uuid()) id String @id @default(uuid())
zoneId Int zoneId Int
zone Zone @relation(fields: [zoneId], references: [id], onDelete: Cascade) zone Zone @relation(fields: [zoneId], references: [id], onDelete: Cascade)
objectId String objectId String
object Object @relation(fields: [objectId], references: [id], onDelete: Cascade) object Object @relation(fields: [objectId], references: [id], onDelete: Cascade)
depth Int @default(0) depth Int @default(0)
positionX Int @default(0) isRotated Boolean @default(false)
positionY Int @default(0) positionX Int @default(0)
positionY Int @default(0)
} }
enum ZoneEventTileType { enum ZoneEventTileType {
@ -81,6 +91,7 @@ model ZoneEventTileTeleport {
zoneEventTile ZoneEventTile @relation(fields: [zoneEventTileId], references: [id], onDelete: Cascade) zoneEventTile ZoneEventTile @relation(fields: [zoneEventTileId], references: [id], onDelete: Cascade)
toZoneId Int toZoneId Int
toZone Zone @relation(fields: [toZoneId], references: [id], onDelete: Cascade) toZone Zone @relation(fields: [toZoneId], references: [id], onDelete: Cascade)
toRotation Int
toPositionX Int toPositionX Int
toPositionY Int toPositionY Int
} }

View File

@ -2,8 +2,12 @@ import { Server } from 'socket.io'
type CommandInput = string[] type CommandInput = string[]
export default function (input: CommandInput, io: Server) { export default class AlertCommand {
const message: string = input.join(' ') ?? null constructor(private readonly io: Server) {}
if (!message) return console.log('message is required')
io.emit('notification', { message: message }) public execute(input: CommandInput): void {
const message: string = input.join(' ') ?? null
if (!message) return console.log('message is required')
this.io.emit('notification', { message: message })
}
} }

View File

@ -3,6 +3,10 @@ import ZoneManager from '../managers/zoneManager'
type CommandInput = string[] type CommandInput = string[]
export default function (input: CommandInput, io: Server) { export default class ListZonesCommand {
console.log(ZoneManager.getLoadedZones()) constructor(private readonly io: Server) {}
public execute(input: CommandInput): void {
console.log(ZoneManager.getLoadedZones())
}
} }

58
src/commands/tiles.ts Normal file
View File

@ -0,0 +1,58 @@
import fs from 'fs'
import sharp from 'sharp'
import { commandLogger } from '../utilities/logger'
import { Server } from 'socket.io'
import { getPublicPath } from '../utilities/storage'
import path from 'path'
export default class TilesCommand {
constructor(private readonly io: Server) {}
public async execute(): Promise<void> {
// Get all tiles
const tilesDir = getPublicPath('tiles')
const tiles = fs.readdirSync(tilesDir).filter((file) => file.endsWith('.png'))
// Create output directory if it doesn't exist
if (!fs.existsSync(tilesDir)) {
fs.mkdirSync(tilesDir, { recursive: true })
}
for (const tile of tiles) {
// Check if tile is already 66x34
const metadata = await sharp(getPublicPath('tiles', tile)).metadata()
if (metadata.width === 66 && metadata.height === 34) {
commandLogger.info(`Tile ${tile} already processed`)
continue
}
const inputPath = getPublicPath('tiles', tile)
const tempPath = getPublicPath('tiles', `temp_${tile}`)
try {
await sharp(inputPath)
.resize({
width: 66,
height: 34,
fit: 'fill',
kernel: 'nearest'
})
.toFile(tempPath)
// Replace original file with processed file
fs.unlinkSync(inputPath)
fs.renameSync(tempPath, inputPath)
commandLogger.info(`Processed and replaced: ${tile}`)
} catch (error) {
console.error(`Error processing ${tile}:`, error)
// Clean up temp file if it exists
if (fs.existsSync(tempPath)) {
fs.unlinkSync(tempPath)
}
}
}
commandLogger.info('Tile processing completed.')
}
}

View File

@ -2,9 +2,11 @@ import * as readline from 'readline'
import * as fs from 'fs' import * as fs from 'fs'
import * as path from 'path' import * as path from 'path'
import { Server } from 'socket.io' import { Server } from 'socket.io'
import { commandLogger } from '../utilities/logger'
import { getAppPath } from '../utilities/storage'
class CommandManager { class CommandManager {
private commands: Map<string, Function> = new Map() private commands: Map<string, any> = new Map()
private rl: readline.Interface private rl: readline.Interface
private io: Server | null = null private io: Server | null = null
private rlClosed: boolean = false private rlClosed: boolean = false
@ -23,7 +25,7 @@ class CommandManager {
public async boot(io: Server) { public async boot(io: Server) {
this.io = io this.io = io
await this.loadCommands() await this.loadCommands()
console.log('[✅] Command manager loaded') commandLogger.info('Command manager loaded')
this.startPrompt() this.startPrompt()
} }
@ -39,7 +41,9 @@ class CommandManager {
private async processCommand(command: string): Promise<void> { private async processCommand(command: string): Promise<void> {
const [cmd, ...args] = command.trim().split(' ') const [cmd, ...args] = command.trim().split(' ')
if (this.commands.has(cmd)) { if (this.commands.has(cmd)) {
this.commands.get(cmd)?.(args, this.io as Server) const CommandClass = this.commands.get(cmd)
const commandInstance = new CommandClass(this.io as Server)
await commandInstance.execute(args)
} else { } else {
this.handleUnknownCommand(cmd) this.handleUnknownCommand(cmd)
} }
@ -48,7 +52,6 @@ class CommandManager {
private handleUnknownCommand(command: string) { private handleUnknownCommand(command: string) {
switch (command) { switch (command) {
case 'exit': case 'exit':
console.log('Goodbye!')
this.rl.close() this.rl.close()
break break
default: default:
@ -58,37 +61,43 @@ class CommandManager {
} }
private async loadCommands() { private async loadCommands() {
const commandsDir = path.resolve(__dirname, 'commands') const directory = getAppPath('commands')
commandLogger.info(`Loading commands from: ${directory}`)
try { try {
const files: string[] = await fs.promises.readdir(commandsDir) const files = await fs.promises.readdir(directory, { withFileTypes: true })
for (const file of files) { for (const file of files) {
await this.loadCommand(commandsDir, file) if (!file.isFile() || (!file.name.endsWith('.ts') && !file.name.endsWith('.js'))) {
continue
}
const fullPath = getAppPath('commands', file.name)
const commandName = path.basename(file.name, path.extname(file.name))
try {
const module = await import(fullPath)
if (typeof module.default !== 'function') {
commandLogger.warn(`Unrecognized export in ${file.name}`)
continue
}
this.registerCommand(commandName, module.default)
} catch (error) {
commandLogger.error(`Error loading command ${file.name}: ${error instanceof Error ? error.message : String(error)}`)
}
} }
} catch (error) { } catch (error) {
console.error('[❌] Failed to read commands directory:', error) commandLogger.error(`Failed to read commands directory: ${error instanceof Error ? error.message : String(error)}`)
} }
} }
private async loadCommand(commandsDir: string, file: string) { private registerCommand(name: string, CommandClass: any) {
try {
const ext = path.extname(file)
const commandName = path.basename(file, ext)
const commandPath = path.join(commandsDir, file)
const module = await import(commandPath)
this.registerCommand(commandName, module.default)
} catch (error) {
console.error('[❌] Failed to load command:', file, error)
}
}
private registerCommand(name: string, command: (args: string[], io: Server) => void) {
if (this.commands.has(name)) { if (this.commands.has(name)) {
console.warn(`Command '${name}' is already registered. Overwriting...`) commandLogger.warn(`Command '${name}' is already registered. Overwriting...`)
} }
this.commands.set(name, command) this.commands.set(name, CommandClass)
console.log(`Registered command: ${name}`) commandLogger.info(`Registered command: ${name}`)
} }
} }

View File

@ -0,0 +1,68 @@
import { Server } from 'socket.io'
import { appLogger } from '../utilities/logger'
import { getRootPath } from '../utilities/storage'
import { readJsonValue, setJsonValue } from '../utilities/json'
class DateManager {
private static readonly GAME_SPEED = 8 // 24 game hours / 3 real hours
private static readonly UPDATE_INTERVAL = 1000 // 1 second
private io: Server | null = null
private intervalId: NodeJS.Timeout | null = null
private currentDate: Date = new Date()
public async boot(io: Server): Promise<void> {
this.io = io
await this.loadDate()
this.startDateLoop()
appLogger.info('Date manager loaded')
}
public stop(): void {
if (this.intervalId) {
clearInterval(this.intervalId)
this.intervalId = null
}
}
private async loadDate(): Promise<void> {
try {
const dateString = await readJsonValue<string>(this.getWorldFilePath(), 'date')
this.currentDate = new Date(dateString)
} catch (error) {
appLogger.error(`Failed to load date: ${error instanceof Error ? error.message : String(error)}`)
this.currentDate = new Date() // Use current date as fallback
}
}
private startDateLoop(): void {
this.intervalId = setInterval(() => {
this.advanceGameTime()
this.emitDate()
this.saveDate()
}, DateManager.UPDATE_INTERVAL)
}
private advanceGameTime(): void {
const advanceMilliseconds = DateManager.GAME_SPEED * DateManager.UPDATE_INTERVAL
this.currentDate = new Date(this.currentDate.getTime() + advanceMilliseconds)
}
private emitDate(): void {
this.io?.emit('date', this.currentDate)
}
private async saveDate(): Promise<void> {
try {
await setJsonValue(this.getWorldFilePath(), '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')
}
}
export default new DateManager()

View File

@ -5,7 +5,7 @@ import { Server as SocketServer } from 'socket.io'
import { TSocket } from '../utilities/types' import { TSocket } from '../utilities/types'
import { queueLogger } from '../utilities/logger' import { queueLogger } from '../utilities/logger'
import fs from 'fs' import fs from 'fs'
import path from 'path' import { getAppPath } from '../utilities/storage'
class QueueManager { class QueueManager {
private connection!: IORedis private connection!: IORedis
@ -52,9 +52,9 @@ class QueueManager {
const { jobName, params, socketId } = job.data const { jobName, params, socketId } = job.data
try { try {
const jobsDir = path.join(process.cwd(), 'src', 'jobs') const jobsDir = getAppPath('jobs')
const extension = config.ENV === 'development' ? '.ts' : '.js' const extension = config.ENV === 'development' ? '.ts' : '.js'
const jobPath = path.join(jobsDir, `${jobName}${extension}`) const jobPath = getAppPath('jobs', `${jobName}${extension}`)
if (!fs.existsSync(jobPath)) { if (!fs.existsSync(jobPath)) {
queueLogger.warn(`Job file not found: ${jobPath}`) queueLogger.warn(`Job file not found: ${jobPath}`)

View File

View File

@ -2,8 +2,7 @@ import { Zone } from '@prisma/client'
import ZoneRepository from '../repositories/zoneRepository' import ZoneRepository from '../repositories/zoneRepository'
import ZoneService from '../services/zoneService' import ZoneService from '../services/zoneService'
import LoadedZone from '../models/loadedZone' import LoadedZone from '../models/loadedZone'
import zoneRepository from '../repositories/zoneRepository' import { gameLogger } from '../utilities/logger'
import { gameMasterLogger } from '../utilities/logger'
class ZoneManager { class ZoneManager {
private loadedZones: LoadedZone[] = [] private loadedZones: LoadedZone[] = []
@ -21,36 +20,20 @@ class ZoneManager {
await this.loadZone(zone) await this.loadZone(zone)
} }
gameMasterLogger.info('Zone manager loaded') gameLogger.info('Zone manager loaded')
}
public async getZoneAssets(zone: Zone): Promise<ZoneAssets> {
const tiles: string[] = this.getUnique((JSON.parse(JSON.stringify(zone.tiles)) as string[][]).reduce((acc, val) => [...acc, ...val]))
const objects = await zoneRepository.getObjects(zone.id)
const mappedObjects = this.getUnique(objects.map((x) => x.objectId))
return {
tiles: tiles,
objects: mappedObjects
} as ZoneAssets
}
private getUnique<T>(array: T[]) {
return [...new Set<T>(array)]
} }
// Method to handle individual zoneEditor loading // Method to handle individual zoneEditor loading
public async loadZone(zone: Zone) { public async loadZone(zone: Zone) {
const loadedZone = new LoadedZone(zone) const loadedZone = new LoadedZone(zone)
this.loadedZones.push(loadedZone) this.loadedZones.push(loadedZone)
await this.getZoneAssets(zone) gameLogger.info(`Zone ID ${zone.id} loaded`)
gameMasterLogger.info(`Zone ID ${zone.id} loaded`)
} }
// Method to handle individual zoneEditor unloading // Method to handle individual zoneEditor unloading
public unloadZone(zoneId: number) { public unloadZone(zoneId: number) {
this.loadedZones = this.loadedZones.filter((loadedZone) => loadedZone.getZone().id !== zoneId) this.loadedZones = this.loadedZones.filter((loadedZone) => loadedZone.getZone().id !== zoneId)
gameMasterLogger.info(`Zone ID ${zoneId} unloaded`) gameLogger.info(`Zone ID ${zoneId} unloaded`)
} }
// Getter for loaded zones // Getter for loaded zones

View File

@ -1,7 +1,5 @@
import { Zone } from '@prisma/client' import { Zone } from '@prisma/client'
import zoneRepository from '../repositories/zoneRepository' import zoneRepository from '../repositories/zoneRepository'
import characterManager from '../managers/characterManager'
import { ExtendedCharacter } from '../utilities/types'
class LoadedZone { class LoadedZone {
private readonly zone: Zone private readonly zone: Zone

View File

@ -0,0 +1,10 @@
import prisma from '../utilities/prisma' // Import the global Prisma instance
import { CharacterType } from '@prisma/client'
class CharacterTypeRepository {
async getAll(): Promise<CharacterType[]> {
return prisma.characterType.findMany()
}
}
export default new CharacterTypeRepository()

View File

@ -1,5 +1,5 @@
import prisma from '../utilities/prisma' // Import the global Prisma instance import prisma from '../utilities/prisma' // Import the global Prisma instance
import { Sprite, SpriteAction } from '@prisma/client' import { SpriteAction } from '@prisma/client'
class SpriteRepository { class SpriteRepository {
async getById(id: string) { async getById(id: string) {

View File

@ -2,6 +2,8 @@ import { Zone, ZoneEventTile, ZoneEventTileType, ZoneObject } from '@prisma/clie
import prisma from '../utilities/prisma' import prisma from '../utilities/prisma'
import { ZoneEventTileWithTeleport } from '../socketEvents/zone/characterMove' import { ZoneEventTileWithTeleport } from '../socketEvents/zone/characterMove'
import { appLogger } from '../utilities/logger' import { appLogger } from '../utilities/logger'
import { TAsset } from '../utilities/types'
import tileRepository from './tileRepository'
class ZoneRepository { class ZoneRepository {
async getFirst(): Promise<Zone | null> { async getFirst(): Promise<Zone | null> {
@ -22,7 +24,7 @@ class ZoneRepository {
} }
} }
async getById(id: number): Promise<Zone | null> { async getById(id: number) {
try { try {
return await prisma.zone.findUnique({ return await prisma.zone.findUnique({
where: { where: {
@ -39,7 +41,8 @@ class ZoneRepository {
include: { include: {
object: true object: true
} }
} },
zoneEffects: true
} }
}) })
} catch (error: any) { } catch (error: any) {
@ -76,7 +79,7 @@ class ZoneRepository {
} }
} }
async getObjects(id: number): Promise<ZoneObject[]> { async getZoneObjects(id: number): Promise<ZoneObject[]> {
try { try {
return await prisma.zoneObject.findMany({ return await prisma.zoneObject.findMany({
where: { where: {
@ -88,6 +91,52 @@ class ZoneRepository {
return [] 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() export default new ZoneRepository()

View File

@ -1,21 +1,21 @@
import fs from 'fs' import fs from 'fs'
import path from 'path'
import express, { Application } from 'express' import express, { Application } from 'express'
import config from './utilities/config'
import { getAppPath } from './utilities/storage'
import { createServer as httpServer, Server as HTTPServer } from 'http' import { createServer as httpServer, Server as HTTPServer } from 'http'
import { addHttpRoutes } from './utilities/http' import { addHttpRoutes } from './utilities/http'
import cors from 'cors' import cors from 'cors'
import { Server as SocketServer } from 'socket.io' import { Server as SocketServer } from 'socket.io'
import { Authentication } from './middleware/authentication'
import { TSocket } from './utilities/types' import { TSocket } from './utilities/types'
import config from './utilities/config'
import prisma from './utilities/prisma' import prisma from './utilities/prisma'
import { appLogger, watchLogs } from './utilities/logger'
import ZoneManager from './managers/zoneManager' import ZoneManager from './managers/zoneManager'
import UserManager from './managers/userManager' import UserManager from './managers/userManager'
import { Authentication } from './middleware/authentication' import CommandManager from './managers/commandManager'
// import CommandManager from './managers/CommandManager'
import { Dirent } from 'node:fs'
import { appLogger, watchLogs } from './utilities/logger'
import CharacterManager from './managers/characterManager' import CharacterManager from './managers/characterManager'
import QueueManager from './managers/queueManager' import QueueManager from './managers/queueManager'
import DateManager from './managers/dateManager'
export class Server { export class Server {
private readonly app: Application private readonly app: Application
@ -58,23 +58,26 @@ export class Server {
appLogger.error(`Socket.IO failed to start: ${error.message}`) appLogger.error(`Socket.IO failed to start: ${error.message}`)
} }
// Load queue manager
await QueueManager.boot(this.io)
// Add http API routes // Add http API routes
await addHttpRoutes(this.app) await addHttpRoutes(this.app)
// Load queue manager
await QueueManager.boot(this.io)
// Load user manager // Load user manager
await UserManager.boot() await UserManager.boot()
// Load date manager
await DateManager.boot(this.io)
// Load zoneEditor manager // Load zoneEditor manager
await ZoneManager.boot() await ZoneManager.boot()
// Load character manager // Load character manager
await CharacterManager.boot() await CharacterManager.boot()
// Load command manager - Disabled for now // Load command manager
// await CommandManager.boot(this.io); await CommandManager.boot(this.io)
// Listen for socket connections // Listen for socket connections
this.io.on('connection', this.handleConnection.bind(this)) this.io.on('connection', this.handleConnection.bind(this))
@ -86,42 +89,46 @@ export class Server {
* @private * @private
*/ */
private async handleConnection(socket: TSocket) { private async handleConnection(socket: TSocket) {
const eventsPath = path.join(__dirname, 'socketEvents')
try { try {
await this.loadEventHandlers(eventsPath, socket) await this.loadEventHandlers('socketEvents', '', socket)
} catch (error: any) { } catch (error: any) {
appLogger.error(`Failed to load event handlers: ${error.message}`) appLogger.error(`Failed to load event handlers: ${error.message}`)
} }
} }
private async loadEventHandlers(dir: string, socket: TSocket) { private async loadEventHandlers(baseDir: string, subDir: string, socket: TSocket) {
const files: Dirent[] = await fs.promises.readdir(dir, { withFileTypes: true }) try {
const fullDir = getAppPath(baseDir, subDir)
const files = await fs.promises.readdir(fullDir, { withFileTypes: true })
for (const file of files) { for (const file of files) {
const fullPath = path.join(dir, file.name) const filePath = getAppPath(baseDir, subDir, file.name)
if (file.isDirectory()) {
await this.loadEventHandlers(baseDir, `${subDir}/${file.name}`, socket)
continue
}
if (!file.isFile() || (!file.name.endsWith('.ts') && !file.name.endsWith('.js'))) {
continue
}
if (file.isDirectory()) {
await this.loadEventHandlers(fullPath, socket)
} else if (file.isFile() && (file.name.endsWith('.ts') || file.name.endsWith('.js'))) {
try { try {
const module = await import(fullPath) const module = await import(filePath)
if (typeof module.default === 'function') { if (typeof module.default !== 'function') {
if (module.default.prototype && module.default.prototype.listen) {
// This is a class-based event
const EventClass = module.default
const eventInstance = new EventClass(this.io, socket)
eventInstance.listen()
} else {
// This is a function-based event
module.default(this.io, socket)
}
} else {
appLogger.warn(`Unrecognized export in ${file.name}`) appLogger.warn(`Unrecognized export in ${file.name}`)
continue
} }
} catch (error: any) {
appLogger.error(`Error loading event handler ${file.name}: ${error.message}`) const EventClass = module.default
const eventInstance = new EventClass(this.io, socket)
eventInstance.listen()
} catch (error) {
appLogger.error(`Error loading event handler ${file.name}: ${error instanceof Error ? error.message : String(error)}`)
} }
} }
} catch (error) {
appLogger.error(`Error reading directory: ${error instanceof Error ? error.message : String(error)}`)
} }
} }
} }

View File

@ -1,3 +0,0 @@
class AssetService {}
export default AssetService

View File

@ -1,5 +0,0 @@
import { Character } from '@prisma/client'
class CharacterService {}
export default CharacterService

View File

@ -23,14 +23,17 @@ export class ZoneEventTileService {
data: { data: {
zoneId: newZoneId, zoneId: newZoneId,
positionX: teleport.toPositionX, positionX: teleport.toPositionX,
positionY: teleport.toPositionY positionY: teleport.toPositionY,
rotation: teleport.toRotation
} }
}) })
// Update local character object // Update local character object
character.zoneId = newZoneId character.zoneId = newZoneId
character.rotation = teleport.toRotation
character.positionX = teleport.toPositionX character.positionX = teleport.toPositionX
character.positionY = teleport.toPositionY character.positionY = teleport.toPositionY
character.isMoving = false
// Emit events // Emit events
io.to(oldZoneId.toString()).emit('zone:character:leave', character.id) io.to(oldZoneId.toString()).emit('zone:character:leave', character.id)

View File

@ -1,24 +1,31 @@
import { Server } from 'socket.io' import { Server } from 'socket.io'
import { TSocket, ExtendedCharacter } from '../../utilities/types' import { TSocket } from '../../utilities/types'
import CharacterRepository from '../../repositories/characterRepository' import CharacterRepository from '../../repositories/characterRepository'
import CharacterManager from '../../managers/characterManager'
type SocketResponseT = { type SocketResponseT = {
character_id: number character_id: number
} }
export default function (io: Server, socket: TSocket) { export default class CharacterConnectEvent {
socket.on('character:connect', async (data: SocketResponseT) => { constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void {
this.socket.on('character:connect', this.handleCharacterConnect.bind(this))
}
private async handleCharacterConnect(data: SocketResponseT): Promise<void> {
console.log('character:connect requested', data) console.log('character:connect requested', data)
try { try {
const character = await CharacterRepository.getByUserAndId(socket?.user?.id as number, data.character_id) const character = await CharacterRepository.getByUserAndId(this.socket?.user?.id as number, data.character_id)
if (!character) return if (!character) return
socket.characterId = character.id
CharacterManager.initCharacter(character as ExtendedCharacter) this.socket.characterId = character.id
socket.emit('character:connect', character) this.socket.emit('character:connect', character)
} catch (error: any) { } catch (error: any) {
console.log('character:connect error', error) console.log('character:connect error', error)
} }
}) }
} }

View File

@ -5,27 +5,37 @@ import CharacterRepository from '../../repositories/characterRepository'
import { ZCharacterCreate } from '../../utilities/zodTypes' import { ZCharacterCreate } from '../../utilities/zodTypes'
import prisma from '../../utilities/prisma' import prisma from '../../utilities/prisma'
import { gameLogger } from '../../utilities/logger' import { gameLogger } from '../../utilities/logger'
import { ZodError } from 'zod'
export default function (io: Server, socket: TSocket) { export default class CharacterCreateEvent {
socket.on('character:create', async (data: any) => { constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void {
this.socket.on('character:create', this.handleCharacterCreate.bind(this))
}
private async handleCharacterCreate(data: any): Promise<any> {
console.log('character:create requested', data) console.log('character:create requested', data)
// zod validate // zod validate
try { try {
data = ZCharacterCreate.parse(data) data = ZCharacterCreate.parse(data)
const user_id = socket.user?.id as number const user_id = this.socket.user?.id as number
// Check if character name already exists // Check if character name already exists
const characterExists = await CharacterRepository.getByName(data.name) const characterExists = await CharacterRepository.getByName(data.name)
if (characterExists) { if (characterExists) {
return socket.emit('notification', { message: 'Character name already exists' }) return this.socket.emit('notification', { message: 'Character name already exists' })
} }
let characters: Character[] = (await CharacterRepository.getByUserId(user_id)) as Character[] let characters: Character[] = (await CharacterRepository.getByUserId(user_id)) as Character[]
if (characters.length >= 4) { if (characters.length >= 4) {
return socket.emit('notification', { message: 'You can only have 4 characters' }) return this.socket.emit('notification', { message: 'You can only have 4 characters' })
} }
const character: Character = await prisma.character.create({ const character: Character = await prisma.character.create({
@ -38,14 +48,16 @@ export default function (io: Server, socket: TSocket) {
characters = [...characters, character] characters = [...characters, character]
socket.emit('character:create:success') this.socket.emit('character:create:success')
socket.emit('character:list', characters) this.socket.emit('character:list', characters)
gameLogger.info('character:create success') gameLogger.info('character:create success')
} catch (error: any) { } catch (error: any) {
console.log(error)
gameLogger.error(`character:create error: ${error.message}`) gameLogger.error(`character:create error: ${error.message}`)
return socket.emit('notification', { message: 'Could not create character. Please try again (later).' }) if (error instanceof ZodError) {
return this.socket.emit('notification', { message: error.issues[0].message })
}
return this.socket.emit('notification', { message: 'Could not create character. Please try again (later).' })
} }
}) }
} }

View File

@ -12,19 +12,28 @@ type TypeResponse = {
characters: Character[] characters: Character[]
} }
export default function (io: Server, socket: TSocket) { export default class CharacterDeleteEvent {
socket.on('character:delete', async (data: TypePayload, callback: (response: TypeResponse) => void) => { constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void {
this.socket.on('character:delete', this.handleCharacterDelete.bind(this))
}
private async handleCharacterDelete(data: TypePayload, callback: (response: TypeResponse) => void): Promise<any> {
// zod validate // zod validate
try { try {
await CharacterRepository.deleteByUserIdAndId(socket.user?.id as number, data.character_id as number) await CharacterRepository.deleteByUserIdAndId(this.socket.user?.id as number, data.character_id as number)
const user_id = socket.user?.id as number 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(user_id)) as Character[]
socket.emit('character:list', characters) this.socket.emit('character:list', characters)
} catch (error: any) { } catch (error: any) {
console.log(error) console.log(error)
return socket.emit('notification', { message: 'Character delete failed. Please try again.' }) return this.socket.emit('notification', { message: 'Character delete failed. Please try again.' })
} }
}) }
} }

View File

@ -3,15 +3,24 @@ import { TSocket } from '../../utilities/types'
import { Character } from '@prisma/client' import { Character } from '@prisma/client'
import CharacterRepository from '../../repositories/characterRepository' import CharacterRepository from '../../repositories/characterRepository'
export default function CharacterList(io: Server, socket: TSocket) { export default class CharacterListEvent {
socket.on('character:list', async (data: any) => { constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void {
this.socket.on('character:list', this.handleCharacterList.bind(this))
}
private async handleCharacterList(data: any): Promise<void> {
try { try {
console.log('character:list requested') console.log('character:list requested')
const user_id = socket.user?.id as number 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(user_id)) as Character[]
socket.emit('character:list', characters) this.socket.emit('character:list', characters)
} catch (error: any) { } catch (error: any) {
console.log('character:list error', error) console.log('character:list error', error)
} }
}) }
} }

View File

@ -45,4 +45,4 @@ export default class AlertCommandEvent {
callback(false) callback(false)
} }
} }
} }

View File

@ -1,9 +1,9 @@
import { Server } from 'socket.io' import { Server } from 'socket.io'
import { TSocket } from '../../utilities/types' import { TSocket } from '../../utilities/types'
import CharacterRepository from '../../repositories/characterRepository'
import ZoneRepository from '../../repositories/zoneRepository' import ZoneRepository from '../../repositories/zoneRepository'
import { isCommand } from '../../utilities/chat' import { isCommand } from '../../utilities/chat'
import { gameLogger } from '../../utilities/logger' import { gameLogger } from '../../utilities/logger'
import CharacterManager from '../../managers/characterManager'
type TypePayload = { type TypePayload = {
message: string message: string
@ -26,7 +26,7 @@ export default class ChatMessageEvent {
return return
} }
const character = await CharacterRepository.getByUserAndId(this.socket.user?.id as number, this.socket.characterId as number) const character = CharacterManager.getCharacterFromSocket(this.socket)
if (!character) { if (!character) {
gameLogger.error('chat:send_message error', 'Character not found') gameLogger.error('chat:send_message error', 'Character not found')
callback(false) callback(false)

View File

@ -29,6 +29,8 @@ export default class DisconnectEvent {
return return
} }
character.resetMovement = true
gameLogger.info('User disconnected along with their character') gameLogger.info('User disconnected along with their character')
await CharacterManager.removeCharacter(character) await CharacterManager.removeCharacter(character)
@ -39,4 +41,4 @@ export default class DisconnectEvent {
gameLogger.error('disconnect error', error.message) gameLogger.error('disconnect error', error.message)
} }
} }
} }

View File

@ -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 { CharacterGender, CharacterRace } from '@prisma/client'
export default class CharacterTypeCreateEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void {
this.socket.on('gm:characterType:create', this.handleCharacterTypeCreate.bind(this))
}
private async handleCharacterTypeCreate(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 newCharacterType = await prisma.characterType.create({
data: {
name: 'New character type',
gender: CharacterGender.MALE,
race: CharacterRace.HUMAN
}
})
callback(true, newCharacterType)
} catch (error) {
console.error('Error creating character type:', error)
callback(false)
}
}
}

View File

@ -0,0 +1,36 @@
import { Server } from 'socket.io'
import { TSocket } from '../../../../utilities/types'
import { CharacterType } from '@prisma/client'
import characterRepository from '../../../../repositories/characterRepository'
import { gameMasterLogger } from '../../../../utilities/logger'
import CharacterTypeRepository from '../../../../repositories/characterTypeRepository'
interface IPayload {}
export default class CharacterTypeListEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void {
this.socket.on('gm:characterType:list', this.handleCharacterTypeList.bind(this))
}
private async handleCharacterTypeList(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')
return callback([])
}
if (character.role !== 'gm') {
gameMasterLogger.info(`User ${character.id} tried to list character types but is not a game master.`)
return callback([])
}
// get all objects
const items = await CharacterTypeRepository.getAll()
callback(items)
}
}

View File

@ -0,0 +1,56 @@
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)
}
}
}

View File

@ -0,0 +1,58 @@
import { Server } from 'socket.io'
import { TSocket } from '../../../../utilities/types'
import prisma from '../../../../utilities/prisma'
import characterRepository from '../../../../repositories/characterRepository'
type Payload = {
id: string
name: string
tags: string[]
originX: number
originY: number
isAnimated: boolean
frameSpeed: number
frameWidth: number
frameHeight: number
}
export default class ObjectUpdateEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void {
this.socket.on('gm:object: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 {
const object = await prisma.object.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
}
})
callback(true)
} catch (error) {
console.error(error)
callback(false)
}
}
}

View File

@ -2,19 +2,22 @@ import { Server } from 'socket.io'
import { TSocket } from '../../../../utilities/types' import { TSocket } from '../../../../utilities/types'
import { Object } from '@prisma/client' import { Object } from '@prisma/client'
import ObjectRepository from '../../../../repositories/objectRepository' import ObjectRepository from '../../../../repositories/objectRepository'
import CharacterManager from '../../../../managers/characterManager'
import characterRepository from '../../../../repositories/characterRepository' import characterRepository from '../../../../repositories/characterRepository'
interface IPayload {} interface IPayload {}
/** export default class ObjectListEvent {
* Handle game master list object event constructor(
* @param socket private readonly io: Server,
* @param io private readonly socket: TSocket
*/ ) {}
export default function (io: Server, socket: TSocket) {
socket.on('gm:object:list', async (data: any, callback: (response: Object[]) => void) => { public listen(): void {
const character = await characterRepository.getById(socket.characterId as number) this.socket.on('gm:object:list', this.handleObjectList.bind(this))
}
private async handleObjectList(data: IPayload, callback: (response: Object[]) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) return callback([]) if (!character) return callback([])
if (character.role !== 'gm') { if (character.role !== 'gm') {
@ -24,5 +27,5 @@ export default function (io: Server, socket: TSocket) {
// get all objects // get all objects
const objects = await ObjectRepository.getAll() const objects = await ObjectRepository.getAll()
callback(objects) callback(objects)
}) }
} }

View File

@ -1,23 +1,26 @@
import fs from 'fs'
import { Server } from 'socket.io' import { Server } from 'socket.io'
import { TSocket } from '../../../../utilities/types' import { TSocket } from '../../../../utilities/types'
import path from 'path'
import fs from 'fs'
import prisma from '../../../../utilities/prisma' import prisma from '../../../../utilities/prisma'
import CharacterManager from '../../../../managers/characterManager'
import characterRepository from '../../../../repositories/characterRepository' import characterRepository from '../../../../repositories/characterRepository'
import { getPublicPath } from '../../../../utilities/storage'
interface IPayload { interface IPayload {
object: string object: string
} }
/** export default class ObjectRemoveEvent {
* Handle game master remove object event constructor(
* @param socket private readonly io: Server,
* @param io private readonly socket: TSocket
*/ ) {}
export default function (io: Server, socket: TSocket) {
socket.on('gm:object:remove', async (data: IPayload, callback: (response: boolean) => void) => { public listen(): void {
const character = await characterRepository.getById(socket.characterId as number) 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) return callback(false)
if (character.role !== 'gm') { if (character.role !== 'gm') {
@ -32,10 +35,10 @@ export default function (io: Server, socket: TSocket) {
}) })
// get root path // get root path
const public_folder = path.join(process.cwd(), 'public', 'objects') const public_folder = getPublicPath('objects')
// remove the tile from the disk // remove the tile from the disk
const finalFilePath = path.join(public_folder, data.object + '.png') const finalFilePath = getPublicPath('objects', data.object + '.png')
fs.unlink(finalFilePath, (err) => { fs.unlink(finalFilePath, (err) => {
if (err) { if (err) {
console.log(err) console.log(err)
@ -49,5 +52,5 @@ export default function (io: Server, socket: TSocket) {
console.log(e) console.log(e)
callback(false) callback(false)
} }
}) }
} }

View File

@ -1,7 +1,6 @@
import { Server } from 'socket.io' import { Server } from 'socket.io'
import { TSocket } from '../../../../utilities/types' import { TSocket } from '../../../../utilities/types'
import prisma from '../../../../utilities/prisma' import prisma from '../../../../utilities/prisma'
import CharacterManager from '../../../../managers/characterManager'
import characterRepository from '../../../../repositories/characterRepository' import characterRepository from '../../../../repositories/characterRepository'
type Payload = { type Payload = {
@ -16,14 +15,18 @@ type Payload = {
frameHeight: number frameHeight: number
} }
/** export default class ObjectUpdateEvent {
* Handle game master object update event constructor(
* @param socket private readonly io: Server,
* @param io private readonly socket: TSocket
*/ ) {}
export default function (io: Server, socket: TSocket) {
socket.on('gm:object:update', async (data: Payload, callback: (success: boolean) => void) => { public listen(): void {
const character = await characterRepository.getById(socket.characterId as number) this.socket.on('gm:object: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) return callback(false)
if (character.role !== 'gm') { if (character.role !== 'gm') {
@ -51,5 +54,5 @@ export default function (io: Server, socket: TSocket) {
console.error(error) console.error(error)
callback(false) callback(false)
} }
}) }
} }

View File

@ -1,12 +1,12 @@
import { Server } from 'socket.io' import { Server } from 'socket.io'
import { TSocket } from '../../../../utilities/types' import { TSocket } from '../../../../utilities/types'
import { writeFile } from 'node:fs/promises' import { writeFile } from 'node:fs/promises'
import path from 'path'
import fs from 'fs/promises' import fs from 'fs/promises'
import prisma from '../../../../utilities/prisma' import prisma from '../../../../utilities/prisma'
import sharp from 'sharp' import sharp from 'sharp'
import characterRepository from '../../../../repositories/characterRepository' import characterRepository from '../../../../repositories/characterRepository'
import { gameMasterLogger } from '../../../../utilities/logger' import { gameMasterLogger } from '../../../../utilities/logger'
import { getPublicPath } from '../../../../utilities/storage'
interface IObjectData { interface IObjectData {
[key: string]: Buffer [key: string]: Buffer
@ -30,7 +30,7 @@ export default class ObjectUploadEvent {
if (character.role !== 'gm') { if (character.role !== 'gm') {
return callback(false) return callback(false)
} }
const public_folder = path.join(process.cwd(), 'public', 'objects') const public_folder = getPublicPath('objects')
// Ensure the folder exists // Ensure the folder exists
await fs.mkdir(public_folder, { recursive: true }) await fs.mkdir(public_folder, { recursive: true })
@ -54,7 +54,7 @@ export default class ObjectUploadEvent {
const uuid = object.id const uuid = object.id
const filename = `${uuid}.png` const filename = `${uuid}.png`
const finalFilePath = path.join(public_folder, filename) const finalFilePath = getPublicPath('objects', filename)
await writeFile(finalFilePath, objectData) await writeFile(finalFilePath, objectData)
gameMasterLogger.info('gm:object:upload', `Object ${key} uploaded with id ${uuid}`) gameMasterLogger.info('gm:object:upload', `Object ${key} uploaded with id ${uuid}`)

View File

@ -1,27 +1,30 @@
import { Server } from 'socket.io' import { Server } from 'socket.io'
import { TSocket } from '../../../../utilities/types' import { TSocket } from '../../../../utilities/types'
import path from 'path'
import fs from 'fs/promises' import fs from 'fs/promises'
import prisma from '../../../../utilities/prisma' import prisma from '../../../../utilities/prisma'
import CharacterManager from '../../../../managers/characterManager'
import characterRepository from '../../../../repositories/characterRepository' import characterRepository from '../../../../repositories/characterRepository'
import { getPublicPath } from '../../../../utilities/storage'
/** export default class SpriteCreateEvent {
* Handle game master new sprite event constructor(
* @param socket private readonly io: Server,
* @param io private readonly socket: TSocket
*/ ) {}
export default function (io: Server, socket: TSocket) {
socket.on('gm:sprite:create', async (data: undefined, callback: (response: boolean) => void) => { public listen(): void {
this.socket.on('gm:sprite:create', this.handleSpriteCreate.bind(this))
}
private async handleSpriteCreate(data: undefined, callback: (response: boolean) => void): Promise<void> {
try { try {
const character = await characterRepository.getById(socket.characterId as number) const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) return callback(false) if (!character) return callback(false)
if (character.role !== 'gm') { if (character.role !== 'gm') {
return callback(false) return callback(false)
} }
const public_folder = path.join(process.cwd(), 'public', 'sprites') const public_folder = getPublicPath('sprites')
// Ensure the folder exists // Ensure the folder exists
await fs.mkdir(public_folder, { recursive: true }) await fs.mkdir(public_folder, { recursive: true })
@ -34,7 +37,7 @@ export default function (io: Server, socket: TSocket) {
const uuid = sprite.id const uuid = sprite.id
// Create folder with uuid // Create folder with uuid
const sprite_folder = path.join(public_folder, uuid) const sprite_folder = getPublicPath('sprites', uuid)
await fs.mkdir(sprite_folder, { recursive: true }) await fs.mkdir(sprite_folder, { recursive: true })
callback(true) callback(true)
@ -42,5 +45,5 @@ export default function (io: Server, socket: TSocket) {
console.error('Error creating sprite:', error) console.error('Error creating sprite:', error)
callback(false) callback(false)
} }
}) }
} }

View File

@ -1,10 +1,10 @@
import { Server } from 'socket.io' import { Server } from 'socket.io'
import { TSocket } from '../../../../utilities/types' import { TSocket } from '../../../../utilities/types'
import fs from 'fs' import fs from 'fs'
import path from 'path'
import prisma from '../../../../utilities/prisma' import prisma from '../../../../utilities/prisma'
import CharacterManager from '../../../../managers/characterManager' import CharacterManager from '../../../../managers/characterManager'
import { gameMasterLogger } from '../../../../utilities/logger' import { gameMasterLogger } from '../../../../utilities/logger'
import { getPublicPath } from '../../../../utilities/storage'
type Payload = { type Payload = {
id: string id: string
@ -17,7 +17,7 @@ export default class GMSpriteDeleteEvent {
private readonly io: Server, private readonly io: Server,
private readonly socket: TSocket private readonly socket: TSocket
) { ) {
this.public_folder = path.join(process.cwd(), 'public', 'sprites') this.public_folder = getPublicPath('sprites')
} }
public listen(): void { public listen(): void {
@ -43,7 +43,7 @@ export default class GMSpriteDeleteEvent {
} }
private async deleteSpriteFolder(spriteId: string): Promise<void> { private async deleteSpriteFolder(spriteId: string): Promise<void> {
const finalFilePath = path.join(this.public_folder, spriteId) const finalFilePath = getPublicPath('sprites', spriteId)
if (fs.existsSync(finalFilePath)) { if (fs.existsSync(finalFilePath)) {
await fs.promises.rmdir(finalFilePath, { recursive: true }) await fs.promises.rmdir(finalFilePath, { recursive: true })

View File

@ -2,19 +2,22 @@ import { Server } from 'socket.io'
import { TSocket } from '../../../../utilities/types' import { TSocket } from '../../../../utilities/types'
import { Sprite } from '@prisma/client' import { Sprite } from '@prisma/client'
import SpriteRepository from '../../../../repositories/spriteRepository' import SpriteRepository from '../../../../repositories/spriteRepository'
import CharacterManager from '../../../../managers/characterManager'
import characterRepository from '../../../../repositories/characterRepository' import characterRepository from '../../../../repositories/characterRepository'
interface IPayload {} interface IPayload {}
/** export default class SpriteListEvent {
* Handle game master list sprite event constructor(
* @param socket private readonly io: Server,
* @param io private readonly socket: TSocket
*/ ) {}
export default function (io: Server, socket: TSocket) {
socket.on('gm:sprite:list', async (data: any, callback: (response: Sprite[]) => void) => { public listen(): void {
const character = await characterRepository.getById(socket.characterId as number) this.socket.on('gm:sprite:list', this.handleSpriteList.bind(this))
}
private async handleSpriteList(data: any, callback: (response: Sprite[]) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) return callback([]) if (!character) return callback([])
if (character.role !== 'gm') { if (character.role !== 'gm') {
@ -24,5 +27,5 @@ export default function (io: Server, socket: TSocket) {
// get all sprites // get all sprites
const sprites = await SpriteRepository.getAll() const sprites = await SpriteRepository.getAll()
callback(sprites) callback(sprites)
}) }
} }

View File

@ -2,10 +2,10 @@ import { Server } from 'socket.io'
import { TSocket } from '../../../../utilities/types' import { TSocket } from '../../../../utilities/types'
import prisma from '../../../../utilities/prisma' import prisma from '../../../../utilities/prisma'
import type { Prisma, SpriteAction } from '@prisma/client' import type { Prisma, SpriteAction } from '@prisma/client'
import path from 'path'
import { writeFile, mkdir } from 'node:fs/promises' import { writeFile, mkdir } from 'node:fs/promises'
import sharp from 'sharp' import sharp from 'sharp'
import CharacterManager from '../../../../managers/characterManager' import CharacterManager from '../../../../managers/characterManager'
import { getPublicPath } from '../../../../utilities/storage'
type SpriteActionInput = Omit<SpriteAction, 'id' | 'spriteId' | 'frameWidth' | 'frameHeight'> & { type SpriteActionInput = Omit<SpriteAction, 'id' | 'spriteId' | 'frameWidth' | 'frameHeight'> & {
sprites: string[] sprites: string[]
@ -27,9 +27,18 @@ interface ProcessedSpriteAction extends SpriteActionInput {
}> }>
} }
export default function (io: Server, socket: TSocket) { export default class SpriteUpdateEvent {
socket.on('gm:sprite:update', async (data: Payload, callback: (success: boolean) => void) => { constructor(
const character = CharacterManager.getCharacterFromSocket(socket) private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void {
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') { if (character?.role !== 'gm') {
return callback(false) return callback(false)
} }
@ -46,101 +55,101 @@ export default function (io: Server, socket: TSocket) {
console.error('Error updating sprite:', error) console.error('Error updating sprite:', error)
callback(false) callback(false)
} }
})
}
function validateSpriteActions(spriteActions: Prisma.JsonValue): SpriteActionInput[] { function validateSpriteActions(spriteActions: Prisma.JsonValue): SpriteActionInput[] {
try { try {
const parsed = JSON.parse(JSON.stringify(spriteActions)) as SpriteActionInput[] const parsed = JSON.parse(JSON.stringify(spriteActions)) as SpriteActionInput[]
if (!Array.isArray(parsed)) { if (!Array.isArray(parsed)) {
throw new Error('spriteActions is not an array') throw new Error('spriteActions is not an array')
} }
return parsed return parsed
} catch (error) { } catch (error) {
console.error('Error parsing spriteActions:', error) console.error('Error parsing spriteActions:', error)
throw 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( async function processSprites(spriteActions: SpriteActionInput[]): Promise<ProcessedSpriteAction[]> {
sprites.map(async (sprite: string) => { return Promise.all(
const buffer = Buffer.from(sprite.split(',')[1], 'base64') spriteActions.map(async (spriteAction) => {
const { width, height } = await sharp(buffer).metadata() const { action, sprites } = spriteAction
return { buffer, width, height }
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
}
}) })
) )
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[]) { async function updateDatabase(id: string, name: string, processedActions: ProcessedSpriteAction[]) {
const publicFolder = path.join(process.cwd(), 'public', 'sprites', id) await prisma.sprite.update({
await mkdir(publicFolder, { recursive: true }) where: { id },
data: {
await Promise.all( name,
processedActions.map(async ({ action, buffersWithDimensions, frameWidth, frameHeight }) => { spriteActions: {
const combinedImage = await sharp({ deleteMany: { spriteId: id },
create: { create: processedActions.map(({ action, sprites, originX, originY, isAnimated, isLooping, frameWidth, frameHeight, frameSpeed }) => ({
width: frameWidth * buffersWithDimensions.length, action,
height: frameHeight, sprites,
channels: 4, originX,
background: { r: 0, g: 0, b: 0, alpha: 0 } originY,
isAnimated,
isLooping,
frameWidth,
frameHeight,
frameSpeed
}))
}
} }
}) })
.composite( }
buffersWithDimensions.map(({ buffer }, index) => ({
input: buffer,
left: index * frameWidth,
top: 0
}))
)
.png()
.toBuffer()
const filename = path.join(publicFolder, `${action}.png`) async function saveSpritesToDisk(id: string, processedActions: ProcessedSpriteAction[]) {
await writeFile(filename, combinedImage) 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)
})
)
}
}
} }

View File

@ -1,10 +1,10 @@
import path from 'path'
import fs from 'fs/promises' import fs from 'fs/promises'
import { Server } from 'socket.io' import { Server } from 'socket.io'
import { TSocket } from '../../../../utilities/types' import { TSocket } from '../../../../utilities/types'
import prisma from '../../../../utilities/prisma' import prisma from '../../../../utilities/prisma'
import characterRepository from '../../../../repositories/characterRepository' import characterRepository from '../../../../repositories/characterRepository'
import { gameMasterLogger } from '../../../../utilities/logger' import { gameMasterLogger } from '../../../../utilities/logger'
import { getPublicPath } from '../../../../utilities/storage'
type Payload = { type Payload = {
id: string id: string
@ -17,7 +17,7 @@ export default class GMTileDeleteEvent {
private readonly io: Server, private readonly io: Server,
private readonly socket: TSocket private readonly socket: TSocket
) { ) {
this.public_folder = path.join(process.cwd(), 'public', 'tiles') this.public_folder = getPublicPath('tiles')
} }
public listen(): void { public listen(): void {
@ -54,7 +54,7 @@ export default class GMTileDeleteEvent {
} }
private async deleteTileFile(tileId: string): Promise<void> { private async deleteTileFile(tileId: string): Promise<void> {
const finalFilePath = path.join(this.public_folder, `${tileId}.png`) const finalFilePath = getPublicPath('tiles', `${tileId}.png`)
try { try {
await fs.unlink(finalFilePath) await fs.unlink(finalFilePath)
} catch (error: any) { } catch (error: any) {

View File

@ -2,19 +2,22 @@ import { Server } from 'socket.io'
import { TSocket } from '../../../../utilities/types' import { TSocket } from '../../../../utilities/types'
import { Tile } from '@prisma/client' import { Tile } from '@prisma/client'
import TileRepository from '../../../../repositories/tileRepository' import TileRepository from '../../../../repositories/tileRepository'
import CharacterManager from '../../../../managers/characterManager'
import characterRepository from '../../../../repositories/characterRepository' import characterRepository from '../../../../repositories/characterRepository'
interface IPayload {} interface IPayload {}
/** export default class TileListEvent {
* Handle game master list tile event constructor(
* @param socket private readonly io: Server,
* @param io private readonly socket: TSocket
*/ ) {}
export default function (io: Server, socket: TSocket) {
socket.on('gm:tile:list', async (data: any, callback: (response: Tile[]) => void) => { public listen(): void {
const character = await characterRepository.getById(socket.characterId as number) this.socket.on('gm:tile:list', this.handleTileList.bind(this))
}
private async handleTileList(data: any, callback: (response: Tile[]) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) return if (!character) return
if (character.role !== 'gm') { if (character.role !== 'gm') {
@ -24,5 +27,5 @@ export default function (io: Server, socket: TSocket) {
// get all tiles // get all tiles
const tiles = await TileRepository.getAll() const tiles = await TileRepository.getAll()
callback(tiles) callback(tiles)
}) }
} }

View File

@ -1,7 +1,6 @@
import { Server } from 'socket.io' import { Server } from 'socket.io'
import { TSocket } from '../../../../utilities/types' import { TSocket } from '../../../../utilities/types'
import prisma from '../../../../utilities/prisma' import prisma from '../../../../utilities/prisma'
import CharacterManager from '../../../../managers/characterManager'
import characterRepository from '../../../../repositories/characterRepository' import characterRepository from '../../../../repositories/characterRepository'
type Payload = { type Payload = {
@ -10,14 +9,18 @@ type Payload = {
tags: string[] tags: string[]
} }
/** export default class TileUpdateEvent {
* Handle game master tile update event constructor(
* @param socket private readonly io: Server,
* @param io private readonly socket: TSocket
*/ ) {}
export default function (io: Server, socket: TSocket) {
socket.on('gm:tile:update', async (data: Payload, callback: (success: boolean) => void) => { public listen(): void {
const character = await characterRepository.getById(socket.characterId as number) this.socket.on('gm:tile:update', this.handleTileUpdate.bind(this))
}
private async handleTileUpdate(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) return callback(false)
if (character.role !== 'gm') { if (character.role !== 'gm') {
@ -40,5 +43,5 @@ export default function (io: Server, socket: TSocket) {
console.error(error) console.error(error)
callback(false) callback(false)
} }
}) }
} }

View File

@ -1,32 +1,36 @@
import { Server } from 'socket.io' import { Server } from 'socket.io'
import { TSocket } from '../../../../utilities/types' import { TSocket } from '../../../../utilities/types'
import { writeFile } from 'node:fs/promises' import { writeFile } from 'node:fs/promises'
import path from 'path'
import fs from 'fs/promises' import fs from 'fs/promises'
import prisma from '../../../../utilities/prisma' import prisma from '../../../../utilities/prisma'
import characterRepository from '../../../../repositories/characterRepository' import characterRepository from '../../../../repositories/characterRepository'
import { gameMasterLogger } from '../../../../utilities/logger' import { gameMasterLogger } from '../../../../utilities/logger'
import { getPublicPath } from '../../../../utilities/storage'
interface ITileData { interface ITileData {
[key: string]: Buffer [key: string]: Buffer
} }
/** export default class TileUploadEvent {
* Handle game master upload tile event constructor(
* @param socket private readonly io: Server,
* @param io private readonly socket: TSocket
*/ ) {}
export default function (io: Server, socket: TSocket) {
socket.on('gm:tile:upload', async (data: ITileData, callback: (response: boolean) => void) => { public listen(): void {
this.socket.on('gm:tile:upload', this.handleTileUpload.bind(this))
}
private async handleTileUpload(data: ITileData, callback: (response: boolean) => void): Promise<void> {
try { try {
const character = await characterRepository.getById(socket.characterId as number) const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) return callback(false) if (!character) return callback(false)
if (character.role !== 'gm') { if (character.role !== 'gm') {
return return
} }
const public_folder = path.join(process.cwd(), 'public', 'tiles') const public_folder = getPublicPath('tiles')
// Ensure the folder exists // Ensure the folder exists
await fs.mkdir(public_folder, { recursive: true }) await fs.mkdir(public_folder, { recursive: true })
@ -39,7 +43,7 @@ export default function (io: Server, socket: TSocket) {
}) })
const uuid = tile.id const uuid = tile.id
const filename = `${uuid}.png` const filename = `${uuid}.png`
const finalFilePath = path.join(public_folder, filename) const finalFilePath = getPublicPath('tiles', filename)
await writeFile(finalFilePath, tileData) await writeFile(finalFilePath, tileData)
}) })
@ -50,5 +54,5 @@ export default function (io: Server, socket: TSocket) {
gameMasterLogger.error('Error uploading tile:', error) gameMasterLogger.error('Error uploading tile:', error)
callback(false) callback(false)
} }
}) }
} }

View File

@ -59,4 +59,4 @@ export default class ZoneCreateEvent {
callback([]) callback([])
} }
} }
} }

View File

@ -58,4 +58,4 @@ export default class ZoneDeleteEvent {
callback(false) callback(false)
} }
} }
} }

View File

@ -41,4 +41,4 @@ export default class ZoneListEvent {
callback([]) callback([])
} }
} }
} }

View File

@ -56,4 +56,4 @@ export default class ZoneRequestEvent {
callback(null) callback(null)
} }
} }
} }

View File

@ -1,7 +1,7 @@
import { Server } from 'socket.io' import { Server } from 'socket.io'
import { TSocket } from '../../../utilities/types' import { TSocket } from '../../../utilities/types'
import ZoneRepository from '../../../repositories/zoneRepository' import ZoneRepository from '../../../repositories/zoneRepository'
import { Zone, ZoneEventTileType, ZoneObject } from '@prisma/client' import { Zone, ZoneEffect, ZoneEventTileType, ZoneObject } from '@prisma/client'
import prisma from '../../../utilities/prisma' import prisma from '../../../utilities/prisma'
import zoneManager from '../../../managers/zoneManager' import zoneManager from '../../../managers/zoneManager'
import CharacterRepository from '../../../repositories/characterRepository' import CharacterRepository from '../../../repositories/characterRepository'
@ -22,8 +22,13 @@ interface IPayload {
toZoneId: number toZoneId: number
toPositionX: number toPositionX: number
toPositionY: number toPositionY: number
toRotation: number
} }
}[] }[]
zoneEffects: {
effect: string
strength: number
}[]
zoneObjects: ZoneObject[] zoneObjects: ZoneObject[]
} }
@ -84,14 +89,15 @@ export default class ZoneUpdateEvent {
positionY: zoneEventTile.positionY, positionY: zoneEventTile.positionY,
...(zoneEventTile.type === 'TELEPORT' && zoneEventTile.teleport ...(zoneEventTile.type === 'TELEPORT' && zoneEventTile.teleport
? { ? {
teleport: { teleport: {
create: { create: {
toZoneId: zoneEventTile.teleport.toZoneId, toZoneId: zoneEventTile.teleport.toZoneId,
toPositionX: zoneEventTile.teleport.toPositionX, toPositionX: zoneEventTile.teleport.toPositionX,
toPositionY: zoneEventTile.teleport.toPositionY toPositionY: zoneEventTile.teleport.toPositionY,
toRotation: zoneEventTile.teleport.toRotation
}
} }
} }
}
: {}) : {})
})) }))
}, },
@ -100,10 +106,19 @@ export default class ZoneUpdateEvent {
create: data.zoneObjects.map((zoneObject) => ({ create: data.zoneObjects.map((zoneObject) => ({
objectId: zoneObject.objectId, objectId: zoneObject.objectId,
depth: zoneObject.depth, depth: zoneObject.depth,
isRotated: zoneObject.isRotated,
positionX: zoneObject.positionX, positionX: zoneObject.positionX,
positionY: zoneObject.positionY positionY: zoneObject.positionY
})) }))
} },
zoneEffects: {
deleteMany: { zoneId: data.zoneId },
create: data.zoneEffects.map((zoneEffect) => ({
effect: zoneEffect.effect,
strength: zoneEffect.strength
}))
},
updatedAt: new Date()
} }
}) })
@ -115,6 +130,8 @@ export default class ZoneUpdateEvent {
return return
} }
gameMasterLogger.info(`User ${character.id} has updated zone via zone editor.`)
callback(zone) callback(zone)
zoneManager.unloadZone(data.zoneId) zoneManager.unloadZone(data.zoneId)
@ -124,4 +141,4 @@ export default class ZoneUpdateEvent {
callback(null) callback(null)
} }
} }
} }

View File

@ -25,4 +25,4 @@ export default class LoginEvent {
gameLogger.error('login error', error.message) gameLogger.error('login error', error.message)
} }
} }
} }

View File

@ -1,9 +1,10 @@
import { Server } from 'socket.io' import { Server } from 'socket.io'
import { TSocket } from '../../utilities/types' import { ExtendedCharacter, TSocket } from '../../utilities/types'
import ZoneRepository from '../../repositories/zoneRepository' import ZoneRepository from '../../repositories/zoneRepository'
import { Character, Zone } from '@prisma/client' import { Character, Zone } from '@prisma/client'
import CharacterManager from '../../managers/characterManager' import CharacterManager from '../../managers/characterManager'
import { gameLogger } from '../../utilities/logger' import { gameLogger } from '../../utilities/logger'
import CharacterRepository from '../../repositories/characterRepository'
interface IResponse { interface IResponse {
zone: Zone zone: Zone
@ -22,10 +23,16 @@ export default class CharacterJoinEvent {
private async handleCharacterJoin(callback: (response: IResponse) => void): Promise<void> { private async handleCharacterJoin(callback: (response: IResponse) => void): Promise<void> {
try { try {
if (!this.socket.characterId) return if (!this.socket.characterId) {
gameLogger.error('zone:character:join error', 'Zone requested but no character id set')
return
}
const character = CharacterManager.getCharacterFromSocket(this.socket) const character = await CharacterRepository.getById(this.socket.characterId as number)
if (!character) return if (!character) {
gameLogger.error('zone:character:join error', 'Character not found')
return
}
const zone = await ZoneRepository.getById(character.zoneId) const zone = await ZoneRepository.getById(character.zoneId)
if (!zone) { if (!zone) {
@ -33,16 +40,16 @@ export default class CharacterJoinEvent {
return return
} }
if (character.zoneId) { CharacterManager.initCharacter(character as ExtendedCharacter)
this.socket.leave(character.zoneId.toString())
this.io.to(character.zoneId.toString()).emit('zone:character:leave', character)
}
this.socket.join(zone.id.toString()) this.socket.join(zone.id.toString())
// let other clients know of new character // let other clients know of new character
this.io.to(zone.id.toString()).emit('zone:character:join', character) this.io.to(zone.id.toString()).emit('zone:character:join', character)
// Log
gameLogger.info(`User ${character.id} joined zone ${zone.id}`)
// send over zone and characters to socket // send over zone and characters to socket
callback({ zone, characters: CharacterManager.getCharactersInZone(zone) }) callback({ zone, characters: CharacterManager.getCharactersInZone(zone) })
} catch (error: any) { } catch (error: any) {
@ -50,4 +57,4 @@ export default class CharacterJoinEvent {
this.socket.disconnect() this.socket.disconnect()
} }
} }
} }

View File

@ -1,11 +1,11 @@
export function isCommand(message: string, command?: string) { export function isCommand(message: string, command?: string) {
if (command) { if (command) {
return message.startsWith(`:${command} `) return message.startsWith(`/${command} `)
} }
return message.startsWith(':') return message.startsWith('/')
} }
export function getArgs(command: string, message: string): string[] | undefined { export function getArgs(command: string, message: string): string[] | undefined {
if (!isCommand(message, command)) return if (!isCommand(message, command)) return
return message.split(`:${command} `)[1].split(' ') return message.split(`/${command} `)[1].split(' ')
} }

View File

@ -3,137 +3,12 @@ import UserService from '../services/userService'
import jwt from 'jsonwebtoken' import jwt from 'jsonwebtoken'
import config from './config' import config from './config'
import { loginAccountSchema, registerAccountSchema } from './zodTypes' import { loginAccountSchema, registerAccountSchema } from './zodTypes'
import path from 'path'
import { TAsset } from './types'
import tileRepository from '../repositories/tileRepository'
import objectRepository from '../repositories/objectRepository'
import spriteRepository from '../repositories/spriteRepository'
import fs from 'fs' import fs from 'fs'
import zoneRepository from '../repositories/zoneRepository'
import zoneManager from '../managers/zoneManager'
import { httpLogger } from './logger' import { httpLogger } from './logger'
import { getPublicPath } from './storage'
import zoneRepository from '../repositories/zoneRepository'
async function addHttpRoutes(app: Application) { async function addHttpRoutes(app: Application) {
/**
* Get all base sprite, assets
* @param req
* @param res
*/
app.get('/assets/sprites', async (req: Request, res: Response) => {
let assets: TAsset[] = []
const sprites = await spriteRepository.getAll()
// sprites all contain spriteActions, loop through these
sprites.forEach((sprite) => {
sprite.spriteActions.forEach((spriteAction) => {
assets.push({
key: sprite.id + '-' + spriteAction.action,
url: '/assets/sprites/' + sprite.id + '/' + spriteAction.action + '.png',
group: spriteAction.isAnimated ? 'sprite_animations' : 'sprites',
frameWidth: spriteAction.frameWidth,
frameHeight: spriteAction.frameHeight
})
})
})
res.json(assets)
})
/**
* Get all assets for all zones
* @param req
* @param res
*/
app.get('/assets/zone', async (req: Request, res: Response) => {
const tiles = await tileRepository.getAll()
const objects = await objectRepository.getAll()
const assets: TAsset[] = []
tiles.forEach((tile) => {
assets.push({
key: tile.id,
url: '/assets/tiles/' + tile.id + '.png',
group: 'tiles'
})
})
objects.forEach((object) => {
assets.push({
key: object.id,
url: '/assets/objects/' + object.id + '.png',
group: 'objects'
})
})
res.json(assets)
})
/**
* Get assets for a specific zone
* @param req
* @param res
*/
app.get('/assets/zone/:zoneId', async (req: Request, res: Response) => {
const zoneId = req.params.zoneId
if (!zoneId || parseInt(zoneId) === 0) {
return res.status(400).json({ message: 'Invalid zone ID' })
}
const zone = await zoneRepository.getById(parseInt(zoneId))
if (!zone) {
return res.status(404).json({ message: 'Zone not found' })
}
const assets = await zoneManager.getZoneAssets(zone)
res.json([
...assets.tiles.map((x) => {
return {
key: x,
url: '/assets/tiles/' + x + '.png',
group: 'tiles'
}
}),
...assets.objects.map((x) => {
return {
key: x,
url: '/assets/objects/' + x + '.png',
group: 'objects'
}
})
])
})
/**
* Get a specific asset
* @param req
* @param res
*/
app.get('/assets/:type/:spriteId?/:file', (req: Request, res: Response) => {
const assetType = req.params.type
const spriteId = req.params.spriteId
const fileName = req.params.file
let assetPath
if (assetType === 'sprites' && spriteId) {
assetPath = path.join(process.cwd(), 'public', assetType, spriteId, fileName)
} else {
assetPath = path.join(process.cwd(), 'public', assetType, fileName)
}
if (!fs.existsSync(assetPath)) {
httpLogger.error(`File not found: ${assetPath}`)
return res.status(404).send('Asset not found')
}
res.sendFile(assetPath, (err) => {
if (err) {
httpLogger.error('Error sending file:', err)
res.status(500).send('Error downloading the asset')
}
})
})
/** /**
* Login * Login
* @param req * @param req
@ -183,6 +58,70 @@ async function addHttpRoutes(app: Application) {
return res.status(400).json({ message: 'Failed to register user' }) return res.status(400).json({ message: 'Failed to register user' })
}) })
/**
* 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) => {
const zoneId = req.params.zoneId
// Check if zoneId is valid number
if (!zoneId || parseInt(zoneId) === 0) {
return res.status(400).json({ message: 'Invalid zone ID' })
}
// Get zone by id
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);
// Return the array
res.json(tiles)
})
/**
* Get a specific asset
* @param req
* @param res
*/
app.get('/assets/:type/:spriteId?/:file', (req: Request, res: Response) => {
const assetType = req.params.type
const spriteId = req.params.spriteId
const fileName = req.params.file
let assetPath
if (assetType === 'sprites' && spriteId) {
assetPath = getPublicPath(assetType, spriteId, fileName)
} else {
assetPath = getPublicPath(assetType, fileName)
}
if (!fs.existsSync(assetPath)) {
httpLogger.error(`File not found: ${assetPath}`)
return res.status(404).send('Asset not found')
}
res.sendFile(assetPath, (err) => {
if (err) {
httpLogger.error('Error sending file:', err)
res.status(500).send('Error downloading the asset')
}
})
})
httpLogger.info('Web routes added') httpLogger.info('Web routes added')
} }

49
src/utilities/json.ts Normal file
View File

@ -0,0 +1,49 @@
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
}
}

View File

@ -1,9 +1,9 @@
import pino from 'pino' import pino from 'pino'
import fs from 'fs' import fs from 'fs'
import path from 'path' import { getRootPath } from './storage'
// Array of log types // Array of log types
const LOG_TYPES = ['http', 'game', 'gameMaster', 'app', 'queue'] as const const LOG_TYPES = ['http', 'game', 'gameMaster', 'app', 'queue', 'command'] as const
type LogType = (typeof LOG_TYPES)[number] type LogType = (typeof LOG_TYPES)[number]
const createLogger = (name: LogType) => const createLogger = (name: LogType) =>
@ -30,7 +30,7 @@ const loggers = Object.fromEntries(LOG_TYPES.map((type) => [type, createLogger(t
const watchLogs = () => { const watchLogs = () => {
LOG_TYPES.forEach((type) => { LOG_TYPES.forEach((type) => {
const logFile = path.join(__dirname, '../../logs', `${type}.log`) const logFile = getRootPath('logs', `${type}.log`)
fs.watchFile(logFile, (curr, prev) => { fs.watchFile(logFile, (curr, prev) => {
if (curr.size > prev.size) { if (curr.size > prev.size) {
@ -43,6 +43,6 @@ const watchLogs = () => {
}) })
} }
export const { http: httpLogger, game: gameLogger, gameMaster: gameMasterLogger, app: appLogger, queue: queueLogger } = loggers export const { http: httpLogger, game: gameLogger, gameMaster: gameMasterLogger, app: appLogger, queue: queueLogger, command: commandLogger } = loggers
export { watchLogs } export { watchLogs }

33
src/utilities/storage.ts Normal file
View File

@ -0,0 +1,33 @@
import config from './config'
import path from 'path'
import fs from 'fs'
export function getRootPath(folder: string, ...additionalSegments: string[]) {
return path.join(process.cwd(), folder, ...additionalSegments)
}
export function getAppPath(folder: string, ...additionalSegments: string[]) {
const baseDir = config.ENV === 'development' ? 'src' : 'dist'
return path.join(process.cwd(), baseDir, folder, ...additionalSegments)
}
export function getPublicPath(folder: string, ...additionalSegments: string[]) {
return path.join(process.cwd(), 'public', folder, ...additionalSegments)
}
export function doesPathExist(path: string) {
try {
fs.accessSync(path, fs.constants.F_OK)
return true
} catch (e) {
return false
}
}
export function createDir(path: string) {
try {
fs.mkdirSync(path, { recursive: true })
} catch (e) {
console.error(e)
}
}

View File

@ -25,10 +25,19 @@ export type TAsset = {
key: string key: string
url: string url: string
group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other' group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
updatedAt: Date
frameCount?: number
frameWidth?: number frameWidth?: number
frameHeight?: number frameHeight?: number
} }
export type WorldSettings = {
date: Date
isRainEnabled: boolean
isFogEnabled: boolean
fogDensity: number
}
// export type TCharacter = Socket & { // export type TCharacter = Socket & {
// user?: User // user?: User
// character?: Character // character?: Character