Compare commits

...

26 Commits

Author SHA1 Message Date
bd8caaf27c POC 2 2024-12-20 23:51:39 +01:00
3cf86e322c proof of concept 2024-12-20 23:44:13 +01:00
6f32fbdc79 Improved readability 2024-12-20 23:36:56 +01:00
743d4594df 🎃 2024-12-20 23:13:55 +01:00
2be49c010f Horror code 2024-12-20 21:29:19 +01:00
2ac9416fe6 senior dev moment, finished spritesheet generator 2024-12-20 20:34:39 +01:00
43fe6ab33e Better sprite-sheet generating 2024-12-20 01:28:36 +01:00
1cbf116ad4 🤠🔫 2024-12-18 03:05:11 +01:00
3f10b03d24 Improved table names 2024-12-17 17:07:56 +01:00
a525d80530 New migration file 2024-12-17 17:01:10 +01:00
4748044ab3 Added equipment tables and columns in game & user schema 2024-12-17 16:59:52 +01:00
3b0138130b Removed extra lines that made no differenec 2024-12-15 21:16:33 +01:00
54c75896f9 Put all images in a container size as big as the biggest sprite that was found 2024-12-15 20:57:54 +01:00
9467797dc9 Aids 2024-12-15 20:25:30 +01:00
a8934f8e40 Spritesheet generator improvements 2024-12-15 17:39:47 +01:00
65cae5d824 Spritesheet gen. optimisations 2024-12-15 14:41:00 +01:00
179ccdbc55 Improved texture generation 2024-12-12 00:43:34 +01:00
ff39628f0c Solved teleporting issue 2024-12-07 23:06:59 +01:00
d4680b198e npm update 2024-12-05 09:56:28 +01:00
550b961505 Removed func. 2024-11-24 20:11:52 +01:00
1839bd9a22 Renamed hair > characterHair 2024-11-24 15:13:28 +01:00
1017013032 Moved service logic from repo to service, minor improvements, working hair customisation proof of concept 2024-11-23 16:48:07 +01:00
d5c7cd0294 Added CRUD logic for character hair, made some minor improvements, npm update 2024-11-23 15:30:11 +01:00
4a62bbb118 #245 & #254: Started working on character hair sprite management & character customisation 2024-11-21 02:58:25 +01:00
40c7f6289a npm update 2024-11-20 21:19:42 +01:00
72d731c6f2 OOP improvement 2024-11-17 21:21:34 +01:00
24 changed files with 992 additions and 389 deletions

311
package-lock.json generated
View File

