forked from noxious/server
Compare commits
125 Commits
how-i-see-
...
feature/#0
Author | SHA1 | Date | |
---|---|---|---|
2ac9416fe6 | |||
43fe6ab33e | |||
1cbf116ad4 | |||
3f10b03d24 | |||
a525d80530 | |||
4748044ab3 | |||
3b0138130b | |||
54c75896f9 | |||
9467797dc9 | |||
a8934f8e40 | |||
65cae5d824 | |||
179ccdbc55 | |||
ff39628f0c | |||
d4680b198e | |||
550b961505 | |||
1839bd9a22 | |||
1017013032 | |||
d5c7cd0294 | |||
4a62bbb118 | |||
40c7f6289a | |||
72d731c6f2 | |||
fc92d9ea79 | |||
2e267a36aa | |||
6ee8bb8334 | |||
ec3bf0f51e | |||
27f8bc8784 | |||
3185c478a6 | |||
72ef04d683 | |||
446e8fa617 | |||
f7072acdd2 | |||
fda8cc532e | |||
821e742527 | |||
460308d555 | |||
3f8f8745eb | |||
bf7f585270 | |||
fee4277b4f | |||
ffc07b7403 | |||
344ddbaf39 | |||
86ed3ae4b0 | |||
719c75616e | |||
cf954979c5 | |||
01ed1bce29 | |||
d4e0cbe398 | |||
628b3bf1fa | |||
709d34d59b | |||
c4a42066ab | |||
26dbaa45a7 | |||
ae0241fecb | |||
3a566dae5a | |||
ad4f33676f | |||
44cfbd6ee8 | |||
881e3375ab | |||
6a76c4797a | |||
bf75ad001b | |||
3b473e5826 | |||
929a36554a | |||
3fbc5f4e87 | |||
1526e0947a | |||
7ec4303b40 | |||
f475b69022 | |||
27d8c7cff6 | |||
b9a7f9aa8e | |||
5b6b968541 | |||
93abf4b631 | |||
0de574b9e1 | |||
c04c52aed0 | |||
3f19730bd8 | |||
d0e3c95bb0 | |||
82f51b2b7e | |||
1b9db64854 | |||
41c71d5964 | |||
bd04dc2ab8 | |||
a4e96f9ede | |||
f6bac403a2 | |||
8460d0b535 | |||
5a36d10f0e | |||
8f8f019ab7 | |||
6a1823586a | |||
9d08073fa8 | |||
5631930bf5 | |||
b6e7a5d7fe | |||
63804336be | |||
0b62b4231b | |||
4e1e7d95ac | |||
d29420cbf3 | |||
acc04daa27 | |||
8abf5acef3 | |||
780cac9644 | |||
44481e19a8 | |||
9075bfaad5 | |||
bfd941c091 | |||
bb9f62a9c8 | |||
049b9de2b3 | |||
2008646a3f | |||
075592702c | |||
d271efc1ec | |||
297d4742a4 | |||
ab649b9fa1 | |||
4f643269eb | |||
ce1708a55e | |||
4cbd62cbb0 | |||
7b3c4b92a5 | |||
da8ef9fa65 | |||
4f9a1bc879 | |||
3638e2a793 | |||
6ac827630a | |||
3ec4bc2557 | |||
6a286590b4 | |||
34ed2ba7cb | |||
72159cdc17 | |||
70d8c43350 | |||
3a83f2c1ff | |||
ddeee356b4 | |||
e9fb277d63 | |||
e8aee51248 | |||
46fdb3edb6 | |||
dec6b36699 | |||
21a75f6cbe | |||
10a231b54c | |||
cc9eada654 | |||
6f057639c0 | |||
251a72aa97 | |||
0d7ed18b03 | |||
4a9b7987dc | |||
a729371741 |
@ -4,6 +4,7 @@ PORT=4000
|
|||||||
DATABASE_URL="mysql://root@localhost:3306/nq"
|
DATABASE_URL="mysql://root@localhost:3306/nq"
|
||||||
REDIS_URL="redis://@127.0.0.1:6379/4"
|
REDIS_URL="redis://@127.0.0.1:6379/4"
|
||||||
JWT_SECRET="secret"
|
JWT_SECRET="secret"
|
||||||
|
CLIENT_URL="http://localhost:5173"
|
||||||
|
|
||||||
# Game configuration
|
# Game configuration
|
||||||
ALLOW_DIAGONAL_MOVEMENT=false
|
ALLOW_DIAGONAL_MOVEMENT=false
|
||||||
@ -11,4 +12,10 @@ ALLOW_DIAGONAL_MOVEMENT=false
|
|||||||
# Default character create values
|
# Default character create values
|
||||||
DEFAULT_CHARACTER_ZONE="0"
|
DEFAULT_CHARACTER_ZONE="0"
|
||||||
DEFAULT_CHARACTER_POS_X="0"
|
DEFAULT_CHARACTER_POS_X="0"
|
||||||
DEFAULT_CHARACTER_POS_Y="0"
|
DEFAULT_CHARACTER_POS_Y="0"
|
||||||
|
|
||||||
|
# SMTP configuration
|
||||||
|
SMTP_HOST="my.directonline.io"
|
||||||
|
SMTP_PORT="587"
|
||||||
|
SMTP_USER="no-reply@sylvan.quest"
|
||||||
|
SMTP_PASSWORD=""
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -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
|
|
10
Dockerfile
10
Dockerfile
@ -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
|
||||||
|
628
package-lock.json
generated
628
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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/"
|
||||||
},
|
},
|
||||||
@ -15,11 +15,11 @@
|
|||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"ioredis": "^5.4.1",
|
"ioredis": "^5.4.1",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"nodemailer": "^6.9.15",
|
||||||
"pino": "^9.3.2",
|
"pino": "^9.3.2",
|
||||||
"prisma": "^5.17.0",
|
"prisma": "^5.17.0",
|
||||||
"sharp": "^0.33.4",
|
"sharp": "^0.33.4",
|
||||||
"socket.io": "^4.7.5",
|
"socket.io": "^4.7.5",
|
||||||
"ts-node": "^10.9.2",
|
|
||||||
"typescript": "^5.5.3",
|
"typescript": "^5.5.3",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
@ -28,6 +28,8 @@
|
|||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/jsonwebtoken": "^9.0.6",
|
"@types/jsonwebtoken": "^9.0.6",
|
||||||
"@types/node": "^20.14.11",
|
"@types/node": "^20.14.11",
|
||||||
|
"@types/nodemailer": "^6.4.16",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
"nodemon": "^3.1.4",
|
"nodemon": "^3.1.4",
|
||||||
"prettier": "^3.3.3"
|
"prettier": "^3.3.3"
|
||||||
}
|
}
|
||||||
|
0
prisma/.gitignore
vendored
Normal file
0
prisma/.gitignore
vendored
Normal file
@ -1,10 +1,21 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `World` (
|
||||||
|
`date` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`isRainEnabled` BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
`rainPercentage` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`isFogEnabled` BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
`fogDensity` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
UNIQUE INDEX `World_date_key`(`date`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE `Chat` (
|
CREATE TABLE `Chat` (
|
||||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
`characterId` INTEGER NOT NULL,
|
`characterId` INTEGER NOT NULL,
|
||||||
`zoneId` INTEGER NOT NULL,
|
`zoneId` INTEGER NOT NULL,
|
||||||
`message` VARCHAR(191) NOT NULL,
|
`message` VARCHAR(191) NOT NULL,
|
||||||
`createdAt` DATETIME(3) NOT NULL,
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
|
||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
@ -36,14 +47,41 @@ CREATE TABLE `SpriteAction` (
|
|||||||
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 `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
|
-- CreateTable
|
||||||
CREATE TABLE `User` (
|
CREATE TABLE `User` (
|
||||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
`username` VARCHAR(191) NOT NULL,
|
`username` VARCHAR(191) NOT NULL,
|
||||||
|
`email` VARCHAR(191) NOT NULL,
|
||||||
`password` VARCHAR(191) NOT NULL,
|
`password` VARCHAR(191) NOT NULL,
|
||||||
`online` BOOLEAN NOT NULL DEFAULT false,
|
`online` BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
|
||||||
UNIQUE INDEX `User_username_key`(`username`),
|
UNIQUE INDEX `User_username_key`(`username`),
|
||||||
|
UNIQUE INDEX `User_email_key`(`email`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `PasswordResetToken` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`userId` INTEGER NOT NULL,
|
||||||
|
`token` VARCHAR(191) NOT NULL,
|
||||||
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
|
||||||
|
UNIQUE INDEX `PasswordResetToken_token_key`(`token`),
|
||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
@ -53,30 +91,47 @@ 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,
|
`isEnabledForCharCreation` BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
`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,
|
||||||
|
|
||||||
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 `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
|
-- CreateTable
|
||||||
CREATE TABLE `Character` (
|
CREATE TABLE `Character` (
|
||||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
`userId` INTEGER NOT NULL,
|
`userId` INTEGER NOT NULL,
|
||||||
`name` VARCHAR(191) NOT NULL,
|
`name` VARCHAR(191) NOT NULL,
|
||||||
`online` BOOLEAN NOT NULL DEFAULT false,
|
`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,
|
`hitpoints` INTEGER NOT NULL DEFAULT 100,
|
||||||
`mana` INTEGER NOT NULL DEFAULT 100,
|
`mana` INTEGER NOT NULL DEFAULT 100,
|
||||||
`level` INTEGER NOT NULL DEFAULT 1,
|
`level` INTEGER NOT NULL DEFAULT 1,
|
||||||
`experience` INTEGER NOT NULL DEFAULT 0,
|
`experience` INTEGER NOT NULL DEFAULT 0,
|
||||||
`alignment` INTEGER NOT NULL DEFAULT 50,
|
`strength` INTEGER NOT NULL DEFAULT 10,
|
||||||
`role` VARCHAR(191) NOT NULL DEFAULT 'player',
|
`dexterity` INTEGER NOT NULL DEFAULT 10,
|
||||||
`positionX` INTEGER NOT NULL DEFAULT 0,
|
`intelligence` INTEGER NOT NULL DEFAULT 10,
|
||||||
`positionY` INTEGER NOT NULL DEFAULT 0,
|
`wisdom` INTEGER NOT NULL DEFAULT 10,
|
||||||
`rotation` INTEGER NOT NULL DEFAULT 0,
|
|
||||||
`zoneId` INTEGER NOT NULL DEFAULT 1,
|
|
||||||
`characterTypeId` INTEGER NULL,
|
|
||||||
|
|
||||||
UNIQUE INDEX `Character_name_key`(`name`),
|
UNIQUE INDEX `Character_name_key`(`name`),
|
||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
@ -92,6 +147,17 @@ CREATE TABLE `CharacterItem` (
|
|||||||
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 `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
|
-- CreateTable
|
||||||
CREATE TABLE `Tile` (
|
CREATE TABLE `Tile` (
|
||||||
`id` VARCHAR(191) NOT NULL,
|
`id` VARCHAR(191) NOT NULL,
|
||||||
@ -120,18 +186,6 @@ CREATE TABLE `Object` (
|
|||||||
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 `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
|
-- CreateTable
|
||||||
CREATE TABLE `Zone` (
|
CREATE TABLE `Zone` (
|
||||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
@ -146,12 +200,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 +239,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,
|
||||||
|
|
||||||
@ -190,9 +256,15 @@ ALTER TABLE `Chat` ADD CONSTRAINT `Chat_zoneId_fkey` FOREIGN KEY (`zoneId`) REFE
|
|||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE `SpriteAction` ADD CONSTRAINT `SpriteAction_spriteId_fkey` FOREIGN KEY (`spriteId`) REFERENCES `Sprite`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
ALTER TABLE `SpriteAction` ADD CONSTRAINT `SpriteAction_spriteId_fkey` FOREIGN KEY (`spriteId`) REFERENCES `Sprite`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `PasswordResetToken` ADD CONSTRAINT `PasswordResetToken_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE `CharacterType` ADD CONSTRAINT `CharacterType_spriteId_fkey` FOREIGN KEY (`spriteId`) REFERENCES `Sprite`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
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
|
-- AddForeignKey
|
||||||
ALTER TABLE `Character` ADD CONSTRAINT `Character_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
ALTER TABLE `Character` ADD CONSTRAINT `Character_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
@ -202,12 +274,24 @@ ALTER TABLE `Character` ADD CONSTRAINT `Character_zoneId_fkey` FOREIGN KEY (`zon
|
|||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE `Character` ADD CONSTRAINT `Character_characterTypeId_fkey` FOREIGN KEY (`characterTypeId`) REFERENCES `CharacterType`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
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
|
-- AddForeignKey
|
||||||
ALTER TABLE `CharacterItem` ADD CONSTRAINT `CharacterItem_characterId_fkey` FOREIGN KEY (`characterId`) REFERENCES `Character`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
ALTER TABLE `CharacterItem` ADD CONSTRAINT `CharacterItem_characterId_fkey` FOREIGN KEY (`characterId`) REFERENCES `Character`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
-- 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 `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;
|
||||||
|
|
||||||
-- 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;
|
||||||
|
|
74
prisma/schema/game.prisma
Normal file
74
prisma/schema/game.prisma
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
model World {
|
||||||
|
date DateTime @unique @default(now())
|
||||||
|
isRainEnabled Boolean @default(false)
|
||||||
|
rainPercentage Int @default(0)
|
||||||
|
isFogEnabled Boolean @default(false)
|
||||||
|
fogDensity Int @default(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
model Chat {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
characterId Int
|
||||||
|
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
|
||||||
|
zoneId Int
|
||||||
|
zone Zone @relation(fields: [zoneId], references: [id], onDelete: Cascade)
|
||||||
|
message String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
}
|
||||||
|
|
||||||
|
model Sprite {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
name String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
spriteActions SpriteAction[]
|
||||||
|
characterTypes CharacterType[]
|
||||||
|
characterHairs CharacterHair[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model SpriteAction {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
spriteId String
|
||||||
|
sprite Sprite @relation(fields: [spriteId], references: [id], onDelete: Cascade)
|
||||||
|
action String
|
||||||
|
sprites Json?
|
||||||
|
originX Decimal @default(0)
|
||||||
|
originY Decimal @default(0)
|
||||||
|
isAnimated Boolean @default(false)
|
||||||
|
isLooping Boolean @default(false)
|
||||||
|
frameWidth Int @default(0)
|
||||||
|
frameHeight Int @default(0)
|
||||||
|
frameSpeed Int @default(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
model Item {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
name String
|
||||||
|
description String?
|
||||||
|
itemType ItemType
|
||||||
|
stackable Boolean @default(false)
|
||||||
|
rarity ItemRarity @default(COMMON)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
characters CharacterItem[]
|
||||||
|
equipment CharacterEquipment[]
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ItemType {
|
||||||
|
WEAPON
|
||||||
|
HELMET
|
||||||
|
CHEST
|
||||||
|
LEGS
|
||||||
|
BOOTS
|
||||||
|
GLOVES
|
||||||
|
RING
|
||||||
|
NECKLACE
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ItemRarity {
|
||||||
|
COMMON
|
||||||
|
UNCOMMON
|
||||||
|
RARE
|
||||||
|
EPIC
|
||||||
|
LEGENDARY
|
||||||
|
}
|
@ -11,7 +11,7 @@
|
|||||||
// npx prisma migrate deploy
|
// npx prisma migrate deploy
|
||||||
|
|
||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
previewFeatures = ["prismaSchemaFolder"]
|
previewFeatures = ["prismaSchemaFolder"]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,13 +19,3 @@ datasource db {
|
|||||||
provider = "mysql"
|
provider = "mysql"
|
||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Chat {
|
|
||||||
id Int @id @default(autoincrement())
|
|
||||||
characterId Int
|
|
||||||
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
|
|
||||||
zoneId Int
|
|
||||||
zone Zone @relation(fields: [zoneId], references: [id], onDelete: Cascade)
|
|
||||||
message String
|
|
||||||
createdAt DateTime
|
|
||||||
}
|
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
model Sprite {
|
|
||||||
id String @id @default(uuid())
|
|
||||||
name String
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
spriteActions SpriteAction[]
|
|
||||||
characterTypes CharacterType[]
|
|
||||||
}
|
|
||||||
|
|
||||||
model SpriteAction {
|
|
||||||
id String @id @default(uuid())
|
|
||||||
spriteId String
|
|
||||||
sprite Sprite @relation(fields: [spriteId], references: [id], onDelete: Cascade)
|
|
||||||
action String
|
|
||||||
sprites Json?
|
|
||||||
originX Decimal @default(0)
|
|
||||||
originY Decimal @default(0)
|
|
||||||
isAnimated Boolean @default(false)
|
|
||||||
isLooping Boolean @default(false)
|
|
||||||
frameWidth Int @default(0)
|
|
||||||
frameHeight Int @default(0)
|
|
||||||
frameSpeed Int @default(0)
|
|
||||||
}
|
|
@ -1,11 +1,3 @@
|
|||||||
model User {
|
|
||||||
id Int @id @default(autoincrement())
|
|
||||||
username String @unique
|
|
||||||
password String
|
|
||||||
online Boolean @default(false)
|
|
||||||
characters Character[]
|
|
||||||
}
|
|
||||||
|
|
||||||
enum CharacterGender {
|
enum CharacterGender {
|
||||||
MALE
|
MALE
|
||||||
FEMALE
|
FEMALE
|
||||||
@ -19,39 +11,83 @@ enum CharacterRace {
|
|||||||
GOBLIN
|
GOBLIN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
username String @unique
|
||||||
|
email String @unique
|
||||||
|
password String
|
||||||
|
online Boolean @default(false)
|
||||||
|
characters Character[]
|
||||||
|
passwordResetTokens PasswordResetToken[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model PasswordResetToken {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
userId Int
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
token String @unique
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
}
|
||||||
|
|
||||||
model CharacterType {
|
model CharacterType {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String
|
name String
|
||||||
gender CharacterGender
|
gender CharacterGender
|
||||||
race CharacterRace
|
race CharacterRace
|
||||||
characters Character[]
|
isEnabledForCharCreation Boolean @default(false)
|
||||||
spriteId String
|
characters Character[]
|
||||||
sprite Sprite @relation(fields: [spriteId], references: [id], onDelete: Cascade)
|
spriteId String?
|
||||||
createdAt DateTime @default(now())
|
sprite Sprite? @relation(fields: [spriteId], references: [id], onDelete: Cascade)
|
||||||
updatedAt DateTime @updatedAt
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model CharacterHair {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
name String
|
||||||
|
gender CharacterGender @default(MALE)
|
||||||
|
isEnabledForCharCreation Boolean @default(false)
|
||||||
|
sprite Sprite? @relation(fields: [spriteId], references: [id], onDelete: Cascade)
|
||||||
|
spriteId String?
|
||||||
|
characters Character[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Character {
|
model Character {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
userId Int
|
userId Int
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
name String @unique
|
name String @unique
|
||||||
online Boolean @default(false)
|
online Boolean @default(false)
|
||||||
hitpoints Int @default(100)
|
role String @default("player")
|
||||||
mana Int @default(100)
|
chats Chat[]
|
||||||
level Int @default(1)
|
|
||||||
experience Int @default(0)
|
// Position
|
||||||
alignment Int @default(50)
|
zoneId Int @default(1)
|
||||||
role String @default("player")
|
zone Zone @relation(fields: [zoneId], references: [id], onDelete: Cascade)
|
||||||
positionX Int @default(0)
|
positionX Int @default(0)
|
||||||
positionY Int @default(0)
|
positionY Int @default(0)
|
||||||
rotation Int @default(0)
|
rotation Int @default(0)
|
||||||
zoneId Int @default(1)
|
|
||||||
zone Zone @relation(fields: [zoneId], references: [id], onDelete: Cascade)
|
// Customization
|
||||||
characterTypeId Int?
|
characterTypeId Int?
|
||||||
characterType CharacterType? @relation(fields: [characterTypeId], references: [id], onDelete: Cascade)
|
characterType CharacterType? @relation(fields: [characterTypeId], references: [id], onDelete: Cascade)
|
||||||
chats Chat[]
|
characterHairId Int?
|
||||||
items CharacterItem[]
|
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 {
|
model CharacterItem {
|
||||||
@ -62,3 +98,22 @@ model CharacterItem {
|
|||||||
item Item @relation(fields: [itemId], references: [id], onDelete: Cascade)
|
item Item @relation(fields: [itemId], references: [id], onDelete: Cascade)
|
||||||
quantity Int
|
quantity Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model CharacterEquipment {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
characterId Int
|
||||||
|
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
|
||||||
|
itemId String
|
||||||
|
item Item @relation(fields: [itemId], references: [id], onDelete: Cascade)
|
||||||
|
quantity Int
|
||||||
|
slot CharacterEquipmentSlotType
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CharacterEquipmentSlotType {
|
||||||
|
HEAD
|
||||||
|
BODY
|
||||||
|
ARMS
|
||||||
|
LEGS
|
||||||
|
NECK
|
||||||
|
RING
|
||||||
|
}
|
||||||
|
@ -21,16 +21,6 @@ model Object {
|
|||||||
ZoneObject ZoneObject[]
|
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 {
|
model Zone {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String
|
name String
|
||||||
@ -38,6 +28,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 +38,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 +81,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
|
||||||
}
|
}
|
||||||
|
@ -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 })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
58
src/commands/tiles.ts
Normal 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.')
|
||||||
|
}
|
||||||
|
}
|
@ -1,42 +0,0 @@
|
|||||||
import { ExtendedCharacter, TSocket } from '../utilities/types'
|
|
||||||
import { Zone } from '@prisma/client'
|
|
||||||
import prisma from '../utilities/prisma'
|
|
||||||
|
|
||||||
class CharacterManager {
|
|
||||||
private characters!: ExtendedCharacter[]
|
|
||||||
|
|
||||||
public async boot() {
|
|
||||||
this.characters = []
|
|
||||||
}
|
|
||||||
|
|
||||||
public initCharacter(character: ExtendedCharacter) {
|
|
||||||
this.characters = [...this.characters, character]
|
|
||||||
}
|
|
||||||
|
|
||||||
public async removeCharacter(character: ExtendedCharacter) {
|
|
||||||
await prisma.character.update({
|
|
||||||
where: { id: character.id },
|
|
||||||
data: {
|
|
||||||
positionX: character.positionX,
|
|
||||||
positionY: character.positionY,
|
|
||||||
rotation: character.rotation,
|
|
||||||
zoneId: character.zoneId
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.characters = this.characters.filter((x) => x.id !== character.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
public getCharacterFromSocket(socket: TSocket) {
|
|
||||||
return this.characters.find((x) => x.id === socket?.characterId)
|
|
||||||
}
|
|
||||||
|
|
||||||
public hasResetMovement(character: ExtendedCharacter) {
|
|
||||||
return this.characters.find((x) => x.id === character.id)?.resetMovement
|
|
||||||
}
|
|
||||||
|
|
||||||
public getCharactersInZone(zone: Zone) {
|
|
||||||
return this.characters.filter((x) => x.zoneId === zone.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new CharacterManager()
|
|
@ -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}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
96
src/managers/dateManager.ts
Normal file
96
src/managers/dateManager.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import { Server } from 'socket.io'
|
||||||
|
import { appLogger } from '../utilities/logger'
|
||||||
|
import worldService from '../services/worldService'
|
||||||
|
import worldRepository from '../repositories/worldRepository'
|
||||||
|
|
||||||
|
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 async setTime(time: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
let newDate: Date
|
||||||
|
|
||||||
|
// Check if it's just a time (HH:mm or HH:mm:ss format)
|
||||||
|
if (/^\d{1,2}:\d{2}(:\d{2})?$/.test(time)) {
|
||||||
|
const [hours, minutes] = time.split(':').map(Number)
|
||||||
|
newDate = new Date(this.currentDate) // Clone current date
|
||||||
|
newDate.setHours(hours, minutes)
|
||||||
|
} else {
|
||||||
|
// Treat as full datetime string
|
||||||
|
newDate = new Date(time)
|
||||||
|
if (isNaN(newDate.getTime())) return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentDate = newDate
|
||||||
|
this.emitDate()
|
||||||
|
await this.saveDate()
|
||||||
|
} catch (error) {
|
||||||
|
appLogger.error(`Failed to set time: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadDate(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const world = await worldRepository.getFirst()
|
||||||
|
|
||||||
|
if (world) {
|
||||||
|
this.currentDate = world.date
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
appLogger.error(`Failed to load date: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
this.currentDate = new Date() // Use current date as fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private startDateLoop(): void {
|
||||||
|
this.intervalId = setInterval(() => {
|
||||||
|
this.advanceGameTime()
|
||||||
|
this.emitDate()
|
||||||
|
void 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 worldService.update({
|
||||||
|
date: this.currentDate
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
appLogger.error(`Failed to save date: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public cleanup(): void {
|
||||||
|
if (this.intervalId) {
|
||||||
|
clearInterval(this.intervalId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCurrentDate(): Date {
|
||||||
|
return this.currentDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new DateManager()
|
@ -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}`)
|
||||||
|
127
src/managers/weatherManager.ts
Normal file
127
src/managers/weatherManager.ts
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import { Server } from 'socket.io'
|
||||||
|
import { appLogger } from '../utilities/logger'
|
||||||
|
import worldService from '../services/worldService'
|
||||||
|
import worldRepository from '../repositories/worldRepository'
|
||||||
|
|
||||||
|
interface WeatherState {
|
||||||
|
isRainEnabled: boolean
|
||||||
|
rainPercentage: number
|
||||||
|
isFogEnabled: boolean
|
||||||
|
fogDensity: number
|
||||||
|
}
|
||||||
|
|
||||||
|
class WeatherManager {
|
||||||
|
private static readonly UPDATE_INTERVAL = 60000 // Check weather every minute
|
||||||
|
private static readonly RAIN_CHANCE = 0.2 // 20% chance of rain
|
||||||
|
private static readonly FOG_CHANCE = 0.15 // 15% chance of fog
|
||||||
|
|
||||||
|
private io: Server | null = null
|
||||||
|
private intervalId: NodeJS.Timeout | null = null
|
||||||
|
private weatherState: WeatherState = {
|
||||||
|
isRainEnabled: false,
|
||||||
|
rainPercentage: 0,
|
||||||
|
isFogEnabled: false,
|
||||||
|
fogDensity: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
public async boot(io: Server): Promise<void> {
|
||||||
|
this.io = io
|
||||||
|
await this.loadWeather()
|
||||||
|
this.startWeatherLoop()
|
||||||
|
appLogger.info('Weather manager loaded')
|
||||||
|
}
|
||||||
|
|
||||||
|
public async toggleRain(): Promise<void> {
|
||||||
|
this.weatherState.isRainEnabled = !this.weatherState.isRainEnabled
|
||||||
|
this.weatherState.rainPercentage = this.weatherState.isRainEnabled
|
||||||
|
? Math.floor(Math.random() * 50) + 50 // 50-100%
|
||||||
|
: 0
|
||||||
|
|
||||||
|
await this.saveWeather()
|
||||||
|
this.emitWeather()
|
||||||
|
}
|
||||||
|
|
||||||
|
public async toggleFog(): Promise<void> {
|
||||||
|
this.weatherState.isFogEnabled = !this.weatherState.isFogEnabled
|
||||||
|
this.weatherState.fogDensity = this.weatherState.isFogEnabled
|
||||||
|
? Math.floor((Math.random() * 0.7 + 0.3) * 100) // Convert 0.3-1.0 to 30-100
|
||||||
|
: 0
|
||||||
|
|
||||||
|
await this.saveWeather()
|
||||||
|
this.emitWeather()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadWeather(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const world = await worldRepository.getFirst()
|
||||||
|
|
||||||
|
if (world) {
|
||||||
|
this.weatherState = {
|
||||||
|
isRainEnabled: world.isRainEnabled,
|
||||||
|
rainPercentage: world.rainPercentage,
|
||||||
|
isFogEnabled: world.isFogEnabled,
|
||||||
|
fogDensity: world.fogDensity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
appLogger.error(`Failed to load weather: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getWeatherState(): WeatherState {
|
||||||
|
return this.weatherState
|
||||||
|
}
|
||||||
|
|
||||||
|
private startWeatherLoop(): void {
|
||||||
|
this.intervalId = setInterval(async () => {
|
||||||
|
this.updateWeather()
|
||||||
|
this.emitWeather()
|
||||||
|
await this.saveWeather().catch((error) => {
|
||||||
|
appLogger.error(`Failed to save weather: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
})
|
||||||
|
}, WeatherManager.UPDATE_INTERVAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateWeather(): void {
|
||||||
|
// Update rain
|
||||||
|
if (Math.random() < WeatherManager.RAIN_CHANCE) {
|
||||||
|
this.weatherState.isRainEnabled = !this.weatherState.isRainEnabled
|
||||||
|
this.weatherState.rainPercentage = this.weatherState.isRainEnabled
|
||||||
|
? Math.floor(Math.random() * 50) + 50 // 50-100%
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update fog
|
||||||
|
if (Math.random() < WeatherManager.FOG_CHANCE) {
|
||||||
|
this.weatherState.isFogEnabled = !this.weatherState.isFogEnabled
|
||||||
|
this.weatherState.fogDensity = this.weatherState.isFogEnabled
|
||||||
|
? Math.floor((Math.random() * 0.7 + 0.3) * 100) // Convert 0.3-1.0 to 30-100
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitWeather(): void {
|
||||||
|
this.io?.emit('weather', this.weatherState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async saveWeather(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await worldService.update({
|
||||||
|
isRainEnabled: this.weatherState.isRainEnabled,
|
||||||
|
rainPercentage: this.weatherState.rainPercentage,
|
||||||
|
isFogEnabled: this.weatherState.isFogEnabled,
|
||||||
|
fogDensity: this.weatherState.fogDensity
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
appLogger.error(`Failed to save weather: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public cleanup(): void {
|
||||||
|
if (this.intervalId) {
|
||||||
|
clearInterval(this.intervalId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new WeatherManager()
|
@ -2,71 +2,54 @@ 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'
|
import ZoneCharacter from '../models/zoneCharacter'
|
||||||
|
|
||||||
class ZoneManager {
|
class ZoneManager {
|
||||||
private loadedZones: LoadedZone[] = []
|
private readonly zones = new Map<number, LoadedZone>()
|
||||||
|
|
||||||
// Method to initialize zoneEditor manager
|
public async boot(): Promise<void> {
|
||||||
public async boot() {
|
// Create first zone if it doesn't exist
|
||||||
if (!(await ZoneRepository.getById(1))) {
|
if (!(await ZoneRepository.getById(1))) {
|
||||||
const zoneService = new ZoneService()
|
await new ZoneService().createDemoZone()
|
||||||
await zoneService.createDemoZone()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const zones = await ZoneRepository.getAll()
|
const zones = await ZoneRepository.getAll()
|
||||||
|
await Promise.all(zones.map((zone) => this.loadZone(zone)))
|
||||||
|
|
||||||
for (const zone of zones) {
|
gameLogger.info(`Zone manager loaded with ${this.zones.size} zones`)
|
||||||
await this.loadZone(zone)
|
|
||||||
}
|
|
||||||
|
|
||||||
gameMasterLogger.info('Zone manager loaded')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getZoneAssets(zone: Zone): Promise<ZoneAssets> {
|
public async loadZone(zone: Zone): Promise<void> {
|
||||||
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
|
|
||||||
public async loadZone(zone: Zone) {
|
|
||||||
const loadedZone = new LoadedZone(zone)
|
const loadedZone = new LoadedZone(zone)
|
||||||
this.loadedZones.push(loadedZone)
|
this.zones.set(zone.id, 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
|
public unloadZone(zoneId: number): void {
|
||||||
public unloadZone(zoneId: number) {
|
this.zones.delete(zoneId)
|
||||||
this.loadedZones = this.loadedZones.filter((loadedZone) => loadedZone.getZone().id !== zoneId)
|
gameLogger.info(`Zone ID ${zoneId} unloaded`)
|
||||||
gameMasterLogger.info(`Zone ID ${zoneId} unloaded`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getter for loaded zones
|
|
||||||
public getLoadedZones(): LoadedZone[] {
|
public getLoadedZones(): LoadedZone[] {
|
||||||
return this.loadedZones
|
return Array.from(this.zones.values())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getter for zone by id
|
|
||||||
public getZoneById(zoneId: number): LoadedZone | undefined {
|
public getZoneById(zoneId: number): LoadedZone | undefined {
|
||||||
return this.loadedZones.find((loadedZone) => loadedZone.getZone().id === zoneId)
|
return this.zones.get(zoneId)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export interface ZoneAssets {
|
public getCharacter(characterId: number): ZoneCharacter | undefined {
|
||||||
tiles: string[]
|
for (const zone of this.zones.values()) {
|
||||||
objects: string[]
|
const character = zone.getCharactersInZone().find((char) => char.character.id === characterId)
|
||||||
|
if (character) return character
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeCharacter(characterId: number): void {
|
||||||
|
this.zones.forEach((zone) => zone.removeCharacter(characterId))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new ZoneManager()
|
export default new ZoneManager()
|
||||||
|
@ -37,7 +37,7 @@ export async function Authentication(socket: TSocket, next: any) {
|
|||||||
return next(new Error('Authentication error'))
|
return next(new Error('Authentication error'))
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.user = (await UserRepository.getById(decoded.id)) as User
|
socket.userId = decoded.id
|
||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import { Zone } from '@prisma/client'
|
import { Character, Zone } from '@prisma/client'
|
||||||
import zoneRepository from '../repositories/zoneRepository'
|
import zoneEventTileRepository from '../repositories/zoneEventTileRepository'
|
||||||
import characterManager from '../managers/characterManager'
|
import ZoneCharacter from './zoneCharacter'
|
||||||
import { ExtendedCharacter } from '../utilities/types'
|
|
||||||
|
|
||||||
class LoadedZone {
|
class LoadedZone {
|
||||||
private readonly zone: Zone
|
private readonly zone: Zone
|
||||||
// private readonly npcs: ZoneNPC[] = []
|
private characters: ZoneCharacter[] = []
|
||||||
|
|
||||||
constructor(zone: Zone) {
|
constructor(zone: Zone) {
|
||||||
this.zone = zone
|
this.zone = zone
|
||||||
@ -15,10 +14,31 @@ class LoadedZone {
|
|||||||
return this.zone
|
return this.zone
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public addCharacter(character: Character) {
|
||||||
|
const zoneCharacter = new ZoneCharacter(character)
|
||||||
|
this.characters.push(zoneCharacter)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async removeCharacter(id: number) {
|
||||||
|
const zoneCharacter = this.getCharacterById(id)
|
||||||
|
if (zoneCharacter) {
|
||||||
|
await zoneCharacter.savePosition()
|
||||||
|
this.characters = this.characters.filter((c) => c.character.id !== id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCharacterById(id: number): ZoneCharacter | undefined {
|
||||||
|
return this.characters.find((c) => c.character.id === id)
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCharactersInZone(): ZoneCharacter[] {
|
||||||
|
return this.characters
|
||||||
|
}
|
||||||
|
|
||||||
public async getGrid(): Promise<number[][]> {
|
public async getGrid(): Promise<number[][]> {
|
||||||
let grid: number[][] = Array.from({ length: this.zone.height }, () => Array.from({ length: this.zone.width }, () => 0))
|
let grid: number[][] = Array.from({ length: this.zone.height }, () => Array.from({ length: this.zone.width }, () => 0))
|
||||||
|
|
||||||
const eventTiles = await zoneRepository.getEventTiles(this.zone.id)
|
const eventTiles = await zoneEventTileRepository.getAll(this.zone.id)
|
||||||
|
|
||||||
// Set the grid values based on the event tiles, these are strings
|
// Set the grid values based on the event tiles, these are strings
|
||||||
eventTiles.forEach((eventTile) => {
|
eventTiles.forEach((eventTile) => {
|
||||||
@ -29,20 +49,6 @@ class LoadedZone {
|
|||||||
|
|
||||||
return grid
|
return grid
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @TODO: Implement this
|
|
||||||
* @param position
|
|
||||||
*/
|
|
||||||
public async isPositionWalkable(position: { x: number; y: number }): Promise<boolean> {
|
|
||||||
const grid = await this.getGrid()
|
|
||||||
if (!grid?.length) return false
|
|
||||||
|
|
||||||
const gridX = Math.floor(position.x)
|
|
||||||
const gridY = Math.floor(position.y)
|
|
||||||
|
|
||||||
return grid[gridY]?.[gridX] === 1 || grid[gridY]?.[Math.ceil(position.x)] === 1 || grid[Math.ceil(position.y)]?.[gridX] === 1 || grid[Math.ceil(position.y)]?.[Math.ceil(position.x)] === 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LoadedZone
|
export default LoadedZone
|
||||||
|
19
src/models/zoneCharacter.ts
Normal file
19
src/models/zoneCharacter.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Character } from '@prisma/client'
|
||||||
|
import { CharacterService } from '../services/characterService'
|
||||||
|
|
||||||
|
class ZoneCharacter {
|
||||||
|
public readonly character: Character
|
||||||
|
public isMoving: boolean = false
|
||||||
|
public currentPath: Array<{ x: number; y: number }> | null = null
|
||||||
|
|
||||||
|
constructor(character: Character) {
|
||||||
|
this.character = character
|
||||||
|
}
|
||||||
|
|
||||||
|
public async savePosition() {
|
||||||
|
const characterService = new CharacterService()
|
||||||
|
await characterService.updateCharacterPosition(this.character.id, this.character.positionX, this.character.positionY, this.character.rotation, this.character.zoneId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ZoneCharacter
|
17
src/repositories/characterHairRepository.ts
Normal file
17
src/repositories/characterHairRepository.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import prisma from '../utilities/prisma' // Import the global Prisma instance
|
||||||
|
import { CharacterHair } from '@prisma/client'
|
||||||
|
|
||||||
|
class CharacterHairRepository {
|
||||||
|
async getAll(): Promise<CharacterHair[]> {
|
||||||
|
return prisma.characterHair.findMany()
|
||||||
|
}
|
||||||
|
async getIsEnabledForCharCreationHair(): Promise<CharacterHair[]> {
|
||||||
|
return prisma.characterHair.findMany({
|
||||||
|
where: {
|
||||||
|
isEnabledForCharCreation: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new CharacterHairRepository()
|
@ -1,8 +1,8 @@
|
|||||||
import prisma from '../utilities/prisma' // Import the global Prisma instance
|
import prisma from '../utilities/prisma' // Import the global Prisma instance
|
||||||
import { Character } from '@prisma/client'
|
import { appLogger } from '../utilities/logger'
|
||||||
|
|
||||||
class CharacterRepository {
|
class CharacterRepository {
|
||||||
async getByUserId(userId: number): Promise<Character[] | null> {
|
async getByUserId(userId: number) {
|
||||||
try {
|
try {
|
||||||
return await prisma.character.findMany({
|
return await prisma.character.findMany({
|
||||||
where: {
|
where: {
|
||||||
@ -14,16 +14,22 @@ class CharacterRepository {
|
|||||||
include: {
|
include: {
|
||||||
sprite: true
|
sprite: true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
characterHair: {
|
||||||
|
include: {
|
||||||
|
sprite: true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Handle error
|
// Handle error
|
||||||
throw new Error(`Failed to get character by user ID: ${error.message}`)
|
appLogger.error(`Failed to get character by user ID: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getByUserAndId(userId: number, characterId: number): Promise<Character | null> {
|
async getByUserAndId(userId: number, characterId: number) {
|
||||||
try {
|
try {
|
||||||
return await prisma.character.findFirst({
|
return await prisma.character.findFirst({
|
||||||
where: {
|
where: {
|
||||||
@ -36,16 +42,22 @@ class CharacterRepository {
|
|||||||
include: {
|
include: {
|
||||||
sprite: true
|
sprite: true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
characterHair: {
|
||||||
|
include: {
|
||||||
|
sprite: true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Handle error
|
// Handle error
|
||||||
throw new Error(`Failed to get character by user ID and character ID: ${error.message}`)
|
appLogger.error(`Failed to get character by user ID and character ID: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getById(id: number): Promise<Character | null> {
|
async getById(id: number) {
|
||||||
try {
|
try {
|
||||||
return await prisma.character.findUnique({
|
return await prisma.character.findUnique({
|
||||||
where: {
|
where: {
|
||||||
@ -57,47 +69,22 @@ class CharacterRepository {
|
|||||||
include: {
|
include: {
|
||||||
sprite: true
|
sprite: true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
characterHair: {
|
||||||
|
include: {
|
||||||
|
sprite: true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Handle error
|
// Handle error
|
||||||
throw new Error(`Failed to get character by ID: ${error.message}`)
|
appLogger.error(`Failed to get character by ID: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updatePosition(id: number, positionX: number, positionY: number): Promise<Character | null> {
|
async getByName(name: string) {
|
||||||
try {
|
|
||||||
return await prisma.character.update({
|
|
||||||
where: {
|
|
||||||
id: id
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
positionX,
|
|
||||||
positionY
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} catch (error: any) {
|
|
||||||
// Handle error
|
|
||||||
throw new Error(`Failed to update character: ${error.message}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteByUserIdAndId(userId: number, characterId: number): Promise<Character | null> {
|
|
||||||
try {
|
|
||||||
return await prisma.character.delete({
|
|
||||||
where: {
|
|
||||||
userId,
|
|
||||||
id: characterId
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} catch (error: any) {
|
|
||||||
// Handle error
|
|
||||||
throw new Error(`Failed to delete character by user ID and character ID: ${error.message}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getByName(name: string): Promise<Character | null> {
|
|
||||||
try {
|
try {
|
||||||
return await prisma.character.findFirst({
|
return await prisma.character.findFirst({
|
||||||
where: {
|
where: {
|
||||||
@ -109,12 +96,18 @@ class CharacterRepository {
|
|||||||
include: {
|
include: {
|
||||||
sprite: true
|
sprite: true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
characterHair: {
|
||||||
|
include: {
|
||||||
|
sprite: true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Handle error
|
// Handle error
|
||||||
throw new Error(`Failed to get character by name: ${error.message}`)
|
appLogger.error(`Failed to get character by name: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
10
src/repositories/characterTypeRepository.ts
Normal file
10
src/repositories/characterTypeRepository.ts
Normal 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()
|
49
src/repositories/chatRepository.ts
Normal file
49
src/repositories/chatRepository.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import prisma from '../utilities/prisma'
|
||||||
|
import { Chat } from '@prisma/client'
|
||||||
|
|
||||||
|
class ChatRepository {
|
||||||
|
async getById(id: number): Promise<Chat | null> {
|
||||||
|
return prisma.chat.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: {
|
||||||
|
character: true,
|
||||||
|
zone: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAll(): Promise<Chat[]> {
|
||||||
|
return prisma.chat.findMany({
|
||||||
|
include: {
|
||||||
|
character: true,
|
||||||
|
zone: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async getByCharacterId(characterId: number): Promise<Chat[]> {
|
||||||
|
return prisma.chat.findMany({
|
||||||
|
where: {
|
||||||
|
characterId
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
character: true,
|
||||||
|
zone: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async getByZoneId(zoneId: number): Promise<Chat[]> {
|
||||||
|
return prisma.chat.findMany({
|
||||||
|
where: {
|
||||||
|
zoneId
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
character: true,
|
||||||
|
zone: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new ChatRepository()
|
45
src/repositories/passwordResetTokenRepository.ts
Normal file
45
src/repositories/passwordResetTokenRepository.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import prisma from '../utilities/prisma'
|
||||||
|
import { appLogger } from '../utilities/logger' // Import the global Prisma instance
|
||||||
|
|
||||||
|
class PasswordResetTokenRepository {
|
||||||
|
async getById(id: number): Promise<any> {
|
||||||
|
try {
|
||||||
|
return await prisma.passwordResetToken.findUnique({
|
||||||
|
where: {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
// Handle error
|
||||||
|
appLogger.error(`Failed to get password reset token by ID: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getByUserId(userId: number): Promise<any> {
|
||||||
|
try {
|
||||||
|
return await prisma.passwordResetToken.findFirst({
|
||||||
|
where: {
|
||||||
|
userId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
// Handle error
|
||||||
|
appLogger.error(`Failed to get password reset token by user ID: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getByToken(token: string): Promise<any> {
|
||||||
|
try {
|
||||||
|
return await prisma.passwordResetToken.findFirst({
|
||||||
|
where: {
|
||||||
|
token
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
// Handle error
|
||||||
|
appLogger.error(`Failed to get password reset token by token: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new PasswordResetTokenRepository()
|
@ -1,5 +1,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) {
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import prisma from '../utilities/prisma' // Import the global Prisma instance
|
import prisma from '../utilities/prisma' // Import the global Prisma instance
|
||||||
import { Tile } from '@prisma/client'
|
import { Tile } from '@prisma/client'
|
||||||
|
import zoneRepository from './zoneRepository'
|
||||||
|
import { unduplicateArray } from '../utilities/utilities'
|
||||||
|
import { FlattenZoneArray } from '../utilities/zone'
|
||||||
|
|
||||||
class TileRepository {
|
class TileRepository {
|
||||||
async getById(id: string): Promise<Tile | null> {
|
async getById(id: string): Promise<Tile | null> {
|
||||||
@ -8,9 +11,28 @@ class TileRepository {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getByIds(ids: string[]): Promise<Tile[]> {
|
||||||
|
return prisma.tile.findMany({
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
in: ids
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async getAll(): Promise<Tile[]> {
|
async getAll(): Promise<Tile[]> {
|
||||||
return prisma.tile.findMany()
|
return prisma.tile.findMany()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getByZoneId(zoneId: number): Promise<Tile[]> {
|
||||||
|
const zone = await zoneRepository.getById(zoneId)
|
||||||
|
if (!zone) return []
|
||||||
|
|
||||||
|
const zoneTileArray = unduplicateArray(FlattenZoneArray(JSON.parse(JSON.stringify(zone.tiles))))
|
||||||
|
|
||||||
|
return this.getByIds(zoneTileArray)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new TileRepository()
|
export default new TileRepository()
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import prisma from '../utilities/prisma' // Import the global Prisma instance
|
import prisma from '../utilities/prisma' // Import the global Prisma instance
|
||||||
import { User } from '@prisma/client'
|
import { User } from '@prisma/client'
|
||||||
|
import { appLogger } from '../utilities/logger'
|
||||||
|
|
||||||
class UserRepository {
|
class UserRepository {
|
||||||
async getById(id: number): Promise<User | null> {
|
async getById(id: number): Promise<User | null> {
|
||||||
@ -11,7 +12,8 @@ class UserRepository {
|
|||||||
})
|
})
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Handle error
|
// Handle error
|
||||||
throw new Error(`Failed to get user by ID: ${error.message}`)
|
appLogger.error(`Failed to get user by ID: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,7 +26,22 @@ class UserRepository {
|
|||||||
})
|
})
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Handle error
|
// Handle error
|
||||||
throw new Error(`Failed to get user by username: ${error.message}`)
|
appLogger.error(`Failed to get user by username: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getByEmail(email: string): Promise<User | null> {
|
||||||
|
try {
|
||||||
|
return await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
email
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
// Handle error
|
||||||
|
appLogger.error(`Failed to get user by email: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
19
src/repositories/worldRepository.ts
Normal file
19
src/repositories/worldRepository.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import prisma from '../utilities/prisma' // Import the global Prisma instance
|
||||||
|
import { World } from '@prisma/client'
|
||||||
|
import { gameLogger } from '../utilities/logger'
|
||||||
|
|
||||||
|
class WorldRepository {
|
||||||
|
async getFirst(): Promise<World | null> {
|
||||||
|
try {
|
||||||
|
return await prisma.world.findFirst({
|
||||||
|
orderBy: { date: 'desc' }
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
// Handle error
|
||||||
|
gameLogger.error(`Failed to get first world: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new WorldRepository()
|
36
src/repositories/zoneEventTileRepository.ts
Normal file
36
src/repositories/zoneEventTileRepository.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { ZoneEventTile } from '@prisma/client'
|
||||||
|
import prisma from '../utilities/prisma'
|
||||||
|
import { appLogger } from '../utilities/logger'
|
||||||
|
|
||||||
|
class ZoneEventTileRepository {
|
||||||
|
async getAll(id: number): Promise<ZoneEventTile[]> {
|
||||||
|
try {
|
||||||
|
return await prisma.zoneEventTile.findMany({
|
||||||
|
where: {
|
||||||
|
zoneId: id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
appLogger.error(`Failed to get zone event tiles: ${error.message}`)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEventTileByZoneIdAndPosition(zoneId: number, positionX: number, positionY: number) {
|
||||||
|
try {
|
||||||
|
return await prisma.zoneEventTile.findFirst({
|
||||||
|
where: {
|
||||||
|
zoneId: zoneId,
|
||||||
|
positionX: positionX,
|
||||||
|
positionY: positionY
|
||||||
|
},
|
||||||
|
include: { teleport: true }
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
appLogger.error(`Failed to get zone event tile: ${error.message}`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new ZoneEventTileRepository()
|
@ -1,18 +1,9 @@
|
|||||||
import { Zone, ZoneEventTile, ZoneEventTileType, ZoneObject } from '@prisma/client'
|
import { Zone, ZoneEventTile, ZoneEventTileType, ZoneObject } from '@prisma/client'
|
||||||
import prisma from '../utilities/prisma'
|
import prisma from '../utilities/prisma'
|
||||||
import { ZoneEventTileWithTeleport } from '../socketEvents/zone/characterMove'
|
import { ZoneEventTileWithTeleport } from '../utilities/types'
|
||||||
import { appLogger } from '../utilities/logger'
|
import { appLogger } from '../utilities/logger'
|
||||||
|
|
||||||
class ZoneRepository {
|
class ZoneRepository {
|
||||||
async getFirst(): Promise<Zone | null> {
|
|
||||||
try {
|
|
||||||
return await prisma.zone.findFirst()
|
|
||||||
} catch (error: any) {
|
|
||||||
appLogger.error(`Failed to get first zone: ${error.message}`)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAll(): Promise<Zone[]> {
|
async getAll(): Promise<Zone[]> {
|
||||||
try {
|
try {
|
||||||
return await prisma.zone.findMany()
|
return await prisma.zone.findMany()
|
||||||
@ -22,7 +13,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 +30,8 @@ class ZoneRepository {
|
|||||||
include: {
|
include: {
|
||||||
object: true
|
object: true
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
zoneEffects: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -61,22 +53,23 @@ class ZoneRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEventTeleportTiles(id: number): Promise<ZoneEventTileWithTeleport[]> {
|
async getFirstEventTile(zoneId: number, positionX: number, positionY: number): Promise<ZoneEventTile | null> {
|
||||||
try {
|
try {
|
||||||
return (await prisma.zoneEventTile.findMany({
|
return await prisma.zoneEventTile.findFirst({
|
||||||
where: {
|
where: {
|
||||||
zoneId: id,
|
zoneId: zoneId,
|
||||||
type: ZoneEventTileType.TELEPORT
|
positionX: positionX,
|
||||||
|
positionY: positionY
|
||||||
},
|
},
|
||||||
include: { teleport: true }
|
include: { teleport: true }
|
||||||
})) as unknown as ZoneEventTileWithTeleport[]
|
})
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
appLogger.error(`Failed to get zone event tiles: ${error.message}`)
|
appLogger.error(`Failed to get zone event tile: ${error.message}`)
|
||||||
return []
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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: {
|
||||||
|
@ -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 QueueManager from './managers/queueManager'
|
import QueueManager from './managers/queueManager'
|
||||||
|
import DateManager from './managers/dateManager'
|
||||||
|
import WeatherManager from './managers/weatherManager'
|
||||||
|
|
||||||
export class Server {
|
export class Server {
|
||||||
private readonly app: Application
|
private readonly app: Application
|
||||||
@ -27,7 +27,14 @@ export class Server {
|
|||||||
*/
|
*/
|
||||||
constructor() {
|
constructor() {
|
||||||
this.app = express()
|
this.app = express()
|
||||||
this.app.use(cors())
|
this.app.use(
|
||||||
|
cors({
|
||||||
|
origin: config.CLIENT_URL,
|
||||||
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // Add supported methods
|
||||||
|
allowedHeaders: ['Content-Type', 'Authorization'], // Add allowed headers
|
||||||
|
credentials: true
|
||||||
|
})
|
||||||
|
)
|
||||||
this.app.use(express.json())
|
this.app.use(express.json())
|
||||||
this.app.use(express.urlencoded({ extended: true }))
|
this.app.use(express.urlencoded({ extended: true }))
|
||||||
this.http = httpServer(this.app)
|
this.http = httpServer(this.app)
|
||||||
@ -58,23 +65,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 weather manager
|
||||||
|
await WeatherManager.boot(this.io)
|
||||||
|
|
||||||
// Load zoneEditor manager
|
// Load zoneEditor manager
|
||||||
await ZoneManager.boot()
|
await ZoneManager.boot()
|
||||||
|
|
||||||
// Load character manager
|
// Load command manager
|
||||||
await CharacterManager.boot()
|
await CommandManager.boot(this.io)
|
||||||
|
|
||||||
// Load command manager - Disabled for now
|
|
||||||
// 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 +96,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)}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
class AssetService {}
|
|
||||||
|
|
||||||
export default AssetService
|
|
@ -1,43 +0,0 @@
|
|||||||
import { ExtendedCharacter } from '../../utilities/types'
|
|
||||||
import { AStar } from '../../utilities/character/aStar'
|
|
||||||
import ZoneManager from '../../managers/zoneManager'
|
|
||||||
import Rotation from '../../utilities/character/rotation'
|
|
||||||
import { gameLogger } from '../../utilities/logger'
|
|
||||||
|
|
||||||
export class CharacterMoveService {
|
|
||||||
public updatePosition(character: ExtendedCharacter, position: { x: number; y: number }, newZoneId?: number) {
|
|
||||||
Object.assign(character, {
|
|
||||||
positionX: position.x,
|
|
||||||
positionY: position.y,
|
|
||||||
rotation: Rotation.calculate(character.positionX, character.positionY, position.x, position.y),
|
|
||||||
zoneId: newZoneId || character.zoneId
|
|
||||||
})
|
|
||||||
|
|
||||||
// await prisma.character.update({
|
|
||||||
// where: { id: character.id },
|
|
||||||
// data: {
|
|
||||||
// positionX: position.x,
|
|
||||||
// positionY: position.y,
|
|
||||||
// rotation: character.rotation,
|
|
||||||
// zoneId: newZoneId
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
}
|
|
||||||
|
|
||||||
public async calculatePath(character: ExtendedCharacter, targetX: number, targetY: number): Promise<Array<{ x: number; y: number }> | null> {
|
|
||||||
const grid = await ZoneManager.getZoneById(character.zoneId)?.getGrid()
|
|
||||||
if (!grid?.length) {
|
|
||||||
gameLogger.error('character:move error', 'Grid not found or empty')
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const start = { x: Math.floor(character.positionX), y: Math.floor(character.positionY) }
|
|
||||||
const end = { x: Math.floor(targetX), y: Math.floor(targetY) }
|
|
||||||
|
|
||||||
return AStar.findPath(start, end, grid)
|
|
||||||
}
|
|
||||||
|
|
||||||
public async applyMovementDelay(): Promise<void> {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 250)) // 250ms delay between steps
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
import { Character } from '@prisma/client'
|
|
||||||
|
|
||||||
class CharacterService {}
|
|
||||||
|
|
||||||
export default CharacterService
|
|
104
src/services/characterService.ts
Normal file
104
src/services/characterService.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import { AStar } from '../utilities/character/aStar'
|
||||||
|
import ZoneManager from '../managers/zoneManager'
|
||||||
|
import prisma from '../utilities/prisma'
|
||||||
|
import Rotation from '../utilities/character/rotation'
|
||||||
|
import { appLogger, gameLogger } from '../utilities/logger'
|
||||||
|
import { Character } from '@prisma/client'
|
||||||
|
|
||||||
|
interface Position {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CharacterService {
|
||||||
|
private readonly MOVEMENT_DELAY_MS = 250
|
||||||
|
|
||||||
|
async create(name: string, userId: number) {
|
||||||
|
return prisma.character.create({
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
userId
|
||||||
|
// characterTypeId: 1 // @TODO set to chosen character type
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateHair(characterId: number, characterHairId: number | null) {
|
||||||
|
await prisma.character.update({
|
||||||
|
where: { id: characterId },
|
||||||
|
data: {
|
||||||
|
characterHairId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteByUserIdAndId(userId: number, characterId: number): Promise<Character | null> {
|
||||||
|
try {
|
||||||
|
return await prisma.character.delete({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
id: characterId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
// Handle error
|
||||||
|
appLogger.error(`Failed to delete character by user ID and character ID: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateCharacterPosition(id: number, positionX: number, positionY: number, rotation: number, zoneId: number) {
|
||||||
|
await prisma.character.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
positionX,
|
||||||
|
positionY,
|
||||||
|
rotation,
|
||||||
|
zoneId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public updatePosition(character: Character, position: Position, newZoneId?: number): void {
|
||||||
|
if (!this.isValidPosition(position)) {
|
||||||
|
gameLogger.error(`Invalid position coordinates: ${position.x}, ${position.y}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(character, {
|
||||||
|
positionX: position.x,
|
||||||
|
positionY: position.y,
|
||||||
|
rotation: Rotation.calculate(character.positionX, character.positionY, position.x, position.y),
|
||||||
|
zoneId: newZoneId ?? character.zoneId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public async calculatePath(character: Character, targetX: number, targetY: number): Promise<Position[] | null> {
|
||||||
|
const zone = ZoneManager.getZoneById(character.zoneId)
|
||||||
|
const grid = await zone?.getGrid()
|
||||||
|
|
||||||
|
if (!grid?.length) {
|
||||||
|
gameLogger.error('character:move error', 'Grid not found or empty')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const start: Position = {
|
||||||
|
x: Math.floor(character.positionX),
|
||||||
|
y: Math.floor(character.positionY)
|
||||||
|
}
|
||||||
|
|
||||||
|
const end: Position = {
|
||||||
|
x: Math.floor(targetX),
|
||||||
|
y: Math.floor(targetY)
|
||||||
|
}
|
||||||
|
|
||||||
|
return AStar.findPath(start, end, grid)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async applyMovementDelay(): Promise<void> {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, this.MOVEMENT_DELAY_MS))
|
||||||
|
}
|
||||||
|
|
||||||
|
private isValidPosition(position: Position): boolean {
|
||||||
|
return Number.isFinite(position.x) && Number.isFinite(position.y) && position.x >= 0 && position.y >= 0
|
||||||
|
}
|
||||||
|
}
|
30
src/services/chatService.ts
Normal file
30
src/services/chatService.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import prisma from '../utilities/prisma'
|
||||||
|
import { gameLogger } from '../utilities/logger'
|
||||||
|
import { Server } from 'socket.io'
|
||||||
|
import { TSocket } from '../utilities/types'
|
||||||
|
import ChatRepository from '../repositories/chatRepository'
|
||||||
|
|
||||||
|
class ChatService {
|
||||||
|
async sendZoneMessage(io: Server, socket: TSocket, message: string, characterId: number, zoneId: number): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const newChat = await prisma.chat.create({
|
||||||
|
data: {
|
||||||
|
characterId,
|
||||||
|
zoneId,
|
||||||
|
message
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const chat = await ChatRepository.getById(newChat.id)
|
||||||
|
if (!chat) return false
|
||||||
|
|
||||||
|
io.to(zoneId.toString()).emit('chat:message', chat)
|
||||||
|
return true
|
||||||
|
} catch (error: any) {
|
||||||
|
gameLogger.error(`Failed to save chat message: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChatService
|
31
src/services/passwordResetTokenService.ts
Normal file
31
src/services/passwordResetTokenService.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import prisma from '../utilities/prisma'
|
||||||
|
import passwordResetTokenRepository from '../repositories/passwordResetTokenRepository'
|
||||||
|
import { appLogger } from '../utilities/logger'
|
||||||
|
|
||||||
|
class PasswordResetTokenService {
|
||||||
|
/**
|
||||||
|
* Delete token
|
||||||
|
* @param token
|
||||||
|
*/
|
||||||
|
public async delete(token: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const tokenData = await passwordResetTokenRepository.getByToken(token)
|
||||||
|
if (!tokenData) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.passwordResetToken.delete({
|
||||||
|
where: {
|
||||||
|
token
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (error: any) {
|
||||||
|
appLogger.error(`Error deleting password reset token: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PasswordResetTokenService
|
@ -1,7 +1,12 @@
|
|||||||
import bcrypt from 'bcryptjs'
|
import bcrypt from 'bcryptjs'
|
||||||
import UserRepository from '../repositories/userRepository'
|
import UserRepository from '../repositories/userRepository'
|
||||||
|
import PasswordResetTokenRepository from '../repositories/passwordResetTokenRepository'
|
||||||
import prisma from '../utilities/prisma'
|
import prisma from '../utilities/prisma'
|
||||||
import { User } from '@prisma/client'
|
import { User } from '@prisma/client'
|
||||||
|
import config from '../utilities/config'
|
||||||
|
import NodeMailer from 'nodemailer'
|
||||||
|
import { httpLogger } from '../utilities/logger'
|
||||||
|
import PasswordResetTokenService from './passwordResetTokenService'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User service
|
* User service
|
||||||
@ -15,37 +20,145 @@ class UserService {
|
|||||||
* @param password
|
* @param password
|
||||||
*/
|
*/
|
||||||
async login(username: string, password: string): Promise<boolean | User> {
|
async login(username: string, password: string): Promise<boolean | User> {
|
||||||
const user = await UserRepository.getByUsername(username)
|
try {
|
||||||
if (!user) {
|
const user = await UserRepository.getByUsername(username)
|
||||||
|
if (!user) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordMatch = await bcrypt.compare(password, user.password)
|
||||||
|
if (!passwordMatch) {
|
||||||
|
httpLogger.error(`Failed to login user: ${username}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return user
|
||||||
|
} catch (error: any) {
|
||||||
|
httpLogger.error(`Error logging in user: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const passwordMatch = await bcrypt.compare(password, user.password)
|
|
||||||
if (!passwordMatch) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return user
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register user
|
* Register user
|
||||||
* @param username
|
* @param username
|
||||||
|
* @param email
|
||||||
* @param password
|
* @param password
|
||||||
*/
|
*/
|
||||||
async register(username: string, password: string): Promise<boolean | User> {
|
async register(username: string, email: string, password: string): Promise<boolean | User> {
|
||||||
const user = await UserRepository.getByUsername(username)
|
try {
|
||||||
if (user) {
|
const user = await UserRepository.getByUsername(username)
|
||||||
|
if (user) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const userByEmail = await UserRepository.getByEmail(email)
|
||||||
|
if (userByEmail) {
|
||||||
|
httpLogger.error(`User already exists: ${email}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashedPassword = await bcrypt.hash(password, 10)
|
||||||
|
return prisma.user.create({
|
||||||
|
data: {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password: hashedPassword
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
httpLogger.error(`Error registering user: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const hashedPassword = await bcrypt.hash(password, 10)
|
/**
|
||||||
return prisma.user.create({
|
* Reset password
|
||||||
data: {
|
* @param email
|
||||||
username,
|
*/
|
||||||
password: hashedPassword
|
async requestPasswordReset(email: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const user = await UserRepository.getByEmail(email)
|
||||||
|
if (!user) return false
|
||||||
|
|
||||||
|
const token = await bcrypt.hash(new Date().getTime().toString(), 10)
|
||||||
|
const latestToken = await PasswordResetTokenRepository.getByUserId(user.id)
|
||||||
|
|
||||||
|
// Check if password reset has been requested recently
|
||||||
|
if (latestToken) {
|
||||||
|
const tokenExpiryDate = new Date(Date.now() - 24 * 60 * 60 * 1000) // 24 hours
|
||||||
|
const isTokenExpired = latestToken.createdAt < tokenExpiryDate
|
||||||
|
|
||||||
|
if (!isTokenExpired) return false
|
||||||
|
|
||||||
|
await prisma.passwordResetToken.delete({
|
||||||
|
where: {
|
||||||
|
id: latestToken.id
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
await prisma.passwordResetToken.create({
|
||||||
|
data: {
|
||||||
|
userId: user.id,
|
||||||
|
token: token
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const transporter = NodeMailer.createTransport({
|
||||||
|
host: config.SMTP_HOST,
|
||||||
|
port: config.SMTP_PORT,
|
||||||
|
secure: false,
|
||||||
|
auth: {
|
||||||
|
user: config.SMTP_USER,
|
||||||
|
pass: config.SMTP_PASSWORD
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await transporter.sendMail({
|
||||||
|
from: config.SMTP_USER,
|
||||||
|
to: email,
|
||||||
|
subject: 'Reset your password',
|
||||||
|
text: 'A password reset has been requested, reset your password here: ' + config.CLIENT_URL + '#' + token, // Plain text body
|
||||||
|
html: "<p>A password reset has been requested, reset your password here: <a href='" + config.CLIENT_URL + '#' + token + "'>" + config.CLIENT_URL + '#' + token + '</a></p>' // Html body
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (error: any) {
|
||||||
|
httpLogger.error(`Error sending password reset email: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set new password
|
||||||
|
* @param urlToken
|
||||||
|
* @param password
|
||||||
|
*/
|
||||||
|
async resetPassword(urlToken: string, password: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const tokenData = await PasswordResetTokenRepository.getByToken(urlToken)
|
||||||
|
if (!tokenData) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashedPassword = await bcrypt.hash(password, 10)
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { id: tokenData.userId },
|
||||||
|
data: {
|
||||||
|
password: hashedPassword
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Delete the token
|
||||||
|
const passwordResetTokenService = new PasswordResetTokenService()
|
||||||
|
await passwordResetTokenService.delete(urlToken)
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (error: any) {
|
||||||
|
httpLogger.error(`Error setting new password: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
37
src/services/worldService.ts
Normal file
37
src/services/worldService.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import prisma from '../utilities/prisma'
|
||||||
|
import { gameLogger } from '../utilities/logger'
|
||||||
|
import { World } from '@prisma/client'
|
||||||
|
import WorldRepository from '../repositories/worldRepository'
|
||||||
|
|
||||||
|
class WorldService {
|
||||||
|
async update(worldData: Partial<World>): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const currentWorld = await WorldRepository.getFirst()
|
||||||
|
if (!currentWorld) {
|
||||||
|
// If no world exists, create first record
|
||||||
|
await prisma.world.create({
|
||||||
|
data: {
|
||||||
|
...worldData,
|
||||||
|
date: worldData.date || new Date()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update existing world using its date as unique identifier
|
||||||
|
await prisma.world.update({
|
||||||
|
where: {
|
||||||
|
date: currentWorld.date
|
||||||
|
},
|
||||||
|
data: worldData
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (error: any) {
|
||||||
|
gameLogger.error(`Failed to update world: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new WorldService()
|
@ -1,18 +1,21 @@
|
|||||||
import { ExtendedCharacter, TSocket } from '../utilities/types'
|
import { ExtendedCharacter, TSocket } from '../utilities/types'
|
||||||
import prisma from '../utilities/prisma'
|
import prisma from '../utilities/prisma'
|
||||||
import ZoneRepository from '../repositories/zoneRepository'
|
|
||||||
import { ZoneEventTileTeleport } from '@prisma/client'
|
import { ZoneEventTileTeleport } from '@prisma/client'
|
||||||
import { Server } from 'socket.io'
|
import { Server } from 'socket.io'
|
||||||
import CharacterManager from '../managers/characterManager'
|
import ZoneManager from '../managers/zoneManager'
|
||||||
|
import { gameLogger } from '../utilities/logger'
|
||||||
|
|
||||||
export class ZoneEventTileService {
|
export class ZoneEventTileService {
|
||||||
public async handleTeleport(io: Server, socket: TSocket, character: ExtendedCharacter, teleport: ZoneEventTileTeleport): Promise<void> {
|
public async handleTeleport(io: Server, socket: TSocket, character: ExtendedCharacter, teleport: ZoneEventTileTeleport): Promise<void> {
|
||||||
if (teleport.toZoneId === character.zoneId) return
|
if (teleport.toZoneId === character.zoneId) return
|
||||||
|
|
||||||
const zone = await ZoneRepository.getById(teleport.toZoneId)
|
const loadedZone = ZoneManager.getZoneById(teleport.toZoneId)
|
||||||
if (!zone) return
|
if (!loadedZone) {
|
||||||
|
gameLogger.error('zone:character:join error', 'Loaded zone not found')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// CharacterManager.moveCharacterBetweenZones(character, zone)
|
const zone = loadedZone.getZone()
|
||||||
|
|
||||||
const oldZoneId = character.zoneId
|
const oldZoneId = character.zoneId
|
||||||
const newZoneId = teleport.toZoneId
|
const newZoneId = teleport.toZoneId
|
||||||
@ -23,14 +26,21 @@ 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
|
||||||
|
|
||||||
|
// Remove and add character to new zone
|
||||||
|
await loadedZone.removeCharacter(character.id)
|
||||||
|
loadedZone.addCharacter(character)
|
||||||
|
|
||||||
// Emit events
|
// Emit events
|
||||||
io.to(oldZoneId.toString()).emit('zone:character:leave', character.id)
|
io.to(oldZoneId.toString()).emit('zone:character:leave', character.id)
|
||||||
@ -43,7 +53,7 @@ export class ZoneEventTileService {
|
|||||||
// Send teleport information to the client
|
// Send teleport information to the client
|
||||||
socket.emit('zone:character:teleport', {
|
socket.emit('zone:character:teleport', {
|
||||||
zone,
|
zone,
|
||||||
characters: CharacterManager.getCharactersInZone(zone)
|
characters: loadedZone.getCharactersInZone()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,31 +1,38 @@
|
|||||||
import prisma from '../utilities/prisma'
|
import prisma from '../utilities/prisma'
|
||||||
|
import { gameLogger } from '../utilities/logger'
|
||||||
|
|
||||||
class ZoneService {
|
class ZoneService {
|
||||||
async createDemoZone(): Promise<boolean> {
|
async createDemoZone(): Promise<boolean> {
|
||||||
const tiles = [
|
try {
|
||||||
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
const tiles = [
|
||||||
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
||||||
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
||||||
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
||||||
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
||||||
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
||||||
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
||||||
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
||||||
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
||||||
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile']
|
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'],
|
||||||
]
|
['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile']
|
||||||
|
]
|
||||||
|
|
||||||
await prisma.zone.create({
|
await prisma.zone.create({
|
||||||
data: {
|
data: {
|
||||||
name: 'Demo zone',
|
name: 'Demo zone',
|
||||||
width: 10,
|
width: 10,
|
||||||
height: 10,
|
height: 10,
|
||||||
tiles
|
tiles
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('Demo zone created.')
|
gameLogger.info('Demo zone created.')
|
||||||
return true
|
|
||||||
|
return true
|
||||||
|
} catch (error: any) {
|
||||||
|
gameLogger.error(`Failed to create demo zone: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
import { Server } from 'socket.io'
|
||||||
|
import { CharacterHair } from '@prisma/client'
|
||||||
|
import { TSocket } from '../../../utilities/types'
|
||||||
|
import characterHairRepository from '../../../repositories/characterHairRepository'
|
||||||
|
|
||||||
|
interface IPayload {}
|
||||||
|
|
||||||
|
export default class characterHairListEvent {
|
||||||
|
constructor(
|
||||||
|
private readonly io: Server,
|
||||||
|
private readonly socket: TSocket
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public listen(): void {
|
||||||
|
this.socket.on('character:hair:list', this.handleEvent.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleEvent(data: IPayload, callback: (response: CharacterHair[]) => void): Promise<void> {
|
||||||
|
const items = await characterHairRepository.getIsEnabledForCharCreationHair()
|
||||||
|
callback(items)
|
||||||
|
}
|
||||||
|
}
|
@ -1,24 +1,67 @@
|
|||||||
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'
|
import { gameLogger } from '../../utilities/logger'
|
||||||
|
import ZoneManager from '../../managers/zoneManager'
|
||||||
|
import { CharacterService } from '../../services/characterService'
|
||||||
|
|
||||||
type SocketResponseT = {
|
interface CharacterConnectPayload {
|
||||||
character_id: number
|
characterId: number
|
||||||
|
characterHairId?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function (io: Server, socket: TSocket) {
|
export default class CharacterConnectEvent {
|
||||||
socket.on('character:connect', async (data: SocketResponseT) => {
|
constructor(
|
||||||
console.log('character:connect requested', data)
|
private readonly io: Server,
|
||||||
try {
|
private readonly socket: TSocket
|
||||||
const character = await CharacterRepository.getByUserAndId(socket?.user?.id as number, data.character_id)
|
) {}
|
||||||
if (!character) return
|
|
||||||
socket.characterId = character.id
|
|
||||||
|
|
||||||
CharacterManager.initCharacter(character as ExtendedCharacter)
|
public listen(): void {
|
||||||
socket.emit('character:connect', character)
|
this.socket.on('character:connect', this.handleCharacterConnect.bind(this))
|
||||||
} catch (error: any) {
|
}
|
||||||
console.log('character:connect error', error)
|
|
||||||
|
private async handleCharacterConnect({ characterId, characterHairId }: CharacterConnectPayload): Promise<void> {
|
||||||
|
if (!this.socket.userId) {
|
||||||
|
this.emitError('User not authenticated')
|
||||||
|
return
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
try {
|
||||||
|
if (await this.hasActiveCharacter()) {
|
||||||
|
this.emitError('You are already connected to another character')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update hair
|
||||||
|
const characterService = new CharacterService()
|
||||||
|
await characterService.updateHair(characterId, characterHairId ?? null)
|
||||||
|
|
||||||
|
const character = await CharacterRepository.getByUserAndId(this.socket.userId!, characterId)
|
||||||
|
if (!character) {
|
||||||
|
this.emitError('Character not found or does not belong to this user')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.socket.characterId = character.id
|
||||||
|
this.socket.emit('character:connect', character)
|
||||||
|
} catch (error) {
|
||||||
|
this.handleError('Failed to connect character', error) // @TODO : Make global error handler
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async hasActiveCharacter(): Promise<boolean> {
|
||||||
|
const characters = await CharacterRepository.getByUserId(this.socket.userId!)
|
||||||
|
return characters?.some((char) => ZoneManager.getCharacter(char.id)) ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitError(message: string): void {
|
||||||
|
this.socket.emit('notification', { title: 'Server message', message })
|
||||||
|
gameLogger.error('character:connect error', `Player ${this.socket.userId}: ${message}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleError(context: string, error: unknown): void {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
|
this.emitError(`${context}: ${errorMessage}`)
|
||||||
|
gameLogger.error('character:connect error', errorMessage)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,50 +2,57 @@ import { Server } from 'socket.io'
|
|||||||
import { TSocket } from '../../utilities/types'
|
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'
|
||||||
|
import { CharacterService } from '../../services/characterService'
|
||||||
import { ZCharacterCreate } from '../../utilities/zodTypes'
|
import { ZCharacterCreate } from '../../utilities/zodTypes'
|
||||||
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.userId!
|
||||||
|
|
||||||
// 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 characterService = new CharacterService()
|
||||||
data: {
|
const character: Character = await characterService.create(data.name, user_id)
|
||||||
name: data.name,
|
|
||||||
userId: user_id
|
|
||||||
// characterTypeId: 1 // @TODO set to chosen character type
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
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).' })
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,10 @@ import { Server } from 'socket.io'
|
|||||||
import { TSocket } from '../../utilities/types'
|
import { TSocket } from '../../utilities/types'
|
||||||
import { Character, Zone } from '@prisma/client'
|
import { Character, Zone } from '@prisma/client'
|
||||||
import CharacterRepository from '../../repositories/characterRepository'
|
import CharacterRepository from '../../repositories/characterRepository'
|
||||||
|
import { CharacterService } from '../../services/characterService'
|
||||||
|
|
||||||
type TypePayload = {
|
type TypePayload = {
|
||||||
character_id: number
|
characterId: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type TypeResponse = {
|
type TypeResponse = {
|
||||||
@ -12,19 +13,26 @@ 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(
|
||||||
// zod validate
|
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> {
|
||||||
try {
|
try {
|
||||||
await CharacterRepository.deleteByUserIdAndId(socket.user?.id as number, data.character_id as number)
|
const characterService = new CharacterService()
|
||||||
|
await characterService.deleteByUserIdAndId(this.socket.userId!, data.characterId!)
|
||||||
|
|
||||||
const user_id = socket.user?.id as number
|
const characters: Character[] = (await CharacterRepository.getByUserId(this.socket.userId!)) 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)
|
return this.socket.emit('notification', { message: 'Character delete failed. Please try again.' })
|
||||||
return socket.emit('notification', { message: 'Character delete failed. Please try again.' })
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,16 +2,24 @@ import { Socket, Server } from 'socket.io'
|
|||||||
import { TSocket } from '../../utilities/types'
|
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'
|
||||||
|
import { gameLogger } from '../../utilities/logger'
|
||||||
|
|
||||||
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')
|
const characters: Character[] = (await CharacterRepository.getByUserId(this.socket.userId!)) as Character[]
|
||||||
const user_id = socket.user?.id as number
|
this.socket.emit('character:list', characters)
|
||||||
const characters: Character[] = (await CharacterRepository.getByUserId(user_id)) as Character[]
|
|
||||||
socket.emit('character:list', characters)
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.log('character:list error', error)
|
gameLogger.error('character:list error', error.message)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ export default class AlertCommandEvent {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
public listen(): void {
|
public listen(): void {
|
||||||
this.socket.on('chat:send_message', this.handleAlertCommand.bind(this))
|
this.socket.on('chat:message', this.handleAlertCommand.bind(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleAlertCommand(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
|
private async handleAlertCommand(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
|
||||||
@ -24,25 +24,30 @@ export default class AlertCommandEvent {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if character exists
|
||||||
|
const character = await CharacterRepository.getByUserAndId(this.socket.userId!, this.socket.characterId!)
|
||||||
|
if (!character) {
|
||||||
|
gameLogger.error('chat:alert_command error', 'Character not found')
|
||||||
|
return callback(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the user is the GM
|
||||||
|
if (character.role !== 'gm') {
|
||||||
|
gameLogger.info(`User ${character.id} tried to set time but is not a game master.`)
|
||||||
|
return callback(false)
|
||||||
|
}
|
||||||
|
|
||||||
const args = getArgs('alert', data.message)
|
const args = getArgs('alert', data.message)
|
||||||
|
|
||||||
if (!args) {
|
if (!args) {
|
||||||
callback(false)
|
return callback(false)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const character = await CharacterRepository.getByUserAndId(this.socket.user?.id as number, this.socket.characterId as number)
|
|
||||||
if (!character) {
|
|
||||||
gameLogger.error('chat:alert_command error', 'Character not found')
|
|
||||||
callback(false)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.io.emit('notification', { title: 'Message from GM', message: args.join(' ') })
|
this.io.emit('notification', { title: 'Message from GM', message: args.join(' ') })
|
||||||
callback(true)
|
return callback(true)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
gameLogger.error('chat:alert_command error', error.message)
|
gameLogger.error('chat:alert_command error', error.message)
|
||||||
callback(false)
|
callback(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
60
src/socketEvents/chat/gameMaster/setTimeCommand.ts
Normal file
60
src/socketEvents/chat/gameMaster/setTimeCommand.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { Server } from 'socket.io'
|
||||||
|
import { TSocket } from '../../../utilities/types'
|
||||||
|
import { getArgs, isCommand } from '../../../utilities/chat'
|
||||||
|
import CharacterRepository from '../../../repositories/characterRepository'
|
||||||
|
import { gameLogger } from '../../../utilities/logger'
|
||||||
|
import DateManager from '../../../managers/dateManager'
|
||||||
|
|
||||||
|
type TypePayload = {
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class SetTimeCommand {
|
||||||
|
constructor(
|
||||||
|
private readonly io: Server,
|
||||||
|
private readonly socket: TSocket
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public listen(): void {
|
||||||
|
this.socket.on('chat:message', this.handleAlertCommand.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleAlertCommand(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (!isCommand(data.message, 'time')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if character exists
|
||||||
|
const character = await CharacterRepository.getByUserAndId(this.socket.userId!, this.socket.characterId!)
|
||||||
|
if (!character) {
|
||||||
|
gameLogger.error('chat:alert_command error', 'Character not found')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the user is the GM
|
||||||
|
if (character.role !== 'gm') {
|
||||||
|
gameLogger.info(`User ${character.id} tried to set time but is not a game master.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get arguments
|
||||||
|
const args = getArgs('time', data.message)
|
||||||
|
|
||||||
|
if (!args) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const time = args[0] // 24h time, e.g. 17:34
|
||||||
|
|
||||||
|
if (!time) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await DateManager.setTime(time)
|
||||||
|
} catch (error: any) {
|
||||||
|
gameLogger.error('command error', error.message)
|
||||||
|
callback(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,8 +2,10 @@ import { Server } from 'socket.io'
|
|||||||
import { TSocket } from '../../../utilities/types'
|
import { TSocket } from '../../../utilities/types'
|
||||||
import { getArgs, isCommand } from '../../../utilities/chat'
|
import { getArgs, isCommand } from '../../../utilities/chat'
|
||||||
import ZoneRepository from '../../../repositories/zoneRepository'
|
import ZoneRepository from '../../../repositories/zoneRepository'
|
||||||
import CharacterManager from '../../../managers/characterManager'
|
import { gameLogger, gameMasterLogger } from '../../../utilities/logger'
|
||||||
import { gameMasterLogger } from '../../../utilities/logger'
|
import ZoneManager from '../../../managers/zoneManager'
|
||||||
|
import ZoneCharacter from '../../../models/zoneCharacter'
|
||||||
|
import zoneManager from '../../../managers/zoneManager'
|
||||||
|
|
||||||
type TypePayload = {
|
type TypePayload = {
|
||||||
message: string
|
message: string
|
||||||
@ -16,14 +18,23 @@ export default class TeleportCommandEvent {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
public listen(): void {
|
public listen(): void {
|
||||||
this.socket.on('chat:send_message', this.handleTeleportCommand.bind(this))
|
this.socket.on('chat:message', this.handleTeleportCommand.bind(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleTeleportCommand(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
|
private async handleTeleportCommand(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const character = CharacterManager.getCharacterFromSocket(this.socket)
|
// Check if character exists
|
||||||
if (!character) {
|
const zoneCharacter = ZoneManager.getCharacter(this.socket.characterId!)
|
||||||
this.socket.emit('notification', { title: 'Server message', message: 'Character not found' })
|
if (!zoneCharacter) {
|
||||||
|
gameLogger.error('chat:message error', 'Character not found')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const character = zoneCharacter.character
|
||||||
|
|
||||||
|
// Check if the user is the GM
|
||||||
|
if (character.role !== 'gm') {
|
||||||
|
gameLogger.info(`User ${character.id} tried to set time but is not a game master.`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,10 +65,12 @@ export default class TeleportCommandEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove character from current zone
|
// Remove character from current zone
|
||||||
|
zoneManager.removeCharacter(character.id)
|
||||||
this.io.to(character.zoneId.toString()).emit('zone:character:leave', character.id)
|
this.io.to(character.zoneId.toString()).emit('zone:character:leave', character.id)
|
||||||
this.socket.leave(character.zoneId.toString())
|
this.socket.leave(character.zoneId.toString())
|
||||||
|
|
||||||
// Add character to new zone
|
// Add character to new zone
|
||||||
|
zoneManager.getZoneById(zone.id)?.addCharacter(character)
|
||||||
this.io.to(zone.id.toString()).emit('zone:character:join', character)
|
this.io.to(zone.id.toString()).emit('zone:character:join', character)
|
||||||
this.socket.join(zone.id.toString())
|
this.socket.join(zone.id.toString())
|
||||||
|
|
||||||
@ -65,17 +78,15 @@ export default class TeleportCommandEvent {
|
|||||||
character.positionX = 0
|
character.positionX = 0
|
||||||
character.positionY = 0
|
character.positionY = 0
|
||||||
|
|
||||||
character.resetMovement = true
|
zoneCharacter.isMoving = false
|
||||||
|
|
||||||
this.socket.emit('zone:character:teleport', {
|
this.socket.emit('zone:character:teleport', {
|
||||||
zone,
|
zone,
|
||||||
characters: CharacterManager.getCharactersInZone(zone)
|
characters: ZoneManager.getZoneById(zone.id)?.getCharactersInZone()
|
||||||
})
|
})
|
||||||
|
|
||||||
this.socket.emit('notification', { title: 'Server message', message: `You have been teleported to ${zone.name}` })
|
this.socket.emit('notification', { title: 'Server message', message: `You have been teleported to ${zone.name}` })
|
||||||
gameMasterLogger.info('teleport', `Character ${character.id} teleported to zone ${zone.id}`)
|
gameMasterLogger.info('teleport', `Character ${character.id} teleported to zone ${zone.id}`)
|
||||||
|
|
||||||
callback(true)
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
gameMasterLogger.error(`Error in teleport command: ${error.message}`)
|
gameMasterLogger.error(`Error in teleport command: ${error.message}`)
|
||||||
this.socket.emit('notification', { title: 'Server message', message: 'An error occurred while teleporting' })
|
this.socket.emit('notification', { title: 'Server message', message: 'An error occurred while teleporting' })
|
||||||
|
47
src/socketEvents/chat/gameMaster/toggleFogCommand.ts
Normal file
47
src/socketEvents/chat/gameMaster/toggleFogCommand.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { Server } from 'socket.io'
|
||||||
|
import { TSocket } from '../../../utilities/types'
|
||||||
|
import { isCommand } from '../../../utilities/chat'
|
||||||
|
import CharacterRepository from '../../../repositories/characterRepository'
|
||||||
|
import { gameLogger } from '../../../utilities/logger'
|
||||||
|
import WeatherManager from '../../../managers/weatherManager'
|
||||||
|
|
||||||
|
type TypePayload = {
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ToggleFogCommand {
|
||||||
|
constructor(
|
||||||
|
private readonly io: Server,
|
||||||
|
private readonly socket: TSocket
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public listen(): void {
|
||||||
|
this.socket.on('chat:message', this.handleAlertCommand.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleAlertCommand(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (!isCommand(data.message, 'fog')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if character exists
|
||||||
|
const character = await CharacterRepository.getByUserAndId(this.socket.userId!, this.socket.characterId!)
|
||||||
|
if (!character) {
|
||||||
|
gameLogger.error('chat:alert_command error', 'Character not found')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the user is the GM
|
||||||
|
if (character.role !== 'gm') {
|
||||||
|
gameLogger.info(`User ${character.id} tried to set time but is not a game master.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await WeatherManager.toggleFog()
|
||||||
|
} catch (error: any) {
|
||||||
|
gameLogger.error('command error', error.message)
|
||||||
|
callback(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
47
src/socketEvents/chat/gameMaster/toggleRainCommand.ts
Normal file
47
src/socketEvents/chat/gameMaster/toggleRainCommand.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { Server } from 'socket.io'
|
||||||
|
import { TSocket } from '../../../utilities/types'
|
||||||
|
import { isCommand } from '../../../utilities/chat'
|
||||||
|
import CharacterRepository from '../../../repositories/characterRepository'
|
||||||
|
import { gameLogger } from '../../../utilities/logger'
|
||||||
|
import WeatherManager from '../../../managers/weatherManager'
|
||||||
|
|
||||||
|
type TypePayload = {
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ToggleRainCommand {
|
||||||
|
constructor(
|
||||||
|
private readonly io: Server,
|
||||||
|
private readonly socket: TSocket
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public listen(): void {
|
||||||
|
this.socket.on('chat:message', this.handleAlertCommand.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleAlertCommand(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (!isCommand(data.message, 'rain')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if character exists
|
||||||
|
const character = await CharacterRepository.getByUserAndId(this.socket.userId!, this.socket.characterId!)
|
||||||
|
if (!character) {
|
||||||
|
gameLogger.error('chat:alert_command error', 'Character not found')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the user is the GM
|
||||||
|
if (character.role !== 'gm') {
|
||||||
|
gameLogger.info(`User ${character.id} tried to set time but is not a game master.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await WeatherManager.toggleRain()
|
||||||
|
} catch (error: any) {
|
||||||
|
gameLogger.error('command error', error.message)
|
||||||
|
callback(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
src/socketEvents/chat/message.ts
Normal file
54
src/socketEvents/chat/message.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { Server } from 'socket.io'
|
||||||
|
import { TSocket } from '../../utilities/types'
|
||||||
|
import ZoneRepository from '../../repositories/zoneRepository'
|
||||||
|
import { isCommand } from '../../utilities/chat'
|
||||||
|
import { gameLogger } from '../../utilities/logger'
|
||||||
|
import ZoneManager from '../../managers/zoneManager'
|
||||||
|
import ChatService from '../../services/chatService'
|
||||||
|
|
||||||
|
type TypePayload = {
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ChatMessageEvent {
|
||||||
|
constructor(
|
||||||
|
private readonly io: Server,
|
||||||
|
private readonly socket: TSocket
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public listen(): void {
|
||||||
|
this.socket.on('chat:message', this.handleEvent.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (!data.message || isCommand(data.message)) {
|
||||||
|
return callback(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const zoneCharacter = ZoneManager.getCharacter(this.socket.characterId!)
|
||||||
|
if (!zoneCharacter) {
|
||||||
|
gameLogger.error('chat:message error', 'Character not found')
|
||||||
|
return callback(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const character = zoneCharacter.character
|
||||||
|
|
||||||
|
const zone = await ZoneRepository.getById(character.zoneId)
|
||||||
|
if (!zone) {
|
||||||
|
gameLogger.error('chat:message error', 'Zone not found')
|
||||||
|
return callback(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const chatService = new ChatService()
|
||||||
|
if (await chatService.sendZoneMessage(this.io, this.socket, data.message, character.id, zone.id)) {
|
||||||
|
return callback(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(false)
|
||||||
|
} catch (error: any) {
|
||||||
|
gameLogger.error('chat:message error', error.message)
|
||||||
|
callback(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,54 +0,0 @@
|
|||||||
import { Server } from 'socket.io'
|
|
||||||
import { TSocket } from '../../utilities/types'
|
|
||||||
import CharacterRepository from '../../repositories/characterRepository'
|
|
||||||
import ZoneRepository from '../../repositories/zoneRepository'
|
|
||||||
import { isCommand } from '../../utilities/chat'
|
|
||||||
import { gameLogger } from '../../utilities/logger'
|
|
||||||
|
|
||||||
type TypePayload = {
|
|
||||||
message: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class ChatMessageEvent {
|
|
||||||
constructor(
|
|
||||||
private readonly io: Server,
|
|
||||||
private readonly socket: TSocket
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public listen(): void {
|
|
||||||
this.socket.on('chat:send_message', this.handleChatMessage.bind(this))
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handleChatMessage(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
|
|
||||||
try {
|
|
||||||
if (!data.message || isCommand(data.message)) {
|
|
||||||
callback(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const character = await CharacterRepository.getByUserAndId(this.socket.user?.id as number, this.socket.characterId as number)
|
|
||||||
if (!character) {
|
|
||||||
gameLogger.error('chat:send_message error', 'Character not found')
|
|
||||||
callback(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const zone = await ZoneRepository.getById(character.zoneId)
|
|
||||||
if (!zone) {
|
|
||||||
gameLogger.error('chat:send_message error', 'Zone not found')
|
|
||||||
callback(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(true)
|
|
||||||
|
|
||||||
this.io.to(zone.id.toString()).emit('chat:message', {
|
|
||||||
character: character,
|
|
||||||
message: data.message
|
|
||||||
})
|
|
||||||
} catch (error: any) {
|
|
||||||
gameLogger.error('chat:send_message error', error.message)
|
|
||||||
callback(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +1,7 @@
|
|||||||
import { Server } from 'socket.io'
|
import { Server } from 'socket.io'
|
||||||
import { TSocket } from '../utilities/types'
|
import { TSocket } from '../utilities/types'
|
||||||
import CharacterManager from '../managers/characterManager'
|
|
||||||
import { gameLogger } from '../utilities/logger'
|
import { gameLogger } from '../utilities/logger'
|
||||||
|
import ZoneManager from '../managers/zoneManager'
|
||||||
|
|
||||||
export default class DisconnectEvent {
|
export default class DisconnectEvent {
|
||||||
constructor(
|
constructor(
|
||||||
@ -10,33 +10,38 @@ export default class DisconnectEvent {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
public listen(): void {
|
public listen(): void {
|
||||||
this.socket.on('disconnect', this.handleDisconnect.bind(this))
|
this.socket.on('disconnect', this.handleEvent.bind(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleDisconnect(data: any): Promise<void> {
|
private async handleEvent(data: any): Promise<void> {
|
||||||
try {
|
try {
|
||||||
if (!this.socket.user) {
|
if (!this.socket.userId) {
|
||||||
gameLogger.info('User disconnected but had no user set')
|
gameLogger.info('User disconnected but had no user set')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.io.emit('user:disconnect', this.socket.user.id)
|
this.io.emit('user:disconnect', this.socket.userId)
|
||||||
|
|
||||||
const character = CharacterManager.getCharacterFromSocket(this.socket)
|
const zoneCharacter = ZoneManager.getCharacter(this.socket.characterId!)
|
||||||
|
if (!zoneCharacter) {
|
||||||
if (!character) {
|
|
||||||
gameLogger.info('User disconnected but had no character set')
|
gameLogger.info('User disconnected but had no character set')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const character = zoneCharacter.character
|
||||||
|
|
||||||
|
// Save character position and remove from zone
|
||||||
|
zoneCharacter.isMoving = false
|
||||||
|
await zoneCharacter.savePosition()
|
||||||
|
ZoneManager.removeCharacter(this.socket.characterId!)
|
||||||
|
|
||||||
gameLogger.info('User disconnected along with their character')
|
gameLogger.info('User disconnected along with their character')
|
||||||
|
|
||||||
await CharacterManager.removeCharacter(character)
|
// Inform other clients that the character has left
|
||||||
|
|
||||||
this.io.in(character.zoneId.toString()).emit('zone:character:leave', character.id)
|
this.io.in(character.zoneId.toString()).emit('zone:character:leave', character.id)
|
||||||
this.io.emit('character:disconnect', character.id)
|
this.io.emit('character:disconnect', character.id)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
gameLogger.error('disconnect error', error.message)
|
gameLogger.error('disconnect error', error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
import { Server } from 'socket.io'
|
||||||
|
import { TSocket } from '../../../../utilities/types'
|
||||||
|
import prisma from '../../../../utilities/prisma'
|
||||||
|
import characterRepository from '../../../../repositories/characterRepository'
|
||||||
|
|
||||||
|
export default class CharacterHairCreateEvent {
|
||||||
|
constructor(
|
||||||
|
private readonly io: Server,
|
||||||
|
private readonly socket: TSocket
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public listen(): void {
|
||||||
|
this.socket.on('gm:characterHair:create', this.handleEvent.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleEvent(data: undefined, callback: (response: boolean, characterType?: any) => void): Promise<void> {
|
||||||
|
try {
|
||||||
|
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||||
|
if (!character) return callback(false)
|
||||||
|
|
||||||
|
if (character.role !== 'gm') {
|
||||||
|
return callback(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const newCharacterHair = await prisma.characterHair.create({
|
||||||
|
data: {
|
||||||
|
name: 'New hair'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
callback(true, newCharacterHair)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating character hair:', error)
|
||||||
|
callback(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
import { Server } from 'socket.io'
|
||||||
|
import { TSocket } from '../../../../utilities/types'
|
||||||
|
import prisma from '../../../../utilities/prisma'
|
||||||
|
import characterRepository from '../../../../repositories/characterRepository'
|
||||||
|
import { gameMasterLogger } from '../../../../utilities/logger'
|
||||||
|
|
||||||
|
interface IPayload {
|
||||||
|
id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class characterHairDeleteEvent {
|
||||||
|
constructor(
|
||||||
|
private readonly io: Server,
|
||||||
|
private readonly socket: TSocket
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public listen(): void {
|
||||||
|
this.socket.on('gm:characterHair:remove', this.handleEvent.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleEvent(data: IPayload, callback: (response: boolean) => void): Promise<void> {
|
||||||
|
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||||
|
if (!character) return callback(false)
|
||||||
|
|
||||||
|
if (character.role !== 'gm') {
|
||||||
|
return callback(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await prisma.characterHair.delete({
|
||||||
|
where: { id: data.id }
|
||||||
|
})
|
||||||
|
|
||||||
|
callback(true)
|
||||||
|
} catch (error) {
|
||||||
|
gameMasterLogger.error(`Error deleting character type ${data.id}: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
callback(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
import { Server } from 'socket.io'
|
||||||
|
import { TSocket } from '../../../../utilities/types'
|
||||||
|
import { CharacterHair } from '@prisma/client'
|
||||||
|
import characterRepository from '../../../../repositories/characterRepository'
|
||||||
|
import { gameMasterLogger } from '../../../../utilities/logger'
|
||||||
|
import characterHairRepository from '../../../../repositories/characterHairRepository'
|
||||||
|
|
||||||
|
interface IPayload {}
|
||||||
|
|
||||||
|
export default class characterHairListEvent {
|
||||||
|
constructor(
|
||||||
|
private readonly io: Server,
|
||||||
|
private readonly socket: TSocket
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public listen(): void {
|
||||||
|
this.socket.on('gm:characterHair:list', this.handleEvent.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleEvent(data: IPayload, callback: (response: CharacterHair[]) => void): Promise<void> {
|
||||||
|
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||||
|
if (!character) {
|
||||||
|
gameMasterLogger.error('gm:characterHair:list error', 'Character not found')
|
||||||
|
return callback([])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (character.role !== 'gm') {
|
||||||
|
gameMasterLogger.info(`User ${character.id} tried to list character hair but is not a game master.`)
|
||||||
|
return callback([])
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all objects
|
||||||
|
const items = await characterHairRepository.getAll()
|
||||||
|
callback(items)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
import { Server } from 'socket.io'
|
||||||
|
import { TSocket } from '../../../../utilities/types'
|
||||||
|
import prisma from '../../../../utilities/prisma'
|
||||||
|
import characterRepository from '../../../../repositories/characterRepository'
|
||||||
|
import { CharacterGender, CharacterRace } from '@prisma/client'
|
||||||
|
import { gameMasterLogger } from '../../../../utilities/logger'
|
||||||
|
|
||||||
|
type Payload = {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
gender: CharacterGender
|
||||||
|
isEnabledForCharCreation: boolean
|
||||||
|
spriteId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class CharacterHairUpdateEvent {
|
||||||
|
constructor(
|
||||||
|
private readonly io: Server,
|
||||||
|
private readonly socket: TSocket
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public listen(): void {
|
||||||
|
this.socket.on('gm:characterHair:update', this.handleObjectUpdate.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleObjectUpdate(data: Payload, callback: (success: boolean) => void): Promise<void> {
|
||||||
|
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||||
|
if (!character) return callback(false)
|
||||||
|
|
||||||
|
if (character.role !== 'gm') {
|
||||||
|
return callback(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await prisma.characterHair.update({
|
||||||
|
where: { id: data.id },
|
||||||
|
data: {
|
||||||
|
name: data.name,
|
||||||
|
gender: data.gender,
|
||||||
|
isEnabledForCharCreation: data.isEnabledForCharCreation,
|
||||||
|
spriteId: data.spriteId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return callback(true)
|
||||||
|
} catch (error) {
|
||||||
|
gameMasterLogger.error(`Error updating character hair: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
return callback(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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.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 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
import { Server } from 'socket.io'
|
||||||
|
import { TSocket } from '../../../../utilities/types'
|
||||||
|
import prisma from '../../../../utilities/prisma'
|
||||||
|
import characterRepository from '../../../../repositories/characterRepository'
|
||||||
|
import { gameMasterLogger } from '../../../../utilities/logger'
|
||||||
|
|
||||||
|
interface IPayload {
|
||||||
|
id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class CharacterTypeDeleteEvent {
|
||||||
|
constructor(
|
||||||
|
private readonly io: Server,
|
||||||
|
private readonly socket: TSocket
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public listen(): void {
|
||||||
|
this.socket.on('gm:characterType:remove', this.handleEvent.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleEvent(data: IPayload, callback: (response: boolean) => void): Promise<void> {
|
||||||
|
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||||
|
if (!character) return callback(false)
|
||||||
|
|
||||||
|
if (character.role !== 'gm') {
|
||||||
|
return callback(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await prisma.characterType.delete({
|
||||||
|
where: { id: data.id }
|
||||||
|
})
|
||||||
|
|
||||||
|
callback(true)
|
||||||
|
} catch (error) {
|
||||||
|
gameMasterLogger.error(`Error deleting character type ${data.id}: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
callback(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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.handleEvent.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleEvent(data: IPayload, callback: (response: CharacterType[]) => void): Promise<void> {
|
||||||
|
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||||
|
if (!character) {
|
||||||
|
gameMasterLogger.error('gm:characterType:list error', 'Character not found')
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
import { Server } from 'socket.io'
|
||||||
|
import { TSocket } from '../../../../utilities/types'
|
||||||
|
import prisma from '../../../../utilities/prisma'
|
||||||
|
import characterRepository from '../../../../repositories/characterRepository'
|
||||||
|
import { CharacterGender, CharacterRace } from '@prisma/client'
|
||||||
|
|
||||||
|
type Payload = {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
gender: CharacterGender
|
||||||
|
race: CharacterRace
|
||||||
|
isEnabledForCharCreation: boolean
|
||||||
|
spriteId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class CharacterTypeUpdateEvent {
|
||||||
|
constructor(
|
||||||
|
private readonly io: Server,
|
||||||
|
private readonly socket: TSocket
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public listen(): void {
|
||||||
|
this.socket.on('gm:characterType: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.characterType.update({
|
||||||
|
where: { id: data.id },
|
||||||
|
data: {
|
||||||
|
name: data.name,
|
||||||
|
gender: data.gender,
|
||||||
|
race: data.race,
|
||||||
|
isEnabledForCharCreation: data.isEnabledForCharCreation,
|
||||||
|
spriteId: data.spriteId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
callback(true)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
callback(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,27 @@
|
|||||||
|
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'
|
||||||
|
import { gameLogger, gameMasterLogger } from '../../../../utilities/logger'
|
||||||
|
|
||||||
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,22 +36,22 @@ 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)
|
gameMasterLogger.error(`Error deleting object ${data.object}: ${err.message}`)
|
||||||
callback(false)
|
callback(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(true)
|
callback(true)
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
console.log(e)
|
gameLogger.error(`Error deleting object ${data.object}: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
callback(false)
|
callback(false)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
@ -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}`)
|
||||||
|
@ -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!)
|
||||||
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)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 { gameMasterLogger } from '../../../../utilities/logger'
|
import { gameMasterLogger } from '../../../../utilities/logger'
|
||||||
|
import { getPublicPath } from '../../../../utilities/storage'
|
||||||
|
import CharacterRepository from '../../../../repositories/characterRepository'
|
||||||
|
|
||||||
type Payload = {
|
type Payload = {
|
||||||
id: string
|
id: string
|
||||||
@ -17,15 +17,15 @@ 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 {
|
||||||
this.socket.on('gm:sprite:delete', this.handleSpriteDelete.bind(this))
|
this.socket.on('gm:sprite:delete', this.handleEvent.bind(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleSpriteDelete(data: Payload, callback: (response: boolean) => void): Promise<void> {
|
private async handleEvent(data: Payload, callback: (response: boolean) => void): Promise<void> {
|
||||||
const character = CharacterManager.getCharacterFromSocket(this.socket)
|
const character = await CharacterRepository.getById(this.socket.characterId!)
|
||||||
if (character?.role !== 'gm') {
|
if (character?.role !== 'gm') {
|
||||||
return callback(false)
|
return callback(false)
|
||||||
}
|
}
|
||||||
@ -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 })
|
||||||
|
@ -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!)
|
||||||
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)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,11 @@ 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 { getPublicPath } from '../../../../utilities/storage'
|
||||||
|
import CharacterRepository from '../../../../repositories/characterRepository'
|
||||||
|
import { gameMasterLogger } from '../../../../utilities/logger'
|
||||||
|
|
||||||
type SpriteActionInput = Omit<SpriteAction, 'id' | 'spriteId' | 'frameWidth' | 'frameHeight'> & {
|
type SpriteActionInput = Omit<SpriteAction, 'id' | 'spriteId' | 'frameWidth' | 'frameHeight'> & {
|
||||||
sprites: string[]
|
sprites: string[]
|
||||||
@ -27,9 +28,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 = await CharacterRepository.getById(this.socket.characterId!)
|
||||||
if (character?.role !== 'gm') {
|
if (character?.role !== 'gm') {
|
||||||
return callback(false)
|
return callback(false)
|
||||||
}
|
}
|
||||||
@ -43,104 +53,176 @@ export default function (io: Server, socket: TSocket) {
|
|||||||
|
|
||||||
callback(true)
|
callback(true)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating sprite:', error)
|
gameMasterLogger.error(`Error updating sprite ${data.id}: ${error instanceof Error ? error.message : String(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')
|
gameMasterLogger.error('Error parsing spriteActions: spriteActions is not an array')
|
||||||
}
|
}
|
||||||
return parsed
|
return parsed
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error parsing spriteActions:', error)
|
gameMasterLogger.error(`Error parsing spriteActions: ${error instanceof Error ? error.message : String(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) {
|
||||||
|
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 normalizedBuffer = await normalizeSprite(buffer)
|
||||||
|
const { width, height } = await sharp(normalizedBuffer).metadata()
|
||||||
|
return { buffer: normalizedBuffer, 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 normalizeSprite(buffer: Buffer): Promise<Buffer> {
|
||||||
const publicFolder = path.join(process.cwd(), 'public', 'sprites', id)
|
const image = sharp(buffer)
|
||||||
await mkdir(publicFolder, { recursive: true })
|
|
||||||
|
|
||||||
await Promise.all(
|
// Remove any transparent edges
|
||||||
processedActions.map(async ({ action, buffersWithDimensions, frameWidth, frameHeight }) => {
|
const trimmed = await image
|
||||||
const combinedImage = await sharp({
|
.trim()
|
||||||
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()
|
.toBuffer()
|
||||||
|
|
||||||
const filename = path.join(publicFolder, `${action}.png`)
|
// Optional: Ensure dimensions are even numbers
|
||||||
await writeFile(filename, combinedImage)
|
const metadata = await sharp(trimmed).metadata()
|
||||||
})
|
const width = Math.ceil(metadata.width! / 2) * 2
|
||||||
)
|
const height = Math.ceil(metadata.height! / 2) * 2
|
||||||
|
|
||||||
|
return sharp(trimmed)
|
||||||
|
.resize(width, height, {
|
||||||
|
fit: 'contain',
|
||||||
|
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
||||||
|
})
|
||||||
|
.toBuffer()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 }) => {
|
||||||
|
// First pass: analyze all frames to determine the consistent dimensions
|
||||||
|
const frames = await Promise.all(
|
||||||
|
buffersWithDimensions.map(async ({ buffer }) => {
|
||||||
|
const image = sharp(buffer)
|
||||||
|
|
||||||
|
// Get trim boundaries to find actual sprite content
|
||||||
|
const { info: trimData } = await image
|
||||||
|
.trim()
|
||||||
|
.toBuffer({ resolveWithObject: true })
|
||||||
|
|
||||||
|
// Get original metadata
|
||||||
|
const metadata = await sharp(buffer).metadata()
|
||||||
|
|
||||||
|
return {
|
||||||
|
buffer,
|
||||||
|
width: metadata.width!,
|
||||||
|
height: metadata.height!,
|
||||||
|
trimmed: {
|
||||||
|
width: trimData.width,
|
||||||
|
height: trimData.height,
|
||||||
|
left: metadata.width! - trimData.width,
|
||||||
|
top: metadata.height! - trimData.height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// Calculate the average center point of all frames
|
||||||
|
const centerPoints = frames.map(frame => ({
|
||||||
|
x: Math.floor(frame.trimmed.left + frame.trimmed.width / 2),
|
||||||
|
y: Math.floor(frame.trimmed.top + frame.trimmed.height / 2)
|
||||||
|
}))
|
||||||
|
|
||||||
|
const avgCenterX = Math.round(centerPoints.reduce((sum, p) => sum + p.x, 0) / frames.length)
|
||||||
|
const avgCenterY = Math.round(centerPoints.reduce((sum, p) => sum + p.y, 0) / frames.length)
|
||||||
|
|
||||||
|
// Create the combined image with precise alignment
|
||||||
|
const combinedImage = await sharp({
|
||||||
|
create: {
|
||||||
|
width: frameWidth * frames.length,
|
||||||
|
height: frameHeight,
|
||||||
|
channels: 4,
|
||||||
|
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.composite(
|
||||||
|
frames.map(({ buffer, width, height }, index) => {
|
||||||
|
// Calculate offset to maintain consistent center point
|
||||||
|
const frameCenterX = Math.floor(width / 2)
|
||||||
|
const frameCenterY = Math.floor(height / 2)
|
||||||
|
|
||||||
|
const adjustedLeft = index * frameWidth + (frameWidth / 2) - frameCenterX
|
||||||
|
const adjustedTop = (frameHeight / 2) - frameCenterY
|
||||||
|
|
||||||
|
// Round to nearest even number to prevent sub-pixel rendering
|
||||||
|
const left = Math.round(adjustedLeft / 2) * 2
|
||||||
|
const top = 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
input: buffer,
|
||||||
|
left,
|
||||||
|
top
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.png()
|
||||||
|
.toBuffer()
|
||||||
|
|
||||||
|
const filename = getPublicPath('sprites', id, `${action}.png`)
|
||||||
|
await writeFile(filename, combinedImage)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,4 +59,4 @@ export default class ZoneCreateEvent {
|
|||||||
callback([])
|
callback([])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,4 +58,4 @@ export default class ZoneDeleteEvent {
|
|||||||
callback(false)
|
callback(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,4 +41,4 @@ export default class ZoneListEvent {
|
|||||||
callback([])
|
callback([])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,4 +56,4 @@ export default class ZoneRequestEvent {
|
|||||||
callback(null)
|
callback(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,40 +39,52 @@ export default class ZoneUpdateEvent {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
public listen(): void {
|
public listen(): void {
|
||||||
this.socket.on('gm:zone_editor:zone:update', this.handleZoneUpdate.bind(this))
|
this.socket.on('gm:zone_editor:zone:update', this.handleEvent.bind(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleZoneUpdate(data: IPayload, callback: (response: Zone | null) => void): Promise<void> {
|
private async handleEvent(data: IPayload, callback: (response: Zone | null) => void): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const character = await CharacterRepository.getById(this.socket.characterId as number)
|
const character = await CharacterRepository.getById(this.socket.characterId as number)
|
||||||
if (!character) {
|
if (!character) {
|
||||||
gameMasterLogger.error('gm:zone_editor:zone:update error', 'Character not found')
|
gameMasterLogger.error('gm:zone_editor:zone:update error', 'Character not found')
|
||||||
callback(null)
|
return callback(null)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (character.role !== 'gm') {
|
if (character.role !== 'gm') {
|
||||||
gameMasterLogger.info(`User ${character.id} tried to update zone but is not a game master.`)
|
gameMasterLogger.info(`User ${character.id} tried to update zone but is not a game master.`)
|
||||||
callback(null)
|
return callback(null)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gameMasterLogger.info(`User ${character.id} has updated zone via zone editor.`)
|
gameMasterLogger.info(`User ${character.id} has updated zone via zone editor.`)
|
||||||
|
|
||||||
if (!data.zoneId) {
|
if (!data.zoneId) {
|
||||||
gameMasterLogger.info(`User ${character.id} tried to update zone but did not provide a zone id.`)
|
gameMasterLogger.info(`User ${character.id} tried to update zone but did not provide a zone id.`)
|
||||||
callback(null)
|
return callback(null)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let zone = await ZoneRepository.getById(data.zoneId)
|
let zone = await ZoneRepository.getById(data.zoneId)
|
||||||
|
|
||||||
if (!zone) {
|
if (!zone) {
|
||||||
gameMasterLogger.info(`User ${character.id} tried to update zone ${data.zoneId} but it does not exist.`)
|
gameMasterLogger.info(`User ${character.id} tried to update zone ${data.zoneId} but it does not exist.`)
|
||||||
callback(null)
|
return callback(null)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If tiles are larger than the zone, remove the extra tiles
|
||||||
|
if (data.tiles.length > data.height) {
|
||||||
|
data.tiles = data.tiles.slice(0, data.height)
|
||||||
|
}
|
||||||
|
for (let i = 0; i < data.tiles.length; i++) {
|
||||||
|
if (data.tiles[i].length > data.width) {
|
||||||
|
data.tiles[i] = data.tiles[i].slice(0, data.width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If zone event tiles are placed outside the zone's bounds, remove these
|
||||||
|
data.zoneEventTiles = data.zoneEventTiles.filter((tile) => tile.positionX >= 0 && tile.positionX < data.width && tile.positionY >= 0 && tile.positionY < data.height)
|
||||||
|
|
||||||
|
// If zone objects are placed outside the zone's bounds, remove these
|
||||||
|
data.zoneObjects = data.zoneObjects.filter((obj) => obj.positionX >= 0 && obj.positionX < data.width && obj.positionY >= 0 && obj.positionY < data.height)
|
||||||
|
|
||||||
await prisma.zone.update({
|
await prisma.zone.update({
|
||||||
where: { id: data.zoneId },
|
where: { id: data.zoneId },
|
||||||
data: {
|
data: {
|
||||||
@ -84,14 +101,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 +118,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,13 +142,18 @@ export default class ZoneUpdateEvent {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gameMasterLogger.info(`User ${character.id} has updated zone via zone editor.`)
|
||||||
|
|
||||||
callback(zone)
|
callback(zone)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @TODO #246: Reload zone for players who are currently in the zone
|
||||||
|
*/
|
||||||
zoneManager.unloadZone(data.zoneId)
|
zoneManager.unloadZone(data.zoneId)
|
||||||
await zoneManager.loadZone(zone)
|
await zoneManager.loadZone(zone)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
gameMasterLogger.error('gm:zone_editor:zone:update error', error.message)
|
gameMasterLogger.error(`gm:zone_editor:zone:update error: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
callback(null)
|
callback(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Server } from 'socket.io'
|
import { Server } from 'socket.io'
|
||||||
import { TSocket } from '../utilities/types'
|
import { TSocket } from '../utilities/types'
|
||||||
import { gameLogger } from '../utilities/logger'
|
import { gameLogger } from '../utilities/logger'
|
||||||
|
import UserRepository from '../repositories/userRepository'
|
||||||
|
|
||||||
export default class LoginEvent {
|
export default class LoginEvent {
|
||||||
constructor(
|
constructor(
|
||||||
@ -14,15 +15,15 @@ export default class LoginEvent {
|
|||||||
|
|
||||||
private handleLogin(): void {
|
private handleLogin(): void {
|
||||||
try {
|
try {
|
||||||
if (!this.socket.user) {
|
if (!this.socket.userId) {
|
||||||
gameLogger.warn('Login attempt without user data')
|
gameLogger.warn('Login attempt without user data')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.socket.emit('logged_in', { user: this.socket.user })
|
this.socket.emit('logged_in', { user: UserRepository.getById(this.socket.userId) })
|
||||||
gameLogger.info(`User logged in: ${this.socket.user.id}`)
|
gameLogger.info(`User logged in: ${this.socket.userId}`)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
gameLogger.error('login error', error.message)
|
gameLogger.error('login error', error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
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 { Character, Zone } from '@prisma/client'
|
import { Zone } from '@prisma/client'
|
||||||
import CharacterManager from '../../managers/characterManager'
|
|
||||||
import { gameLogger } from '../../utilities/logger'
|
import { gameLogger } from '../../utilities/logger'
|
||||||
|
import CharacterRepository from '../../repositories/characterRepository'
|
||||||
|
import ZoneManager from '../../managers/zoneManager'
|
||||||
|
import zoneCharacter from '../../models/zoneCharacter'
|
||||||
|
import zoneManager from '../../managers/zoneManager'
|
||||||
|
|
||||||
interface IResponse {
|
interface IResponse {
|
||||||
zone: Zone
|
zone: Zone
|
||||||
characters: Character[]
|
characters: zoneCharacter[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class CharacterJoinEvent {
|
export default class CharacterJoinEvent {
|
||||||
@ -22,32 +25,50 @@ 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)
|
||||||
if (!character) return
|
if (!character) {
|
||||||
|
gameLogger.error('zone:character:join error', 'Character not found')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @TODO: If zone is not found, spawn back to the start
|
||||||
|
*/
|
||||||
const zone = await ZoneRepository.getById(character.zoneId)
|
const zone = await ZoneRepository.getById(character.zoneId)
|
||||||
if (!zone) {
|
if (!zone) {
|
||||||
gameLogger.error('zone:character:join error', 'Zone not found')
|
gameLogger.error('zone:character:join error', 'Zone not found')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (character.zoneId) {
|
/**
|
||||||
this.socket.leave(character.zoneId.toString())
|
* @TODO: If zone is not found, spawn back to the start
|
||||||
this.io.to(character.zoneId.toString()).emit('zone:character:leave', character)
|
*/
|
||||||
|
const loadedZone = ZoneManager.getZoneById(zone.id)
|
||||||
|
if (!loadedZone) {
|
||||||
|
gameLogger.error('zone:character:join error', 'Loaded zone not found')
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadedZone.addCharacter(character)
|
||||||
|
|
||||||
this.socket.join(zone.id.toString())
|
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', zoneManager.getCharacter(character.id))
|
||||||
|
|
||||||
// send over zone and characters to socket
|
// Log
|
||||||
callback({ zone, characters: CharacterManager.getCharactersInZone(zone) })
|
gameLogger.info(`User ${character.id} joined zone ${zone.id}`)
|
||||||
|
|
||||||
|
// Send over zone and characters to socket
|
||||||
|
callback({ zone, characters: loadedZone.getCharactersInZone() })
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
gameLogger.error('zone:character:join error', error.message)
|
gameLogger.error('zone:character:join error', error.message)
|
||||||
this.socket.disconnect()
|
this.socket.disconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
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 CharacterManager from '../../managers/characterManager'
|
|
||||||
import { gameLogger } from '../../utilities/logger'
|
import { gameLogger } from '../../utilities/logger'
|
||||||
|
import ZoneManager from '../../managers/zoneManager'
|
||||||
|
import CharacterRepository from '../../repositories/characterRepository'
|
||||||
|
|
||||||
export default class ZoneLeaveEvent {
|
export default class ZoneLeaveEvent {
|
||||||
constructor(
|
constructor(
|
||||||
@ -16,21 +17,29 @@ export default class ZoneLeaveEvent {
|
|||||||
|
|
||||||
private async handleZoneLeave(): Promise<void> {
|
private async handleZoneLeave(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const character = CharacterManager.getCharacterFromSocket(this.socket)
|
if (!this.socket.characterId) {
|
||||||
|
gameLogger.error('zone:character:join error', 'Zone requested but no character id set')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const character = await CharacterRepository.getById(this.socket.characterId)
|
||||||
if (!character) {
|
if (!character) {
|
||||||
gameLogger.error('zone:character:leave error', 'Character not found')
|
gameLogger.error('zone:character:join error', 'Character not found')
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!character.zoneId) {
|
|
||||||
gameLogger.error('zone:character:leave error', 'Character not in a zone')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @TODO: If zone is not found, spawn back to the start
|
||||||
|
*/
|
||||||
const zone = await ZoneRepository.getById(character.zoneId)
|
const zone = await ZoneRepository.getById(character.zoneId)
|
||||||
|
|
||||||
if (!zone) {
|
if (!zone) {
|
||||||
gameLogger.error('zone:character:leave error', 'Zone not found')
|
gameLogger.error('zone:character:join error', 'Zone not found')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadedZone = ZoneManager.getZoneById(zone.id)
|
||||||
|
if (!loadedZone) {
|
||||||
|
gameLogger.error('zone:character:join error', 'Loaded zone not found')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,7 +49,7 @@ export default class ZoneLeaveEvent {
|
|||||||
this.io.to(zone.id.toString()).emit('zone:character:leave', character.id)
|
this.io.to(zone.id.toString()).emit('zone:character:leave', character.id)
|
||||||
|
|
||||||
// remove character from zone manager
|
// remove character from zone manager
|
||||||
await CharacterManager.removeCharacter(character)
|
await loadedZone.removeCharacter(character.id)
|
||||||
|
|
||||||
gameLogger.info('zone:character:leave', `Character ${character.id} left zone ${zone.id}`)
|
gameLogger.info('zone:character:leave', `Character ${character.id} left zone ${zone.id}`)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
@ -1,144 +1,96 @@
|
|||||||
import { Server } from 'socket.io'
|
import { Server } from 'socket.io'
|
||||||
import { TSocket, ExtendedCharacter } from '../../utilities/types'
|
import { TSocket, ZoneEventTileWithTeleport } from '../../utilities/types'
|
||||||
import { CharacterMoveService } from '../../services/character/characterMoveService'
|
import { CharacterService } from '../../services/characterService'
|
||||||
import { ZoneEventTileService } from '../../services/zoneEventTileService'
|
import { ZoneEventTileService } from '../../services/zoneEventTileService'
|
||||||
import prisma from '../../utilities/prisma'
|
|
||||||
import { ZoneEventTile, ZoneEventTileTeleport } from '@prisma/client'
|
|
||||||
import Rotation from '../../utilities/character/rotation'
|
import Rotation from '../../utilities/character/rotation'
|
||||||
import CharacterManager from '../../managers/characterManager'
|
|
||||||
import { gameLogger } from '../../utilities/logger'
|
import { gameLogger } from '../../utilities/logger'
|
||||||
import QueueManager from '../../managers/queueManager'
|
import ZoneManager from '../../managers/zoneManager'
|
||||||
|
import ZoneCharacter from '../../models/zoneCharacter'
|
||||||
export type ZoneEventTileWithTeleport = ZoneEventTile & {
|
import zoneEventTileRepository from '../../repositories/zoneEventTileRepository'
|
||||||
teleport: ZoneEventTileTeleport
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class CharacterMove {
|
export default class CharacterMove {
|
||||||
private characterMoveService: CharacterMoveService
|
private readonly characterService = new CharacterService()
|
||||||
private zoneEventTileService: ZoneEventTileService
|
private readonly zoneEventTileService = new ZoneEventTileService()
|
||||||
private nextPath: { [index: number]: { x: number; y: number }[] } = []
|
|
||||||
private currentZoneId: { [index: number]: number } = []
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly io: Server,
|
private readonly io: Server,
|
||||||
private readonly socket: TSocket
|
private readonly socket: TSocket
|
||||||
) {
|
) {}
|
||||||
this.characterMoveService = new CharacterMoveService()
|
|
||||||
this.zoneEventTileService = new ZoneEventTileService()
|
|
||||||
}
|
|
||||||
|
|
||||||
public listen(): void {
|
public listen(): void {
|
||||||
this.socket.on('character:initMove', this.handleCharacterMove.bind(this))
|
this.socket.on('character:move', this.handleCharacterMove.bind(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleCharacterMove({ positionX, positionY }: { positionX: number; positionY: number }): Promise<void> {
|
private async handleCharacterMove({ positionX, positionY }: { positionX: number; positionY: number }): Promise<void> {
|
||||||
let character = CharacterManager.getCharacterFromSocket(this.socket)
|
const zoneCharacter = ZoneManager.getCharacter(this.socket.characterId!)
|
||||||
if (!character) {
|
if (!zoneCharacter?.character) {
|
||||||
gameLogger.error('character:move error', 'Character not found')
|
gameLogger.error('character:move error', 'Character not found or not initialized')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!character) {
|
// If already moving, cancel current movement and wait for it to fully stop
|
||||||
gameLogger.error('character:move error', 'character has not been initialized?')
|
if (zoneCharacter.isMoving) {
|
||||||
return
|
zoneCharacter.isMoving = false
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = await this.characterMoveService.calculatePath(character, positionX, positionY)
|
const path = await this.characterService.calculatePath(zoneCharacter.character, positionX, positionY)
|
||||||
if (!path) {
|
if (!path) {
|
||||||
this.io.in(character.zoneId.toString()).emit('character:moveError', 'No valid path found')
|
this.io.in(zoneCharacter.character.zoneId.toString()).emit('character:moveError', 'No valid path found')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!character.isMoving && character.resetMovement) {
|
// Start new movement
|
||||||
character.resetMovement = false
|
zoneCharacter.isMoving = true
|
||||||
}
|
zoneCharacter.currentPath = path // Add this property to ZoneCharacter class
|
||||||
if (character.isMoving && !character.resetMovement) {
|
await this.moveAlongPath(zoneCharacter, path)
|
||||||
character.resetMovement = true
|
|
||||||
this.nextPath[character.id] = path
|
|
||||||
}
|
|
||||||
if (!character.isMoving && !character.resetMovement) {
|
|
||||||
character.isMoving = true
|
|
||||||
this.currentZoneId[character.id] = character.zoneId
|
|
||||||
await this.moveAlongPath(character, path)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async moveAlongPath(character: ExtendedCharacter, path: Array<{ x: number; y: number }>): Promise<void> {
|
private async moveAlongPath(zoneCharacter: ZoneCharacter, path: Array<{ x: number; y: number }>): Promise<void> {
|
||||||
|
const { character } = zoneCharacter
|
||||||
|
|
||||||
for (let i = 0; i < path.length - 1; i++) {
|
for (let i = 0; i < path.length - 1; i++) {
|
||||||
const start = path[i]
|
// Exit if movement was cancelled or interrupted
|
||||||
const end = path[i + 1]
|
if (!zoneCharacter.isMoving || zoneCharacter.currentPath !== path) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// if (!(await this.movementValidator.isValidMove(character, end))) {
|
const [start, end] = [path[i], path[i + 1]]
|
||||||
// break
|
character.rotation = Rotation.calculate(start.x, start.y, end.x, end.y)
|
||||||
// }
|
|
||||||
|
|
||||||
if (CharacterManager.hasResetMovement(character)) {
|
const zoneEventTile = await zoneEventTileRepository.getEventTileByZoneIdAndPosition(character.zoneId, Math.floor(end.x), Math.floor(end.y))
|
||||||
|
|
||||||
|
if (zoneEventTile?.type === 'BLOCK') break
|
||||||
|
if (zoneEventTile?.type === 'TELEPORT' && zoneEventTile.teleport) {
|
||||||
|
await this.handleZoneEventTile(zoneEventTile as ZoneEventTileWithTeleport)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
character.rotation = Rotation.calculate(start.x, start.y, end.x, end.y)
|
this.characterService.updatePosition(character, end)
|
||||||
|
this.io.in(character.zoneId.toString()).emit('character:move', zoneCharacter)
|
||||||
const zoneEventTile = await prisma.zoneEventTile.findFirst({
|
await this.characterService.applyMovementDelay()
|
||||||
where: {
|
|
||||||
zoneId: character.zoneId,
|
|
||||||
positionX: Math.floor(end.x),
|
|
||||||
positionY: Math.floor(end.y)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (zoneEventTile) {
|
|
||||||
if (zoneEventTile.type === 'BLOCK') {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if (zoneEventTile.type === 'TELEPORT') {
|
|
||||||
const teleportTile = (await prisma.zoneEventTile.findFirst({
|
|
||||||
where: { id: zoneEventTile.id },
|
|
||||||
include: { teleport: true }
|
|
||||||
})) as ZoneEventTileWithTeleport
|
|
||||||
|
|
||||||
if (teleportTile) {
|
|
||||||
await this.handleZoneEventTile(teleportTile)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.characterMoveService.updatePosition(character, end)
|
|
||||||
this.io.in(character.zoneId.toString()).emit('character:move', character)
|
|
||||||
|
|
||||||
await this.characterMoveService.applyMovementDelay()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CharacterManager.hasResetMovement(character)) {
|
// Only finalize if this path wasn't interrupted
|
||||||
character.resetMovement = false
|
if (zoneCharacter.isMoving && zoneCharacter.currentPath === path) {
|
||||||
if (this.currentZoneId[character.id] === character.zoneId) {
|
this.finalizeMovement(zoneCharacter)
|
||||||
await this.moveAlongPath(character, this.nextPath[character.id])
|
|
||||||
} else {
|
|
||||||
delete this.currentZoneId[character.id]
|
|
||||||
character.isMoving = false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.finalizeMovement(character)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleZoneEventTile(zoneEventTile: ZoneEventTileWithTeleport): Promise<void> {
|
private async handleZoneEventTile(zoneEventTile: ZoneEventTileWithTeleport): Promise<void> {
|
||||||
const character = CharacterManager.getCharacterFromSocket(this.socket)
|
const zoneCharacter = ZoneManager.getCharacter(this.socket.characterId!)
|
||||||
if (!character) {
|
if (!zoneCharacter) {
|
||||||
gameLogger.error('character:move error', 'Character not found')
|
gameLogger.error('character:move error', 'Character not found')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const teleport = zoneEventTile.teleport
|
if (zoneEventTile.teleport) {
|
||||||
if (teleport) {
|
await this.zoneEventTileService.handleTeleport(this.io, this.socket, zoneCharacter.character, zoneEventTile.teleport)
|
||||||
await this.zoneEventTileService.handleTeleport(this.io, this.socket, character, teleport)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private finalizeMovement(character: ExtendedCharacter): void {
|
private finalizeMovement(zoneCharacter: ZoneCharacter): void {
|
||||||
character.isMoving = false
|
zoneCharacter.isMoving = false
|
||||||
this.io.in(character.zoneId.toString()).emit('character:move', character)
|
this.io.in(zoneCharacter.character.zoneId.toString()).emit('character:move', zoneCharacter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
24
src/socketEvents/zone/weather.ts
Normal file
24
src/socketEvents/zone/weather.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Server } from 'socket.io'
|
||||||
|
import { TSocket } from '../../utilities/types'
|
||||||
|
import { gameLogger } from '../../utilities/logger'
|
||||||
|
import WeatherManager from '../../managers/weatherManager'
|
||||||
|
|
||||||
|
export default class Weather {
|
||||||
|
constructor(
|
||||||
|
private readonly io: Server,
|
||||||
|
private readonly socket: TSocket
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public listen(): void {
|
||||||
|
this.socket.on('weather', this.handleEvent.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleEvent(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const weather = await WeatherManager.getWeatherState()
|
||||||
|
this.socket.emit('weather', weather)
|
||||||
|
} catch (error: any) {
|
||||||
|
gameLogger.error('error', error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,33 +2,31 @@ import config from '../config'
|
|||||||
|
|
||||||
class Rotation {
|
class Rotation {
|
||||||
static calculate(X1: number, Y1: number, X2: number, Y2: number): number {
|
static calculate(X1: number, Y1: number, X2: number, Y2: number): number {
|
||||||
let rotation = 0
|
|
||||||
|
|
||||||
if (config.ALLOW_DIAGONAL_MOVEMENT) {
|
if (config.ALLOW_DIAGONAL_MOVEMENT) {
|
||||||
|
// Check diagonal movements
|
||||||
if (X1 > X2 && Y1 > Y2) {
|
if (X1 > X2 && Y1 > Y2) {
|
||||||
rotation = 7
|
return 7
|
||||||
} else if (X1 < X2 && Y1 < Y2) {
|
} else if (X1 < X2 && Y1 < Y2) {
|
||||||
rotation = 3
|
return 3
|
||||||
} else if (X1 > X2 && Y1 < Y2) {
|
} else if (X1 > X2 && Y1 < Y2) {
|
||||||
rotation = 5
|
return 5
|
||||||
} else if (X1 < X2 && Y1 > Y2) {
|
} else if (X1 < X2 && Y1 > Y2) {
|
||||||
rotation = 1
|
return 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rotation === 0) {
|
// Non-diagonal movements
|
||||||
if (X1 > X2) {
|
if (X1 > X2) {
|
||||||
rotation = 6
|
return 6
|
||||||
} else if (X1 < X2) {
|
} else if (X1 < X2) {
|
||||||
rotation = 2
|
return 2
|
||||||
} else if (Y1 < Y2) {
|
} else if (Y1 < Y2) {
|
||||||
rotation = 4
|
return 4
|
||||||
} else if (Y1 > Y2) {
|
} else if (Y1 > Y2) {
|
||||||
rotation = 0
|
return 0
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return rotation
|
return 0 // Default case
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 === `/${command}` || 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(' ')
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ class config {
|
|||||||
static REDIS_URL: string = process.env.REDIS_URL || 'redis://@127.0.0.1:6379/4'
|
static REDIS_URL: string = process.env.REDIS_URL || 'redis://@127.0.0.1:6379/4'
|
||||||
static HOST: string = process.env.HOST || '0.0.0.0'
|
static HOST: string = process.env.HOST || '0.0.0.0'
|
||||||
static PORT: number = process.env.PORT ? parseInt(process.env.PORT) : 6969
|
static PORT: number = process.env.PORT ? parseInt(process.env.PORT) : 6969
|
||||||
|
static CLIENT_URL: string = process.env.CLIENT_URL ? process.env.CLIENT_URL : 'https://sylvan.quest'
|
||||||
static JWT_SECRET: string = process.env.JWT_SECRET || 'secret'
|
static JWT_SECRET: string = process.env.JWT_SECRET || 'secret'
|
||||||
|
|
||||||
static ALLOW_DIAGONAL_MOVEMENT: boolean = process.env.ALLOW_DIAGONAL_MOVEMENT === 'true'
|
static ALLOW_DIAGONAL_MOVEMENT: boolean = process.env.ALLOW_DIAGONAL_MOVEMENT === 'true'
|
||||||
@ -14,6 +15,11 @@ class config {
|
|||||||
static DEFAULT_CHARACTER_ZONE: number = parseInt(process.env.DEFAULT_CHARACTER_ZONE || '1')
|
static DEFAULT_CHARACTER_ZONE: number = parseInt(process.env.DEFAULT_CHARACTER_ZONE || '1')
|
||||||
static DEFAULT_CHARACTER_X: number = parseInt(process.env.DEFAULT_CHARACTER_POS_X || '0')
|
static DEFAULT_CHARACTER_X: number = parseInt(process.env.DEFAULT_CHARACTER_POS_X || '0')
|
||||||
static DEFAULT_CHARACTER_Y: number = parseInt(process.env.DEFAULT_CHARACTER_POS_Y || '0')
|
static DEFAULT_CHARACTER_Y: number = parseInt(process.env.DEFAULT_CHARACTER_POS_Y || '0')
|
||||||
|
|
||||||
|
static SMTP_HOST: string = process.env.SMTP_HOST || 'my.directonline.io'
|
||||||
|
static SMTP_PORT: number = process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT) : 587
|
||||||
|
static SMTP_USER: string = process.env.SMTP_USER || 'no-reply@sylvan.quest'
|
||||||
|
static SMTP_PASSWORD: string = process.env.SMTP_PASSWORD || 'password'
|
||||||
}
|
}
|
||||||
|
|
||||||
export default config
|
export default config
|
||||||
|
@ -2,138 +2,16 @@ import { Application, Request, Response } from 'express'
|
|||||||
import UserService from '../services/userService'
|
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, resetPasswordSchema, newPasswordSchema } 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 TileRepository from '../repositories/tileRepository'
|
||||||
|
import { AssetData } from './types'
|
||||||
|
import ZoneRepository from '../repositories/zoneRepository'
|
||||||
|
import SpriteRepository from '../repositories/spriteRepository'
|
||||||
|
|
||||||
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
|
||||||
@ -165,16 +43,16 @@ async function addHttpRoutes(app: Application) {
|
|||||||
* @param res
|
* @param res
|
||||||
*/
|
*/
|
||||||
app.post('/register', async (req: Request, res: Response) => {
|
app.post('/register', async (req: Request, res: Response) => {
|
||||||
const { username, password } = req.body
|
const { username, email, password } = req.body
|
||||||
|
|
||||||
try {
|
try {
|
||||||
registerAccountSchema.parse({ username, password })
|
registerAccountSchema.parse({ username, email, password })
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return res.status(400).json({ message: error.errors[0]?.message })
|
return res.status(400).json({ message: error.errors[0]?.message })
|
||||||
}
|
}
|
||||||
|
|
||||||
const userService = new UserService()
|
const userService = new UserService()
|
||||||
const user = await userService.register(username, password)
|
const user = await userService.register(username, email, password)
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
return res.status(200).json({ message: 'User registered' })
|
return res.status(200).json({ message: 'User registered' })
|
||||||
@ -183,6 +61,171 @@ 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' })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset password
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
app.post('/reset-password', async (req: Request, res: Response) => {
|
||||||
|
const { email } = req.body
|
||||||
|
|
||||||
|
try {
|
||||||
|
resetPasswordSchema.parse({ email })
|
||||||
|
} catch (error: any) {
|
||||||
|
return res.status(400).json({ message: error.errors[0]?.message })
|
||||||
|
}
|
||||||
|
|
||||||
|
const userService = new UserService()
|
||||||
|
const sentEmail = await userService.requestPasswordReset(email)
|
||||||
|
|
||||||
|
if (sentEmail) {
|
||||||
|
return res.status(200).json({ message: 'Email has been sent' })
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(400).json({ message: 'Failed to send password reset request. Perhaps one has already been sent recently, check your spam folder.' })
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* New password
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
app.post('/new-password', async (req: Request, res: Response) => {
|
||||||
|
const { urlToken, password } = req.body
|
||||||
|
|
||||||
|
try {
|
||||||
|
newPasswordSchema.parse({ urlToken, password })
|
||||||
|
} catch (error: any) {
|
||||||
|
return res.status(400).json({ message: error.errors[0]?.message })
|
||||||
|
}
|
||||||
|
|
||||||
|
const userService = new UserService()
|
||||||
|
const resetPassword = await userService.resetPassword(urlToken, password)
|
||||||
|
|
||||||
|
if (resetPassword) {
|
||||||
|
return res.status(200).json({ message: 'Password has been reset' })
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(400).json({ message: 'Failed to set new password' })
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all tiles from a zone as an array of ids
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
app.get('/assets/list_tiles', async (req: Request, res: Response) => {
|
||||||
|
// Get all tiles
|
||||||
|
let assets: AssetData[] = []
|
||||||
|
const tiles = await TileRepository.getAll()
|
||||||
|
for (const tile of tiles) {
|
||||||
|
assets.push({
|
||||||
|
key: tile.id,
|
||||||
|
data: '/assets/tiles/' + tile.id + '.png',
|
||||||
|
group: 'tiles',
|
||||||
|
updatedAt: tile.updatedAt
|
||||||
|
} as AssetData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the array
|
||||||
|
res.json(assets)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all tiles from a zone and serve as AssetData array
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
app.get('/assets/list_tiles/:zoneId', async (req: Request, res: Response) => {
|
||||||
|
const zoneId = req.params.zoneId
|
||||||
|
|
||||||
|
// Check if zoneId is valid number
|
||||||
|
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' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all tiles
|
||||||
|
let assets: AssetData[] = []
|
||||||
|
const tiles = await TileRepository.getByZoneId(parseInt(zoneId))
|
||||||
|
for (const tile of tiles) {
|
||||||
|
assets.push({
|
||||||
|
key: tile.id,
|
||||||
|
data: '/assets/tiles/' + tile.id + '.png',
|
||||||
|
group: 'tiles',
|
||||||
|
updatedAt: tile.updatedAt
|
||||||
|
} as AssetData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the array
|
||||||
|
res.json(assets)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/assets/list_sprite_actions/:spriteId', async (req: Request, res: Response) => {
|
||||||
|
const spriteId = req.params.spriteId
|
||||||
|
// Check if spriteId is valid number
|
||||||
|
if (!spriteId || parseInt(spriteId) === 0) {
|
||||||
|
return res.status(400).json({ message: 'Invalid sprite ID' })
|
||||||
|
}
|
||||||
|
// Get sprite by id
|
||||||
|
const sprite = await SpriteRepository.getById(spriteId)
|
||||||
|
if (!sprite) {
|
||||||
|
return res.status(404).json({ message: 'Sprite not found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
let assets: AssetData[] = []
|
||||||
|
sprite.spriteActions.forEach((spriteAction) => {
|
||||||
|
assets.push({
|
||||||
|
key: sprite.id + '-' + spriteAction.action,
|
||||||
|
data: '/assets/sprites/' + sprite.id + '/' + spriteAction.action + '.png',
|
||||||
|
group: spriteAction.isAnimated ? 'sprite_animations' : 'sprites',
|
||||||
|
updatedAt: sprite.updatedAt,
|
||||||
|
isAnimated: spriteAction.isAnimated,
|
||||||
|
frameCount: JSON.parse(JSON.stringify(spriteAction.sprites)).length,
|
||||||
|
frameWidth: spriteAction.frameWidth,
|
||||||
|
frameHeight: spriteAction.frameHeight
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Return the array
|
||||||
|
res.json(assets)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download asset file
|
||||||
|
* @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')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,19 +30,37 @@ 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) => {
|
// Get initial file size
|
||||||
if (curr.size > prev.size) {
|
const stats = fs.statSync(logFile)
|
||||||
const stream = fs.createReadStream(logFile, { start: prev.size, end: curr.size })
|
let lastPosition = stats.size
|
||||||
stream.on('data', (chunk) => {
|
|
||||||
console.log(`[${type}]\n${chunk.toString()}`)
|
fs.watch(logFile, (eventType) => {
|
||||||
})
|
if (eventType !== 'change') {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fs.stat(logFile, (err, stats) => {
|
||||||
|
if (err) return
|
||||||
|
|
||||||
|
if (stats.size > lastPosition) {
|
||||||
|
const stream = fs.createReadStream(logFile, {
|
||||||
|
start: lastPosition,
|
||||||
|
end: stats.size
|
||||||
|
})
|
||||||
|
|
||||||
|
stream.on('data', (chunk) => {
|
||||||
|
console.log(`[${type}]\n${chunk.toString()}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
lastPosition = stats.size
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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
33
src/utilities/storage.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
import { Socket } from 'socket.io'
|
import { Socket } from 'socket.io'
|
||||||
import { Character, User } from '@prisma/client'
|
import { Character, User, ZoneEventTile, ZoneEventTileTeleport } from '@prisma/client'
|
||||||
|
|
||||||
export type TSocket = Socket & {
|
export type TSocket = Socket & {
|
||||||
user?: User
|
userId?: number
|
||||||
characterId?: number
|
characterId?: number
|
||||||
handshake?: {
|
handshake?: {
|
||||||
query?: {
|
query?: {
|
||||||
@ -18,17 +18,31 @@ export type TSocket = Socket & {
|
|||||||
|
|
||||||
export type ExtendedCharacter = Character & {
|
export type ExtendedCharacter = Character & {
|
||||||
isMoving?: boolean
|
isMoving?: boolean
|
||||||
resetMovement: boolean
|
resetMovement?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TAsset = {
|
export type ZoneEventTileWithTeleport = ZoneEventTile & {
|
||||||
|
teleport: ZoneEventTileTeleport
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AssetData = {
|
||||||
key: string
|
key: string
|
||||||
url: string
|
data: 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
|
||||||
|
isAnimated?: boolean
|
||||||
|
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
|
||||||
|
3
src/utilities/utilities.ts
Normal file
3
src/utilities/utilities.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export function unduplicateArray(array: any[]) {
|
||||||
|
return [...new Set(array.flat())]
|
||||||
|
}
|
@ -20,6 +20,29 @@ export const registerAccountSchema = z.object({
|
|||||||
.min(3, { message: 'Name must be at least 3 characters long' })
|
.min(3, { message: 'Name must be at least 3 characters long' })
|
||||||
.max(255, { message: 'Name must be at most 255 characters long' })
|
.max(255, { message: 'Name must be at most 255 characters long' })
|
||||||
.regex(/^[A-Za-z][A-Za-z0-9_-]*$/, { message: 'Name must start with a letter and can only contain letters, numbers, underscores, or dashes' }),
|
.regex(/^[A-Za-z][A-Za-z0-9_-]*$/, { message: 'Name must start with a letter and can only contain letters, numbers, underscores, or dashes' }),
|
||||||
|
email: z
|
||||||
|
.string()
|
||||||
|
.min(3, { message: 'Email must be at least 3 characters long' })
|
||||||
|
.max(255, { message: 'Email must be at most 255 characters long' })
|
||||||
|
.regex(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/, { message: 'Email must be valid' }),
|
||||||
|
password: z
|
||||||
|
.string()
|
||||||
|
.min(8, {
|
||||||
|
message: 'Password must be at least 8 characters long'
|
||||||
|
})
|
||||||
|
.max(255)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const resetPasswordSchema = z.object({
|
||||||
|
email: z
|
||||||
|
.string()
|
||||||
|
.min(3, { message: 'Email must be at least 3 characters long' })
|
||||||
|
.max(255, { message: 'Email must be at most 255 characters long' })
|
||||||
|
.regex(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/, { message: 'Email must be valid' })
|
||||||
|
})
|
||||||
|
|
||||||
|
export const newPasswordSchema = z.object({
|
||||||
|
urlToken: z.string().min(10, { message: 'Invalid request' }).max(255, { message: 'Invalid request' }),
|
||||||
password: z
|
password: z
|
||||||
.string()
|
.string()
|
||||||
.min(8, {
|
.min(8, {
|
||||||
|
9
src/utilities/zone.ts
Normal file
9
src/utilities/zone.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export function FlattenZoneArray(tiles: string[][]) {
|
||||||
|
const normalArray = []
|
||||||
|
|
||||||
|
for (const row of tiles) {
|
||||||
|
normalArray.push(...row)
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalArray
|
||||||
|
}
|
Reference in New Issue
Block a user