@ -729,18 +729,18 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.17.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.6.tgz",
"integrity": "sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==",
"version": "20.17.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.10.tgz",
"integrity": "sha512-/jrvh5h6NXhEauFFexRin69nA0uHJ5gwk4iDivp/DeoEua3uwCUto6PC86IpRITBOs4+6i2I56K5x5b6WYGXHA==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.19.2"
}
},
"node_modules/@types/nodemailer": {
"version": "6.4.16",
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.16.tgz",
"integrity": "sha512-uz6hN6Pp0upXMcilM61CoKyjT7sskBoOWpptkjjJp8jIMlTdc3xG01U7proKkXzruMS4hS0zqtHNkNPFB20rKQ==",
"version": "6.4.17",
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz",
"integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -949,9 +949,9 @@
"license": "BSD-3-Clause"
},
"node_modules/bullmq": {
"version": "5.26.2",
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.26.2.tgz",
"integrity": "sha512-UdHBrJoRkpXoF8b/FVEkuRBnaUZoA7+qHQNyTx1n2oNVZ4iWxqGqss+M9xAwXOpBmSNvOSlaBdHpf+5QJTU8GQ==",
"version": "5.34.3",
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.34.3.tgz",
"integrity": "sha512-S8/V11w7p6jYAGvv+00skLza/4inTOupWPe0uCD8mZSUiYKzvmW4/YEB+KVjZI2CC2oD3KJ3t7/KkUd31MxMig==",
"license": "MIT",
"dependencies": {
"cron-parser": "^4.6.0",
@ -972,17 +972,27 @@
"node": ">= 0.8"
}
},
"node_modules/call-bind": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
"node_modules/call-bind-apply-helpers": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
"integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"set-function-length": "^1.2.1"
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz",
"integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"get-intrinsic": "^1.2.6"
},
"engines": {
"node": ">= 0.4"
@ -1150,23 +1160,6 @@
"ms": "2.0.0"
}
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@ -1215,9 +1208,9 @@
}
},
"node_modules/dotenv": {
"version": "16.4.5",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
"version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
@ -1226,6 +1219,20 @@
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@ -1313,13 +1320,10 @@
"license": "MIT"
},
"node_modules/es-define-property": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"engines": {
"node": ">= 0.4"
}
@ -1333,6 +1337,18 @@
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
"integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@ -1349,9 +1365,9 @@
}
},
"node_modules/express": {
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
@ -1373,7 +1389,7 @@
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.10",
"path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
@ -1388,6 +1404,10 @@
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/fast-redact": {
@ -1472,16 +1492,21 @@
}
},
"node_modules/get-intrinsic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz",
"integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"dunder-proto": "^1.0.0",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.0.0",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
@ -1504,12 +1529,12 @@
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"dependencies": {
"get-intrinsic": "^1.1.3"
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@ -1525,34 +1550,10 @@
"node": ">=4"
}
},
"node_modules/has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-proto": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -1615,9 +1616,9 @@
"license": "ISC"
},
"node_modules/ioredis": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.1.tgz",
"integrity": "sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==",
"version": "5.4.2",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.2.tgz",
"integrity": "sha512-0SZXGNGZ+WzISQ67QDyZ2x0+wVxjjUndtD8oSeik/4ajifeiRufed8fCb8QW8VMyi4MXcS+UO1k/0NGhvq1PAg==",
"license": "MIT",
"dependencies": {
"@ioredis/commands": "^1.1.1",
@ -1639,9 +1640,9 @@
}
},
"node_modules/ioredis/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@ -1841,6 +1842,15 @@
"dev": true,
"license": "ISC"
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@ -1991,9 +2001,9 @@
}
},
"node_modules/nodemon": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz",
"integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==",
"version": "3.1.9",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz",
"integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -2020,9 +2030,9 @@
}
},
"node_modules/nodemon/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -2106,9 +2116,9 @@
}
},
"node_modules/path-to-regexp": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
"node_modules/picomatch": {
@ -2162,9 +2172,9 @@
"license": "MIT"
},
"node_modules/prettier": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
"integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
"dev": true,
"license": "MIT",
"bin": {
@ -2411,23 +2421,6 @@
"node": ">= 0.8.0"
}
},
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"license": "MIT",
"dependencies": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@ -2474,15 +2467,69 @@
}
},
"node_modules/side-channel": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
"integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.7",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.4",
"object-inspect": "^1.13.1"
"object-inspect": "^1.13.3",
"side-channel-list": "^1.0.0",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
@ -2774,9 +2821,9 @@
}
},
"node_modules/typescript": {
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
"integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@ -2878,9 +2925,9 @@
}
},
"node_modules/zod": {
"version": "3.23.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"

View File

@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE `Chat` MODIFY `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3);

View File

@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE `CharacterType` ADD COLUMN `isEnabledForCharCreation` BOOLEAN NOT NULL DEFAULT false;

View File

@ -15,7 +15,7 @@ CREATE TABLE `Chat` (
`characterId` INTEGER NOT NULL,
`zoneId` INTEGER NOT NULL,
`message` VARCHAR(191) NOT NULL,
`createdAt` DATETIME(3) NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
@ -47,6 +47,20 @@ CREATE TABLE `SpriteAction` (
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Item` (
`id` VARCHAR(191) NOT NULL,
`name` VARCHAR(191) NOT NULL,
`description` VARCHAR(191) NULL,
`itemType` ENUM('WEAPON', 'HELMET', 'CHEST', 'LEGS', 'BOOTS', 'GLOVES', 'RING', 'NECKLACE') NOT NULL,
`stackable` BOOLEAN NOT NULL DEFAULT false,
`rarity` ENUM('COMMON', 'UNCOMMON', 'RARE', 'EPIC', 'LEGENDARY') NOT NULL DEFAULT 'COMMON',
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `User` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
@ -77,6 +91,7 @@ CREATE TABLE `CharacterType` (
`name` VARCHAR(191) NOT NULL,
`gender` ENUM('MALE', 'FEMALE') NOT NULL,
`race` ENUM('HUMAN', 'ELF', 'DWARF', 'ORC', 'GOBLIN') NOT NULL,
`isEnabledForCharCreation` BOOLEAN NOT NULL DEFAULT false,
`spriteId` VARCHAR(191) NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
@ -84,23 +99,39 @@ CREATE TABLE `CharacterType` (
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `CharacterHair` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`name` VARCHAR(191) NOT NULL,
`gender` ENUM('MALE', 'FEMALE') NOT NULL DEFAULT 'MALE',
`isEnabledForCharCreation` BOOLEAN NOT NULL DEFAULT false,
`spriteId` VARCHAR(191) NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Character` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`userId` INTEGER NOT NULL,
`name` VARCHAR(191) NOT NULL,
`online` BOOLEAN NOT NULL DEFAULT false,
`role` VARCHAR(191) NOT NULL DEFAULT 'player',
`zoneId` INTEGER NOT NULL DEFAULT 1,
`positionX` INTEGER NOT NULL DEFAULT 0,
`positionY` INTEGER NOT NULL DEFAULT 0,
`rotation` INTEGER NOT NULL DEFAULT 0,
`characterTypeId` INTEGER NULL,
`characterHairId` INTEGER NULL,
`alignment` INTEGER NOT NULL DEFAULT 50,
`hitpoints` INTEGER NOT NULL DEFAULT 100,
`mana` INTEGER NOT NULL DEFAULT 100,
`level` INTEGER NOT NULL DEFAULT 1,
`experience` INTEGER NOT NULL DEFAULT 0,
`alignment` INTEGER NOT NULL DEFAULT 50,
`role` VARCHAR(191) NOT NULL DEFAULT 'player',
`positionX` INTEGER NOT NULL DEFAULT 0,
`positionY` INTEGER NOT NULL DEFAULT 0,
`rotation` INTEGER NOT NULL DEFAULT 0,
`zoneId` INTEGER NOT NULL DEFAULT 1,
`characterTypeId` INTEGER NULL,
`strength` INTEGER NOT NULL DEFAULT 10,
`dexterity` INTEGER NOT NULL DEFAULT 10,
`intelligence` INTEGER NOT NULL DEFAULT 10,
`wisdom` INTEGER NOT NULL DEFAULT 10,
UNIQUE INDEX `Character_name_key`(`name`),
PRIMARY KEY (`id`)
@ -116,6 +147,17 @@ CREATE TABLE `CharacterItem` (
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `CharacterEquipment` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`characterId` INTEGER NOT NULL,
`itemId` VARCHAR(191) NOT NULL,
`quantity` INTEGER NOT NULL,
`slot` ENUM('HEAD', 'BODY', 'ARMS', 'LEGS', 'NECK', 'RING') NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Tile` (
`id` VARCHAR(191) NOT NULL,
@ -144,18 +186,6 @@ CREATE TABLE `Object` (
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Item` (
`id` VARCHAR(191) NOT NULL,
`name` VARCHAR(191) NOT NULL,
`description` VARCHAR(191) NULL,
`stackable` BOOLEAN NOT NULL DEFAULT false,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Zone` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
@ -232,6 +262,9 @@ ALTER TABLE `PasswordResetToken` ADD CONSTRAINT `PasswordResetToken_userId_fkey`
-- AddForeignKey
ALTER TABLE `CharacterType` ADD CONSTRAINT `CharacterType_spriteId_fkey` FOREIGN KEY (`spriteId`) REFERENCES `Sprite`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `CharacterHair` ADD CONSTRAINT `CharacterHair_spriteId_fkey` FOREIGN KEY (`spriteId`) REFERENCES `Sprite`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Character` ADD CONSTRAINT `Character_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
@ -241,12 +274,21 @@ ALTER TABLE `Character` ADD CONSTRAINT `Character_zoneId_fkey` FOREIGN KEY (`zon
-- AddForeignKey
ALTER TABLE `Character` ADD CONSTRAINT `Character_characterTypeId_fkey` FOREIGN KEY (`characterTypeId`) REFERENCES `CharacterType`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Character` ADD CONSTRAINT `Character_characterHairId_fkey` FOREIGN KEY (`characterHairId`) REFERENCES `CharacterHair`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `CharacterItem` ADD CONSTRAINT `CharacterItem_characterId_fkey` FOREIGN KEY (`characterId`) REFERENCES `Character`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `CharacterItem` ADD CONSTRAINT `CharacterItem_itemId_fkey` FOREIGN KEY (`itemId`) REFERENCES `Item`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `CharacterEquipment` ADD CONSTRAINT `CharacterEquipment_characterId_fkey` FOREIGN KEY (`characterId`) REFERENCES `Character`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `CharacterEquipment` ADD CONSTRAINT `CharacterEquipment_itemId_fkey` FOREIGN KEY (`itemId`) REFERENCES `Item`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `ZoneEffect` ADD CONSTRAINT `ZoneEffect_zoneId_fkey` FOREIGN KEY (`zoneId`) REFERENCES `Zone`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -23,6 +23,7 @@ model Sprite {
updatedAt DateTime @updatedAt
spriteActions SpriteAction[]
characterTypes CharacterType[]
characterHairs CharacterHair[]
}
model SpriteAction {
@ -39,3 +40,35 @@ model SpriteAction {
frameHeight Int @default(0)
frameSpeed Int @default(0)
}
model Item {
id String @id @default(uuid())
name String
description String?
itemType ItemType
stackable Boolean @default(false)
rarity ItemRarity @default(COMMON)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
characters CharacterItem[]
equipment CharacterEquipment[]
}
enum ItemType {
WEAPON
HELMET
CHEST
LEGS
BOOTS
GLOVES
RING
NECKLACE
}
enum ItemRarity {
COMMON
UNCOMMON
RARE
EPIC
LEGENDARY
}

View File

@ -1,3 +1,16 @@
enum CharacterGender {
MALE
FEMALE
}
enum CharacterRace {
HUMAN
ELF
DWARF
ORC
GOBLIN
}
model User {
id Int @id @default(autoincrement())
username String @unique
@ -16,19 +29,6 @@ model PasswordResetToken {
createdAt DateTime @default(now())
}
enum CharacterGender {
MALE
FEMALE
}
enum CharacterRace {
HUMAN
ELF
DWARF
ORC
GOBLIN
}
model CharacterType {
id Int @id @default(autoincrement())
name String
@ -42,27 +42,52 @@ model CharacterType {
updatedAt DateTime @updatedAt
}
model CharacterHair {
id Int @id @default(autoincrement())
name String
gender CharacterGender @default(MALE)
isEnabledForCharCreation Boolean @default(false)
sprite Sprite? @relation(fields: [spriteId], references: [id], onDelete: Cascade)
spriteId String?
characters Character[]
}
model Character {
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
name String @unique
online Boolean @default(false)
hitpoints Int @default(100)
mana Int @default(100)
level Int @default(1)
experience Int @default(0)
alignment Int @default(50)
role String @default("player")
positionX Int @default(0)
positionY Int @default(0)
rotation Int @default(0)
zoneId Int @default(1)
zone Zone @relation(fields: [zoneId], references: [id], onDelete: Cascade)
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
name String @unique
online Boolean @default(false)
role String @default("player")
chats Chat[]
// Position
zoneId Int @default(1)
zone Zone @relation(fields: [zoneId], references: [id], onDelete: Cascade)
positionX Int @default(0)
positionY Int @default(0)
rotation Int @default(0)
// Customization
characterTypeId Int?
characterType CharacterType? @relation(fields: [characterTypeId], references: [id], onDelete: Cascade)
chats Chat[]
items CharacterItem[]
characterType CharacterType? @relation(fields: [characterTypeId], references: [id], onDelete: Cascade)
characterHairId Int?
characterHair CharacterHair? @relation(fields: [characterHairId], references: [id], onDelete: Cascade)
// Inventory
items CharacterItem[]
equipment CharacterEquipment[]
// Stats
alignment Int @default(50)
hitpoints Int @default(100)
mana Int @default(100)
level Int @default(1)
experience Int @default(0)
strength Int @default(10)
dexterity Int @default(10)
intelligence Int @default(10)
wisdom Int @default(10)
}
model CharacterItem {
@ -73,3 +98,22 @@ model CharacterItem {
item Item @relation(fields: [itemId], references: [id], onDelete: Cascade)
quantity Int
}
model CharacterEquipment {
id Int @id @default(autoincrement())
characterId Int
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
itemId String
item Item @relation(fields: [itemId], references: [id], onDelete: Cascade)
quantity Int
slot CharacterEquipmentSlotType
}
enum CharacterEquipmentSlotType {
HEAD
BODY
ARMS
LEGS
NECK
RING
}

View File

@ -21,16 +21,6 @@ model Object {
ZoneObject ZoneObject[]
}
model Item {
id String @id @default(uuid())
name String
description String?
stackable Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
characters CharacterItem[]
}
model Zone {
id Int @id @default(autoincrement())
name String

View File

@ -1,5 +1,5 @@
import { Character } from '@prisma/client'
import { CharacterService } from '../services/character/characterService'
import { CharacterService } from '../services/characterService'
class ZoneCharacter {
public readonly character: Character

View File

@ -0,0 +1,17 @@
import prisma from '../utilities/prisma' // Import the global Prisma instance
import { CharacterHair } from '@prisma/client'
class CharacterHairRepository {
async getAll(): Promise<CharacterHair[]> {
return prisma.characterHair.findMany()
}
async getIsEnabledForCharCreationHair(): Promise<CharacterHair[]> {
return prisma.characterHair.findMany({
where: {
isEnabledForCharCreation: true
}
})
}
}
export default new CharacterHairRepository()

View File

@ -1,9 +1,8 @@
import prisma from '../utilities/prisma' // Import the global Prisma instance
import { Character } from '@prisma/client'
import { appLogger } from '../utilities/logger'
class CharacterRepository {
async getByUserId(userId: number): Promise<Character[] | null> {
async getByUserId(userId: number) {
try {
return await prisma.character.findMany({
where: {
@ -15,6 +14,11 @@ class CharacterRepository {
include: {
sprite: true
}
},
characterHair: {
include: {
sprite: true
}
}
}
})
@ -25,7 +29,7 @@ class CharacterRepository {
}
}
async getByUserAndId(userId: number, characterId: number): Promise<Character | null> {
async getByUserAndId(userId: number, characterId: number) {
try {
return await prisma.character.findFirst({
where: {
@ -38,6 +42,11 @@ class CharacterRepository {
include: {
sprite: true
}
},
characterHair: {
include: {
sprite: true
}
}
}
})
@ -48,7 +57,7 @@ class CharacterRepository {
}
}
async getById(id: number): Promise<Character | null> {
async getById(id: number) {
try {
return await prisma.character.findUnique({
where: {
@ -60,6 +69,11 @@ class CharacterRepository {
include: {
sprite: true
}
},
characterHair: {
include: {
sprite: true
}
}
}
})
@ -70,40 +84,7 @@ class CharacterRepository {
}
}
async updatePosition(id: number, positionX: number, positionY: number): Promise<Character | null> {
try {
return await prisma.character.update({
where: {
id: id
},
data: {
positionX,
positionY
}
})
} catch (error: any) {
// Handle error
appLogger.error(`Failed to update character: ${error instanceof Error ? error.message : String(error)}`)
return null
}
}
async deleteByUserIdAndId(userId: number, characterId: number): Promise<Character | null> {
try {
return await prisma.character.delete({
where: {
userId,
id: characterId
}
})
} catch (error: any) {
// Handle error
appLogger.error(`Failed to delete character by user ID and character ID: ${error instanceof Error ? error.message : String(error)}`)
return null
}
}
async getByName(name: string): Promise<Character | null> {
async getByName(name: string) {
try {
return await prisma.character.findFirst({
where: {
@ -115,6 +96,11 @@ class CharacterRepository {
include: {
sprite: true
}
},
characterHair: {
include: {
sprite: true
}
}
}
})

View File

@ -3,7 +3,6 @@ import prisma from '../utilities/prisma'
import { appLogger } from '../utilities/logger'
class ZoneEventTileRepository {
async getAll(id: number): Promise<ZoneEventTile[]> {
try {
return await prisma.zoneEventTile.findMany({

View File

@ -1,9 +1,9 @@
import { AStar } from '../../utilities/character/aStar'
import ZoneManager from '../../managers/zoneManager'
import prisma from '../../utilities/prisma'
import Rotation from '../../utilities/character/rotation'
import { gameLogger } from '../../utilities/logger'
import { Character, CharacterGender, CharacterRace } from '@prisma/client'
import { AStar } from '../utilities/character/aStar'
import ZoneManager from '../managers/zoneManager'
import prisma from '../utilities/prisma'
import Rotation from '../utilities/character/rotation'
import { appLogger, gameLogger } from '../utilities/logger'
import { Character } from '@prisma/client'
interface Position {
x: number
@ -11,7 +11,53 @@ interface Position {
}
export class CharacterService {
private static readonly MOVEMENT_DELAY_MS = 250
private readonly MOVEMENT_DELAY_MS = 250
async create(name: string, userId: number) {
return prisma.character.create({
data: {
name,
userId
// characterTypeId: 1 // @TODO set to chosen character type
}
})
}
async updateHair(characterId: number, characterHairId: number | null) {
await prisma.character.update({
where: { id: characterId },
data: {
characterHairId
}
})
}
async deleteByUserIdAndId(userId: number, characterId: number): Promise<Character | null> {
try {
return await prisma.character.delete({
where: {
userId,
id: characterId
}
})
} catch (error: any) {
// Handle error
appLogger.error(`Failed to delete character by user ID and character ID: ${error instanceof Error ? error.message : String(error)}`)
return null
}
}
async updateCharacterPosition(id: number, positionX: number, positionY: number, rotation: number, zoneId: number) {
await prisma.character.update({
where: { id },
data: {
positionX,
positionY,
rotation,
zoneId
}
})
}
public updatePosition(character: Character, position: Position, newZoneId?: number): void {
if (!this.isValidPosition(position)) {
@ -49,32 +95,10 @@ export class CharacterService {
}
public async applyMovementDelay(): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, CharacterService.MOVEMENT_DELAY_MS))
await new Promise((resolve) => setTimeout(resolve, this.MOVEMENT_DELAY_MS))
}
private isValidPosition(position: Position): boolean {
return Number.isFinite(position.x) && Number.isFinite(position.y) && position.x >= 0 && position.y >= 0
}
async create(name: string, userId: number) {
return prisma.character.create({
data: {
name,
userId
// characterTypeId: 1 // @TODO set to chosen character type
}
})
}
async updateCharacterPosition(id: number, positionX: number, positionY: number, rotation: number, zoneId: number) {
await prisma.character.update({
where: { id },
data: {
positionX,
positionY,
rotation,
zoneId
}
})
}
}

View File

@ -0,0 +1,22 @@
import { Server } from 'socket.io'
import { CharacterHair } from '@prisma/client'
import { TSocket } from '../../../utilities/types'
import characterHairRepository from '../../../repositories/characterHairRepository'
interface IPayload {}
export default class characterHairListEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void {
this.socket.on('character:hair:list', this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: CharacterHair[]) => void): Promise<void> {
const items = await characterHairRepository.getIsEnabledForCharCreationHair()
callback(items)
}
}

View File

@ -3,9 +3,11 @@ import { TSocket } from '../../utilities/types'
import CharacterRepository from '../../repositories/characterRepository'
import { gameLogger } from '../../utilities/logger'
import ZoneManager from '../../managers/zoneManager'
import { CharacterService } from '../../services/characterService'
interface CharacterConnectPayload {
characterId: number
characterHairId?: number
}
export default class CharacterConnectEvent {
@ -18,7 +20,7 @@ export default class CharacterConnectEvent {
this.socket.on('character:connect', this.handleCharacterConnect.bind(this))
}
private async handleCharacterConnect({ characterId }: CharacterConnectPayload): Promise<void> {
private async handleCharacterConnect({ characterId, characterHairId }: CharacterConnectPayload): Promise<void> {
if (!this.socket.userId) {
this.emitError('User not authenticated')
return
@ -30,7 +32,11 @@ export default class CharacterConnectEvent {
return
}
const character = await this.connectCharacter(characterId)
// Update hair
const characterService = new CharacterService()
await characterService.updateHair(characterId, characterHairId ?? null)
const character = await CharacterRepository.getByUserAndId(this.socket.userId!, characterId)
if (!character) {
this.emitError('Character not found or does not belong to this user')
return
@ -48,10 +54,6 @@ export default class CharacterConnectEvent {
return characters?.some((char) => ZoneManager.getCharacter(char.id)) ?? false
}
private async connectCharacter(characterId: number) {
return CharacterRepository.getByUserAndId(this.socket.userId!, characterId)
}
private emitError(message: string): void {
this.socket.emit('notification', { title: 'Server message', message })
gameLogger.error('character:connect error', `Player ${this.socket.userId}: ${message}`)

View File

@ -2,7 +2,7 @@ import { Server } from 'socket.io'
import { TSocket } from '../../utilities/types'
import { Character } from '@prisma/client'
import CharacterRepository from '../../repositories/characterRepository'
import { CharacterService } from '../../services/character/characterService'
import { CharacterService } from '../../services/characterService'
import { ZCharacterCreate } from '../../utilities/zodTypes'
import { gameLogger } from '../../utilities/logger'
import { ZodError } from 'zod'

View File

@ -2,6 +2,7 @@ import { Server } from 'socket.io'
import { TSocket } from '../../utilities/types'
import { Character, Zone } from '@prisma/client'
import CharacterRepository from '../../repositories/characterRepository'
import { CharacterService } from '../../services/characterService'
type TypePayload = {
characterId: number
@ -24,7 +25,8 @@ export default class CharacterDeleteEvent {
private async handleCharacterDelete(data: TypePayload, callback: (response: TypeResponse) => void): Promise<any> {
try {
await CharacterRepository.deleteByUserIdAndId(this.socket.userId!, data.characterId!)
const characterService = new CharacterService()
await characterService.deleteByUserIdAndId(this.socket.userId!, data.characterId!)
const characters: Character[] = (await CharacterRepository.getByUserId(this.socket.userId!)) as Character[]

View File

@ -5,6 +5,7 @@ import ZoneRepository from '../../../repositories/zoneRepository'
import { gameLogger, gameMasterLogger } from '../../../utilities/logger'
import ZoneManager from '../../../managers/zoneManager'
import ZoneCharacter from '../../../models/zoneCharacter'
import zoneManager from '../../../managers/zoneManager'
type TypePayload = {
message: string
@ -64,10 +65,12 @@ export default class TeleportCommandEvent {
}
// Remove character from current zone
zoneManager.removeCharacter(character.id)
this.io.to(character.zoneId.toString()).emit('zone:character:leave', character.id)
this.socket.leave(character.zoneId.toString())
// Add character to new zone
zoneManager.getZoneById(zone.id)?.addCharacter(character)
this.io.to(zone.id.toString()).emit('zone:character:join', character)
this.socket.join(zone.id.toString())

View File

@ -0,0 +1,37 @@
import { Server } from 'socket.io'
import { TSocket } from '../../../../utilities/types'
import prisma from '../../../../utilities/prisma'
import characterRepository from '../../../../repositories/characterRepository'
export default class CharacterHairCreateEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void {
this.socket.on('gm:characterHair:create', this.handleEvent.bind(this))
}
private async handleEvent(data: undefined, callback: (response: boolean, characterType?: any) => void): Promise<void> {
try {
const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) return callback(false)
if (character.role !== 'gm') {
return callback(false)
}
const newCharacterHair = await prisma.characterHair.create({
data: {
name: 'New hair'
}
})
callback(true, newCharacterHair)
} catch (error) {
console.error('Error creating character hair:', error)
callback(false)
}
}
}

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 { gameMasterLogger } from '../../../../utilities/logger'
interface IPayload {
id: number
}
export default class characterHairDeleteEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void {
this.socket.on('gm:characterHair:remove', this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: boolean) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) return callback(false)
if (character.role !== 'gm') {
return callback(false)
}
try {
await prisma.characterHair.delete({
where: { id: data.id }
})
callback(true)
} catch (error) {
gameMasterLogger.error(`Error deleting character type ${data.id}: ${error instanceof Error ? error.message : String(error)}`)
callback(false)
}
}
}

View File

@ -0,0 +1,36 @@
import { Server } from 'socket.io'
import { TSocket } from '../../../../utilities/types'
import { CharacterHair } from '@prisma/client'
import characterRepository from '../../../../repositories/characterRepository'
import { gameMasterLogger } from '../../../../utilities/logger'
import characterHairRepository from '../../../../repositories/characterHairRepository'
interface IPayload {}
export default class characterHairListEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void {
this.socket.on('gm:characterHair:list', this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: CharacterHair[]) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) {
gameMasterLogger.error('gm:characterHair:list error', 'Character not found')
return callback([])
}
if (character.role !== 'gm') {
gameMasterLogger.info(`User ${character.id} tried to list character hair but is not a game master.`)
return callback([])
}
// get all objects
const items = await characterHairRepository.getAll()
callback(items)
}
}

View File

@ -0,0 +1,51 @@
import { Server } from 'socket.io'
import { TSocket } from '../../../../utilities/types'
import prisma from '../../../../utilities/prisma'
import characterRepository from '../../../../repositories/characterRepository'
import { CharacterGender, CharacterRace } from '@prisma/client'
import { gameMasterLogger } from '../../../../utilities/logger'
type Payload = {
id: number
name: string
gender: CharacterGender
isEnabledForCharCreation: boolean
spriteId: string
}
export default class CharacterHairUpdateEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void {
this.socket.on('gm:characterHair:update', this.handleObjectUpdate.bind(this))
}
private async handleObjectUpdate(data: Payload, callback: (success: boolean) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) return callback(false)
if (character.role !== 'gm') {
return callback(false)
}
try {
await prisma.characterHair.update({
where: { id: data.id },
data: {
name: data.name,
gender: data.gender,
isEnabledForCharCreation: data.isEnabledForCharCreation,
spriteId: data.spriteId
}
})
return callback(true)
} catch (error) {
gameMasterLogger.error(`Error updating character hair: ${error instanceof Error ? error.message : String(error)}`)
return callback(false)
}
}
}

View File

@ -1,9 +1,7 @@
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'
import { gameMasterLogger } from '../../../../utilities/logger'
interface IPayload {

View File

@ -8,11 +8,34 @@ import { getPublicPath } from '../../../../utilities/storage'
import CharacterRepository from '../../../../repositories/characterRepository'
import { gameMasterLogger } from '../../../../utilities/logger'
type SpriteActionInput = Omit<SpriteAction, 'id' | 'spriteId' | 'frameWidth' | 'frameHeight'> & {
// Constants
const ISOMETRIC_CONFIG = {
tileWidth: 64,
tileHeight: 32,
centerOffset: 32,
bodyRatios: {
topStart: 0.15,
topEnd: 0.45,
weightUpper: 0.7,
weightLower: 0.3
}
} as const
// Types
interface ContentBounds {
left: number
right: number
top: number
bottom: number
width: number
height: number
}
interface SpriteActionInput extends Omit<SpriteAction, 'id' | 'spriteId' | 'frameWidth' | 'frameHeight'> {
sprites: string[]
}
type Payload = {
interface UpdatePayload {
id: string
name: string
spriteActions: Prisma.JsonValue
@ -21,11 +44,19 @@ type Payload = {
interface ProcessedSpriteAction extends SpriteActionInput {
frameWidth: number
frameHeight: number
buffersWithDimensions: Array<{
buffer: Buffer
width: number | undefined
height: number | undefined
}>
buffersWithDimensions: ProcessedFrame[]
}
interface ProcessedFrame {
buffer: Buffer
width: number
height: number
}
interface SpriteAnalysis {
massCenter: number
spinePosition: number
contentBounds: ContentBounds
}
export default class SpriteUpdateEvent {
@ -38,119 +69,322 @@ export default class SpriteUpdateEvent {
this.socket.on('gm:sprite:update', this.handleSpriteUpdate.bind(this))
}
private async handleSpriteUpdate(data: Payload, callback: (success: boolean) => void): Promise<void> {
const character = await CharacterRepository.getById(this.socket.characterId!)
if (character?.role !== 'gm') {
return callback(false)
}
private async handleSpriteUpdate(payload: UpdatePayload, callback: (success: boolean) => void): Promise<void> {
try {
const parsedSpriteActions = validateSpriteActions(data.spriteActions)
const processedActions = await processSprites(parsedSpriteActions)
if (!await this.validateGameMasterAccess()) {
return callback(false)
}
await updateDatabase(data.id, data.name, processedActions)
await saveSpritesToDisk(data.id, processedActions)
const parsedActions = this.validateSpriteActions(payload.spriteActions)
const processedActions = await this.processSprites(parsedActions)
await Promise.all([
this.updateDatabase(payload.id, payload.name, processedActions),
this.saveSpritesToDisk(payload.id, processedActions)
])
callback(true)
} catch (error) {
gameMasterLogger.error(`Error updating sprite ${data.id}: ${error instanceof Error ? error.message : String(error)}`)
callback(false)
this.handleError(error, payload.id, callback)
}
}
function validateSpriteActions(spriteActions: Prisma.JsonValue): SpriteActionInput[] {
try {
const parsed = JSON.parse(JSON.stringify(spriteActions)) as SpriteActionInput[]
if (!Array.isArray(parsed)) {
gameMasterLogger.error('Error parsing spriteActions: spriteActions is not an array')
private async validateGameMasterAccess(): Promise<boolean> {
const character = await CharacterRepository.getById(this.socket.characterId!)
return character?.role === 'gm'
}
private validateSpriteActions(actions: Prisma.JsonValue): SpriteActionInput[] {
try {
const parsed = JSON.parse(JSON.stringify(actions)) as SpriteActionInput[]
if (!Array.isArray(parsed)) {
throw new Error('Sprite actions must be an array')
}
return parsed
} catch (error) {
throw new Error(`Invalid sprite actions format: ${this.getErrorMessage(error)}`)
}
}
private async processSprites(actions: SpriteActionInput[]): Promise<ProcessedSpriteAction[]> {
return Promise.all(actions.map(async (action) => {
const spriteBuffers = await this.convertBase64ToBuffers(action.sprites)
const frameWidth = ISOMETRIC_CONFIG.tileWidth
const frameHeight = await this.calculateOptimalHeight(spriteBuffers)
const processedFrames = await this.normalizeFrames(spriteBuffers, frameWidth, frameHeight)
return {
...action,
frameWidth,
frameHeight,
buffersWithDimensions: processedFrames
}
}))
}
private async convertBase64ToBuffers(sprites: string[]): Promise<Buffer[]> {
return sprites.map(sprite => Buffer.from(sprite.split(',')[1], 'base64'))
}
private async normalizeFrames(buffers: Buffer[], frameWidth: number, frameHeight: number): Promise<ProcessedFrame[]> {
return Promise.all(
buffers.map(async (buffer) => {
const normalizedBuffer = await this.normalizeIsometricSprite(buffer, frameWidth, frameHeight)
return {
buffer: normalizedBuffer,
width: frameWidth,
height: frameHeight
}
})
)
}
private async calculateOptimalHeight(buffers: Buffer[]): Promise<number> {
const heights = await Promise.all(
buffers.map(async buffer => {
const bounds = await this.findContentBounds(buffer)
return bounds.height
})
)
return Math.ceil(Math.max(...heights) / 2) * 2
}
private async normalizeIsometricSprite(buffer: Buffer, frameWidth: number, frameHeight: number): Promise<Buffer> {
const analysis = await this.analyzeIsometricSprite(buffer)
const idealCenter = Math.floor(frameWidth / 2)
const offset = Math.round(idealCenter - analysis.massCenter)
// Process the input sprite
const processedInput = await sharp(buffer)
.ensureAlpha()
.resize({
kernel: sharp.kernel.nearest,
fit: 'contain',
position: 'center'
})
.png({
compressionLevel: 9,
adaptiveFiltering: false,
palette: true,
quality: 100,
colors: 256
})
.toBuffer()
// Create the final composition
return sharp({
create: {
width: frameWidth,
height: frameHeight,
channels: 4,
background: { r: 0, g: 0, b: 0, alpha: 0 }
}
})
.composite([{
input: processedInput,
left: offset,
top: 0,
blend: 'over'
}])
.png({
compressionLevel: 9,
adaptiveFiltering: false,
palette: true,
quality: 100,
colors: 256
})
.toBuffer()
}
private async analyzeIsometricSprite(buffer: Buffer): Promise<SpriteAnalysis> {
const { data, info } = await sharp(buffer).raw().ensureAlpha().toBuffer({ resolveWithObject: true })
const { width, height } = info
const upperStart = Math.floor(height * ISOMETRIC_CONFIG.bodyRatios.topStart)
const upperEnd = Math.floor(height * ISOMETRIC_CONFIG.bodyRatios.topEnd)
const { columnDensity, upperBodyDensity, bounds } = this.calculatePixelDistribution(data, width, height, upperStart, upperEnd)
const spinePosition = this.findSpinePosition(upperBodyDensity)
const massCenter = this.calculateWeightedMassCenter(columnDensity, upperBodyDensity)
return {
massCenter,
spinePosition,
contentBounds: bounds
}
}
private calculatePixelDistribution(data: Buffer, width: number, height: number, upperStart: number, upperEnd: number) {
const columnDensity = new Array(width).fill(0)
const upperBodyDensity = new Array(width).fill(0)
const bounds = { left: width, right: 0, top: height, bottom: 0 }
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (data[(y * width + x) * 4 + 3] > 0) {
columnDensity[x]++
if (y >= upperStart && y <= upperEnd) {
upperBodyDensity[x]++
}
this.updateBounds(bounds, x, y)
}
return parsed
} catch (error) {
gameMasterLogger.error(`Error parsing spriteActions: ${error instanceof Error ? error.message : String(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) {
gameMasterLogger.error(`Invalid sprites array for action: ${action}`)
}
const buffersWithDimensions = await Promise.all(
sprites.map(async (sprite: string) => {
const buffer = Buffer.from(sprite.split(',')[1], 'base64')
const { width, height } = await sharp(buffer).metadata()
return { buffer, width, height }
})
)
const frameWidth = Math.max(...buffersWithDimensions.map((b) => b.width || 0))
const frameHeight = Math.max(...buffersWithDimensions.map((b) => b.height || 0))
return {
...spriteAction,
frameWidth,
frameHeight,
buffersWithDimensions
}
})
)
}
async function updateDatabase(id: string, name: string, processedActions: ProcessedSpriteAction[]) {
await prisma.sprite.update({
where: { id },
data: {
name,
spriteActions: {
deleteMany: { spriteId: id },
create: processedActions.map(({ action, sprites, originX, originY, isAnimated, isLooping, frameWidth, frameHeight, frameSpeed }) => ({
action,
sprites,
originX,
originY,
isAnimated,
isLooping,
frameWidth,
frameHeight,
frameSpeed
}))
}
}
})
}
async function saveSpritesToDisk(id: string, processedActions: ProcessedSpriteAction[]) {
const publicFolder = getPublicPath('sprites', id)
await mkdir(publicFolder, { recursive: true })
await Promise.all(
processedActions.map(async ({ action, buffersWithDimensions, frameWidth, frameHeight }) => {
const combinedImage = await sharp({
create: {
width: frameWidth * buffersWithDimensions.length,
height: frameHeight,
channels: 4,
background: { r: 0, g: 0, b: 0, alpha: 0 }
}
})
.composite(
buffersWithDimensions.map(({ buffer }, index) => ({
input: buffer,
left: index * frameWidth,
top: 0
}))
)
.png()
.toBuffer()
const filename = getPublicPath('sprites', id, `${action}.png`)
await writeFile(filename, combinedImage)
})
)
return {
columnDensity,
upperBodyDensity,
bounds: {
...bounds,
width: bounds.right - bounds.left + 1,
height: bounds.bottom - bounds.top + 1
}
}
}
}
private updateBounds(bounds: { left: number; right: number; top: number; bottom: number }, x: number, y: number): void {
bounds.left = Math.min(bounds.left, x)
bounds.right = Math.max(bounds.right, x)
bounds.top = Math.min(bounds.top, y)
bounds.bottom = Math.max(bounds.bottom, y)
}
private findSpinePosition(density: number[]): number {
return density.reduce((maxIdx, curr, idx, arr) => curr > arr[maxIdx] ? idx : maxIdx, 0)
}
private calculateWeightedMassCenter(columnDensity: number[], upperBodyDensity: number[]): number {
const upperMassCenter = this.calculateMassCenter(upperBodyDensity)
const lowerMassCenter = this.calculateMassCenter(columnDensity)
return Math.round(
upperMassCenter * ISOMETRIC_CONFIG.bodyRatios.weightUpper +
lowerMassCenter * ISOMETRIC_CONFIG.bodyRatios.weightLower
)
}
private calculateMassCenter(density: number[]): number {
const totalMass = density.reduce((sum, mass) => sum + mass, 0)
if (!totalMass) return 0
const weightedSum = density.reduce((sum, mass, position) => sum + position * mass, 0)
return Math.round(weightedSum / totalMass)
}
private async findContentBounds(buffer: Buffer) {
const { data, info } = await sharp(buffer)
.raw()
.ensureAlpha()
.toBuffer({ resolveWithObject: true })
const width = info.width
const height = info.height
let left = width
let right = 0
let top = height
let bottom = 0
// Find actual content boundaries by checking alpha channel
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const idx = (y * width + x) * 4
if (data[idx + 3] > 0) { // If pixel is not transparent
left = Math.min(left, x)
right = Math.max(right, x)
top = Math.min(top, y)
bottom = Math.max(bottom, y)
}
}
}
return {
width: right - left + 1,
height: bottom - top + 1,
leftOffset: left,
topOffset: top
}
}
private async saveSpritesToDisk(id: string, actions: ProcessedSpriteAction[]): Promise<void> {
const publicFolder = getPublicPath('sprites', id)
await mkdir(publicFolder, { recursive: true })
await Promise.all(actions.map(async (action) => {
const spritesheet = await this.createSpritesheet(action.buffersWithDimensions)
await writeFile(getPublicPath('sprites', id, `${action.action}.png`), spritesheet)
}))
}
private async createSpritesheet(frames: ProcessedFrame[]): Promise<Buffer> {
const background = await sharp({
create: {
width: ISOMETRIC_CONFIG.tileWidth * frames.length,
height: frames[0].height,
channels: 4,
background: { r: 0, g: 0, b: 0, alpha: 0 }
}
})
.png({
compressionLevel: 9,
adaptiveFiltering: false,
palette: true,
quality: 100,
colors: 256,
dither: 0
})
.toBuffer()
return sharp(background)
.composite(frames.map((frame, index) => ({
input: frame.buffer,
left: index * ISOMETRIC_CONFIG.tileWidth,
top: 0,
blend: 'over'
})))
.png({
compressionLevel: 9,
adaptiveFiltering: false,
palette: true,
quality: 100,
colors: 256,
dither: 0
})
.toBuffer()
}
private async updateDatabase(id: string, name: string, actions: ProcessedSpriteAction[]): Promise<void> {
await prisma.sprite.update({
where: { id },
data: {
name,
spriteActions: {
deleteMany: { spriteId: id },
create: actions.map(this.mapActionToDatabase)
}
}
})
}
private mapActionToDatabase(action: ProcessedSpriteAction) {
return {
action: action.action,
sprites: action.sprites,
originX: action.originX,
originY: action.originY,
isAnimated: action.isAnimated,
isLooping: action.isLooping,
frameWidth: action.frameWidth,
frameHeight: action.frameHeight,
frameSpeed: action.frameSpeed
}
}
private handleError(error: unknown, spriteId: string, callback: (success: boolean) => void): void {
gameMasterLogger.error(`Error updating sprite ${spriteId}: ${this.getErrorMessage(error)}`)
callback(false)
}
private getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error)
}
}

View File

@ -1,6 +1,6 @@
import { Server } from 'socket.io'
import { TSocket, ZoneEventTileWithTeleport } from '../../utilities/types'
import { CharacterService } from '../../services/character/characterService'
import { CharacterService } from '../../services/characterService'
import { ZoneEventTileService } from '../../services/zoneEventTileService'
import Rotation from '../../utilities/character/rotation'
import { gameLogger } from '../../utilities/logger'