diff --git a/package-lock.json b/package-lock.json index f7f728a..05e5ae1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -949,9 +949,9 @@ "license": "BSD-3-Clause" }, "node_modules/bullmq": { - "version": "5.24.0", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.24.0.tgz", - "integrity": "sha512-rNWOg4opfHOhZjWWr1aIjfw2nUFB91F9qwIT49CdRypL4FznmHAqamTnw2EcZlj2KeFswV50tisZwq/h1yMUAw==", + "version": "5.25.6", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.25.6.tgz", + "integrity": "sha512-jxpa/DB02V20CqBAgyqpQazT630CJm0r4fky8EchH3mcJAomRtKXLS6tRA0J8tb29BDGlr/LXhlUuZwdBJBSdA==", "license": "MIT", "dependencies": { "cron-parser": "^4.6.0", @@ -2064,9 +2064,9 @@ } }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", "license": "MIT", "engines": { "node": ">= 0.4" diff --git a/prisma/migrations/20241028210606_init/migration.sql b/prisma/migrations/20241113010305_init/migration.sql similarity index 95% rename from prisma/migrations/20241028210606_init/migration.sql rename to prisma/migrations/20241113010305_init/migration.sql index 70e6ef1..ee38c1a 100644 --- a/prisma/migrations/20241028210606_init/migration.sql +++ b/prisma/migrations/20241113010305_init/migration.sql @@ -1,3 +1,14 @@ +-- CreateTable +CREATE TABLE `World` ( + `date` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `isRainEnabled` BOOLEAN NOT NULL DEFAULT false, + `rainPercentage` INTEGER NOT NULL DEFAULT 0, + `isFogEnabled` BOOLEAN NOT NULL DEFAULT false, + `fogDensity` INTEGER NOT NULL DEFAULT 0, + + UNIQUE INDEX `World_date_key`(`date`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + -- CreateTable CREATE TABLE `Chat` ( `id` INTEGER NOT NULL AUTO_INCREMENT, diff --git a/prisma/schema/game.prisma b/prisma/schema/game.prisma new file mode 100644 index 0000000..c3d8571 --- /dev/null +++ b/prisma/schema/game.prisma @@ -0,0 +1,7 @@ +model World { + date DateTime @unique @default(now()) + isRainEnabled Boolean @default(false) + rainPercentage Int @default(0) + isFogEnabled Boolean @default(false) + fogDensity Int @default(0) +} diff --git a/src/managers/characterManager.ts b/src/managers/characterManager.ts deleted file mode 100644 index 6fc17a1..0000000 --- a/src/managers/characterManager.ts +++ /dev/null @@ -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() diff --git a/src/managers/dateManager.ts b/src/managers/dateManager.ts index 919f33c..5ea8d7c 100644 --- a/src/managers/dateManager.ts +++ b/src/managers/dateManager.ts @@ -1,7 +1,7 @@ import { Server } from 'socket.io' import { appLogger } from '../utilities/logger' -import { getRootPath } from '../utilities/storage' -import { readJsonValue, setJsonValue } from '../utilities/json' +import prisma from '../utilities/prisma' +import worldService from '../services/worldService' class DateManager { private static readonly GAME_SPEED = 8 // 24 game hours / 3 real hours @@ -18,7 +18,6 @@ class DateManager { appLogger.info('Date manager loaded') } - // When a GM sets the time, update the current date and update the world file public async setTime(time: string): Promise { try { let newDate: Date @@ -45,8 +44,13 @@ class DateManager { private async loadDate(): Promise { try { - const dateString = await readJsonValue(this.getWorldFilePath(), 'date') - this.currentDate = new Date(dateString) + const world = await prisma.world.findFirst({ + orderBy: { date: 'desc' } + }) + + 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 @@ -57,7 +61,7 @@ class DateManager { this.intervalId = setInterval(() => { this.advanceGameTime() this.emitDate() - this.saveDate() + void this.saveDate() }, DateManager.UPDATE_INTERVAL) } @@ -72,14 +76,22 @@ class DateManager { private async saveDate(): Promise { try { - await setJsonValue(this.getWorldFilePath(), 'date', this.currentDate) + await worldService.update({ + date: this.currentDate + }) } catch (error) { appLogger.error(`Failed to save date: ${error instanceof Error ? error.message : String(error)}`) } } - private getWorldFilePath(): string { - return getRootPath('data', 'world.json') + public cleanup(): void { + if (this.intervalId) { + clearInterval(this.intervalId) + } + } + + public getCurrentDate(): Date { + return this.currentDate } } diff --git a/src/managers/weatherManager.ts b/src/managers/weatherManager.ts index 21e6c8d..eb897a2 100644 --- a/src/managers/weatherManager.ts +++ b/src/managers/weatherManager.ts @@ -1,7 +1,7 @@ import { Server } from 'socket.io' import { appLogger } from '../utilities/logger' -import { getRootPath } from '../utilities/storage' -import { readJsonValue, setJsonValue } from '../utilities/json' +import prisma from '../utilities/prisma' +import worldService from '../services/worldService' interface WeatherState { isRainEnabled: boolean @@ -37,46 +37,48 @@ class WeatherManager { ? Math.floor(Math.random() * 50) + 50 // 50-100% : 0 - // Save weather await this.saveWeather() - - // Emit weather this.emitWeather() } public async toggleFog(): Promise { this.weatherState.isFogEnabled = !this.weatherState.isFogEnabled this.weatherState.fogDensity = this.weatherState.isFogEnabled - ? Math.random() * 0.7 + 0.3 // 0.3-1.0 + ? Math.floor((Math.random() * 0.7 + 0.3) * 100) // Convert 0.3-1.0 to 30-100 : 0 - // Save weather await this.saveWeather() - - // Emit weather this.emitWeather() } private async loadWeather(): Promise { try { - this.weatherState.isRainEnabled = await readJsonValue(this.getWorldFilePath(), 'isRainEnabled') - this.weatherState.rainPercentage = await readJsonValue(this.getWorldFilePath(), 'rainPercentage') - this.weatherState.isFogEnabled = await readJsonValue(this.getWorldFilePath(), 'isFogEnabled') - this.weatherState.fogDensity = await readJsonValue(this.getWorldFilePath(), 'fogDensity') + const world = await prisma.world.findFirst({ + orderBy: { date: 'desc' } + }) + + 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 async getWeatherState(): Promise { + public getWeatherState(): WeatherState { return this.weatherState } private startWeatherLoop(): void { - this.intervalId = setInterval(() => { + this.intervalId = setInterval(async () => { this.updateWeather() this.emitWeather() - this.saveWeather().catch((error) => { + await this.saveWeather().catch((error) => { appLogger.error(`Failed to save weather: ${error instanceof Error ? error.message : String(error)}`) }) }, WeatherManager.UPDATE_INTERVAL) @@ -95,7 +97,7 @@ class WeatherManager { if (Math.random() < WeatherManager.FOG_CHANCE) { this.weatherState.isFogEnabled = !this.weatherState.isFogEnabled this.weatherState.fogDensity = this.weatherState.isFogEnabled - ? Math.random() * 0.7 + 0.3 // 0.3-1.0 + ? Math.floor((Math.random() * 0.7 + 0.3) * 100) // Convert 0.3-1.0 to 30-100 : 0 } } @@ -106,20 +108,21 @@ class WeatherManager { private async saveWeather(): Promise { try { - const promises = [ - await setJsonValue(this.getWorldFilePath(), 'isRainEnabled', this.weatherState.isRainEnabled), - await setJsonValue(this.getWorldFilePath(), 'rainPercentage', this.weatherState.rainPercentage), - await setJsonValue(this.getWorldFilePath(), 'isFogEnabled', this.weatherState.isFogEnabled), - await setJsonValue(this.getWorldFilePath(), 'fogDensity', this.weatherState.fogDensity) - ] - await Promise.all(promises) + 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)}`) } } - private getWorldFilePath(): string { - return getRootPath('data', 'world.json') + public cleanup(): void { + if (this.intervalId) { + clearInterval(this.intervalId) + } } } diff --git a/src/managers/zoneManager.ts b/src/managers/zoneManager.ts index 85f7dce..f4372ea 100644 --- a/src/managers/zoneManager.ts +++ b/src/managers/zoneManager.ts @@ -3,53 +3,53 @@ import ZoneRepository from '../repositories/zoneRepository' import ZoneService from '../services/zoneService' import LoadedZone from '../models/loadedZone' import { gameLogger } from '../utilities/logger' +import ZoneCharacter from '../models/zoneCharacter' class ZoneManager { - private loadedZones: LoadedZone[] = [] + private readonly zones = new Map() - // Method to initialize zoneEditor manager - public async boot() { + public async boot(): Promise { + // Create first zone if it doesn't exist if (!(await ZoneRepository.getById(1))) { - const zoneService = new ZoneService() - await zoneService.createDemoZone() + await new ZoneService().createDemoZone() } const zones = await ZoneRepository.getAll() + await Promise.all(zones.map((zone) => this.loadZone(zone))) - for (const zone of zones) { - await this.loadZone(zone) - } - - gameLogger.info('Zone manager loaded') + gameLogger.info(`Zone manager loaded with ${this.zones.size} zones`) } - // Method to handle individual zoneEditor loading - public async loadZone(zone: Zone) { + public async loadZone(zone: Zone): Promise { const loadedZone = new LoadedZone(zone) - this.loadedZones.push(loadedZone) + this.zones.set(zone.id, loadedZone) gameLogger.info(`Zone ID ${zone.id} loaded`) } - // Method to handle individual zoneEditor unloading - public unloadZone(zoneId: number) { - this.loadedZones = this.loadedZones.filter((loadedZone) => loadedZone.getZone().id !== zoneId) + public unloadZone(zoneId: number): void { + this.zones.delete(zoneId) gameLogger.info(`Zone ID ${zoneId} unloaded`) } - // Getter for loaded zones public getLoadedZones(): LoadedZone[] { - return this.loadedZones + return Array.from(this.zones.values()) } - // Getter for zone by id public getZoneById(zoneId: number): LoadedZone | undefined { - return this.loadedZones.find((loadedZone) => loadedZone.getZone().id === zoneId) + return this.zones.get(zoneId) } -} -export interface ZoneAssets { - tiles: string[] - objects: string[] + public getCharacter(characterId: number): ZoneCharacter | undefined { + for (const zone of this.zones.values()) { + const character = zone.getCharactersInZone().find((char) => char.character.id === characterId) + if (character) return character + } + return undefined + } + + public removeCharacter(characterId: number): void { + this.zones.forEach((zone) => zone.removeCharacter(characterId)) + } } export default new ZoneManager() diff --git a/src/middleware/authentication.ts b/src/middleware/authentication.ts index dd2270c..3f607d0 100644 --- a/src/middleware/authentication.ts +++ b/src/middleware/authentication.ts @@ -37,7 +37,7 @@ export async function Authentication(socket: TSocket, next: any) { return next(new Error('Authentication error')) } - socket.user = (await UserRepository.getById(decoded.id)) as User + socket.userId = decoded.id next() }) } else { diff --git a/src/models/loadedZone.ts b/src/models/loadedZone.ts index a28dd8c..2f72add 100644 --- a/src/models/loadedZone.ts +++ b/src/models/loadedZone.ts @@ -1,9 +1,10 @@ -import { Zone } from '@prisma/client' +import { Character, Zone } from '@prisma/client' import zoneRepository from '../repositories/zoneRepository' +import ZoneCharacter from './zoneCharacter' class LoadedZone { private readonly zone: Zone - // private readonly npcs: ZoneNPC[] = [] + private characters: ZoneCharacter[] = [] constructor(zone: Zone) { this.zone = zone @@ -13,6 +14,27 @@ class LoadedZone { return this.zone } + public addCharacter(character: Character) { + const zoneCharacter = new ZoneCharacter(character) + this.characters.push(zoneCharacter) + } + + public async removeCharacter(id: number) { + const zoneCharacter = this.getCharacterById(id) + if (zoneCharacter) { + await zoneCharacter.savePosition() + this.characters = this.characters.filter((c) => c.character.id !== id) + } + } + + public getCharacterById(id: number): ZoneCharacter | undefined { + return this.characters.find((c) => c.character.id === id) + } + + public getCharactersInZone(): ZoneCharacter[] { + return this.characters + } + public async getGrid(): Promise { let grid: number[][] = Array.from({ length: this.zone.height }, () => Array.from({ length: this.zone.width }, () => 0)) @@ -27,20 +49,6 @@ class LoadedZone { return grid } - - /** - * @TODO: Implement this - * @param position - */ - public async isPositionWalkable(position: { x: number; y: number }): Promise { - 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 diff --git a/src/models/zoneCharacter.ts b/src/models/zoneCharacter.ts new file mode 100644 index 0000000..43ce094 --- /dev/null +++ b/src/models/zoneCharacter.ts @@ -0,0 +1,25 @@ +import { Character } from '@prisma/client' +import prisma from '../utilities/prisma' + +class ZoneCharacter { + public readonly character: Character + public isMoving: boolean = false + + constructor(character: Character) { + this.character = character + } + + public async savePosition() { + await prisma.character.update({ + where: { id: this.character.id }, + data: { + positionX: this.character.positionX, + positionY: this.character.positionY, + rotation: this.character.rotation, + zoneId: this.character.zoneId + } + }) + } +} + +export default ZoneCharacter diff --git a/src/repositories/characterRepository.ts b/src/repositories/characterRepository.ts index 6e851a4..500b167 100644 --- a/src/repositories/characterRepository.ts +++ b/src/repositories/characterRepository.ts @@ -1,5 +1,6 @@ import prisma from '../utilities/prisma' // Import the global Prisma instance import { Character } from '@prisma/client' +import { appLogger } from '../utilities/logger' class CharacterRepository { async getByUserId(userId: number): Promise { @@ -19,7 +20,8 @@ class CharacterRepository { }) } catch (error: any) { // Handle error - throw new Error(`Failed to get character by user ID: ${error.message}`) + appLogger.error(`Failed to get character by user ID: ${error instanceof Error ? error.message : String(error)}`) + return null } } @@ -41,7 +43,8 @@ class CharacterRepository { }) } catch (error: any) { // Handle error - throw new Error(`Failed to get character by user ID and character ID: ${error.message}`) + appLogger.error(`Failed to get character by user ID and character ID: ${error instanceof Error ? error.message : String(error)}`) + return null } } @@ -62,7 +65,8 @@ class CharacterRepository { }) } catch (error: any) { // Handle error - throw new Error(`Failed to get character by ID: ${error.message}`) + appLogger.error(`Failed to get character by ID: ${error instanceof Error ? error.message : String(error)}`) + return null } } @@ -79,7 +83,8 @@ class CharacterRepository { }) } catch (error: any) { // Handle error - throw new Error(`Failed to update character: ${error.message}`) + appLogger.error(`Failed to update character: ${error instanceof Error ? error.message : String(error)}`) + return null } } @@ -93,7 +98,8 @@ class CharacterRepository { }) } catch (error: any) { // Handle error - throw new Error(`Failed to delete character by user ID and character ID: ${error.message}`) + appLogger.error(`Failed to delete character by user ID and character ID: ${error instanceof Error ? error.message : String(error)}`) + return null } } @@ -114,7 +120,8 @@ class CharacterRepository { }) } catch (error: any) { // Handle error - throw new Error(`Failed to get character by name: ${error.message}`) + appLogger.error(`Failed to get character by name: ${error instanceof Error ? error.message : String(error)}`) + return null } } } diff --git a/src/repositories/passwordResetTokenRepository.ts b/src/repositories/passwordResetTokenRepository.ts index cd0e77c..78037d6 100644 --- a/src/repositories/passwordResetTokenRepository.ts +++ b/src/repositories/passwordResetTokenRepository.ts @@ -1,4 +1,5 @@ -import prisma from '../utilities/prisma' // Import the global Prisma instance +import prisma from '../utilities/prisma' +import { appLogger } from '../utilities/logger' // Import the global Prisma instance class PasswordResetTokenRepository { async getById(id: number): Promise { @@ -10,7 +11,7 @@ class PasswordResetTokenRepository { }) } catch (error: any) { // Handle error - throw new Error(`Failed to get password reset token by ID: ${error.message}`) + appLogger.error(`Failed to get password reset token by ID: ${error instanceof Error ? error.message : String(error)}`) } } @@ -23,7 +24,7 @@ class PasswordResetTokenRepository { }) } catch (error: any) { // Handle error - throw new Error(`Failed to get password reset token by user ID: ${error.message}`) + appLogger.error(`Failed to get password reset token by user ID: ${error instanceof Error ? error.message : String(error)}`) } } @@ -36,7 +37,7 @@ class PasswordResetTokenRepository { }) } catch (error: any) { // Handle error - throw new Error(`Failed to get password reset token by token: ${error.message}`) + appLogger.error(`Failed to get password reset token by token: ${error instanceof Error ? error.message : String(error)}`) } } } diff --git a/src/repositories/userRepository.ts b/src/repositories/userRepository.ts index 03b9faf..a11986c 100644 --- a/src/repositories/userRepository.ts +++ b/src/repositories/userRepository.ts @@ -1,5 +1,6 @@ import prisma from '../utilities/prisma' // Import the global Prisma instance import { User } from '@prisma/client' +import { appLogger } from '../utilities/logger' class UserRepository { async getById(id: number): Promise { @@ -11,7 +12,8 @@ class UserRepository { }) } catch (error: any) { // Handle error - throw new Error(`Failed to get user by ID: ${error.message}`) + appLogger.error(`Failed to get user by ID: ${error instanceof Error ? error.message : String(error)}`) + return null } } @@ -24,7 +26,8 @@ class UserRepository { }) } catch (error: any) { // Handle error - throw new Error(`Failed to get user by username: ${error.message}`) + appLogger.error(`Failed to get user by username: ${error instanceof Error ? error.message : String(error)}`) + return null } } @@ -37,7 +40,8 @@ class UserRepository { }) } catch (error: any) { // Handle error - throw new Error(`Failed to get user by email: ${error.message}`) + appLogger.error(`Failed to get user by email: ${error instanceof Error ? error.message : String(error)}`) + return null } } } diff --git a/src/repositories/worldRepository.ts b/src/repositories/worldRepository.ts new file mode 100644 index 0000000..b5648f8 --- /dev/null +++ b/src/repositories/worldRepository.ts @@ -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 { + 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() diff --git a/src/repositories/zoneRepository.ts b/src/repositories/zoneRepository.ts index 33e55ca..22da933 100644 --- a/src/repositories/zoneRepository.ts +++ b/src/repositories/zoneRepository.ts @@ -1,20 +1,9 @@ import { Zone, ZoneEventTile, ZoneEventTileType, ZoneObject } from '@prisma/client' import prisma from '../utilities/prisma' -import { ZoneEventTileWithTeleport } from '../socketEvents/zone/characterMove' +import { ZoneEventTileWithTeleport } from '../utilities/types' import { appLogger } from '../utilities/logger' -import { AssetData } from '../utilities/types' -import tileRepository from './tileRepository' class ZoneRepository { - async getFirst(): Promise { - try { - return await prisma.zone.findFirst() - } catch (error: any) { - appLogger.error(`Failed to get first zone: ${error.message}`) - return null - } - } - async getAll(): Promise { try { return await prisma.zone.findMany() diff --git a/src/server.ts b/src/server.ts index a8e516a..a1d3a9e 100644 --- a/src/server.ts +++ b/src/server.ts @@ -13,7 +13,6 @@ import { appLogger, watchLogs } from './utilities/logger' import ZoneManager from './managers/zoneManager' import UserManager from './managers/userManager' import CommandManager from './managers/commandManager' -import CharacterManager from './managers/characterManager' import QueueManager from './managers/queueManager' import DateManager from './managers/dateManager' import WeatherManager from './managers/weatherManager' @@ -30,7 +29,7 @@ export class Server { this.app = express() this.app.use( cors({ - origin: config.CLIENT_URL + origin: config.CLIENT_URL // Allow CORS from the client URL }) ) this.app.use(express.json()) @@ -81,9 +80,6 @@ export class Server { // Load zoneEditor manager await ZoneManager.boot() - // Load character manager - await CharacterManager.boot() - // Load command manager await CommandManager.boot(this.io) diff --git a/src/services/character/characterMoveService.ts b/src/services/character/characterMoveService.ts index 2dd47cb..0ef7988 100644 --- a/src/services/character/characterMoveService.ts +++ b/src/services/character/characterMoveService.ts @@ -1,43 +1,57 @@ -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' +import { Character } from '@prisma/client' + +interface Position { + x: number + y: number +} export class CharacterMoveService { - public updatePosition(character: ExtendedCharacter, position: { x: number; y: number }, newZoneId?: number) { + private static readonly MOVEMENT_DELAY_MS = 250 + + 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 + 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 | null> { - const grid = await ZoneManager.getZoneById(character.zoneId)?.getGrid() + public async calculatePath(character: Character, targetX: number, targetY: number): Promise { + 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 = { x: Math.floor(character.positionX), y: Math.floor(character.positionY) } - const end = { x: Math.floor(targetX), y: Math.floor(targetY) } + 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 { - await new Promise((resolve) => setTimeout(resolve, 250)) // 250ms delay between steps + await new Promise((resolve) => setTimeout(resolve, CharacterMoveService.MOVEMENT_DELAY_MS)) + } + + private isValidPosition(position: Position): boolean { + return Number.isFinite(position.x) && Number.isFinite(position.y) && position.x >= 0 && position.y >= 0 } } diff --git a/src/services/worldService.ts b/src/services/worldService.ts new file mode 100644 index 0000000..afe0f2e --- /dev/null +++ b/src/services/worldService.ts @@ -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): Promise { + 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() diff --git a/src/services/zoneEventTileService.ts b/src/services/zoneEventTileService.ts index 3ff0b07..89eca78 100644 --- a/src/services/zoneEventTileService.ts +++ b/src/services/zoneEventTileService.ts @@ -3,7 +3,7 @@ import prisma from '../utilities/prisma' import ZoneRepository from '../repositories/zoneRepository' import { ZoneEventTileTeleport } from '@prisma/client' import { Server } from 'socket.io' -import CharacterManager from '../managers/characterManager' +import ZoneManager from '../managers/zoneManager' export class ZoneEventTileService { public async handleTeleport(io: Server, socket: TSocket, character: ExtendedCharacter, teleport: ZoneEventTileTeleport): Promise { @@ -12,8 +12,6 @@ export class ZoneEventTileService { const zone = await ZoneRepository.getById(teleport.toZoneId) if (!zone) return - // CharacterManager.moveCharacterBetweenZones(character, zone) - const oldZoneId = character.zoneId const newZoneId = teleport.toZoneId @@ -46,7 +44,7 @@ export class ZoneEventTileService { // Send teleport information to the client socket.emit('zone:character:teleport', { zone, - characters: CharacterManager.getCharactersInZone(zone) + characters: ZoneManager.getZoneById(zone.id)?.getCharactersInZone() }) } } diff --git a/src/services/zoneService.ts b/src/services/zoneService.ts index d9abd09..8a46fce 100644 --- a/src/services/zoneService.ts +++ b/src/services/zoneService.ts @@ -1,84 +1,38 @@ import prisma from '../utilities/prisma' -import { AssetData } from '../utilities/types' -import tileRepository from '../repositories/tileRepository' -import zoneRepository from '../repositories/zoneRepository' -import { Object, Zone, ZoneObject } from '@prisma/client' - -type getZoneAsetsZoneType = Zone & { - zoneObjects: (ZoneObject & { - object: Object - })[] -} +import { gameLogger } from '../utilities/logger' class ZoneService { async createDemoZone(): Promise { - const tiles = [ - ['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'], - ['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'], - ['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'], - ['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'], - ['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'], - ['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'], - ['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'], - ['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'], - ['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'], - ['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'] - ] + try { + const tiles = [ + ['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'], + ['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'], + ['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'], + ['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'], + ['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'], + ['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'], + ['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'], + ['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'], + ['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'], + ['blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile', 'blank_tile'] + ] - await prisma.zone.create({ - data: { - name: 'Demo zone', - width: 10, - height: 10, - tiles - } - }) + await prisma.zone.create({ + data: { + name: 'Demo zone', + width: 10, + height: 10, + tiles + } + }) - console.log('Demo zone created.') - return true - } + gameLogger.info('Demo zone created.') - async getZoneAssets(zone: getZoneAsetsZoneType): Promise { - const assets: AssetData[] = [] - - // zone.tiles is prisma jsonvalue - let tiles = JSON.parse(JSON.stringify(zone.tiles)) - tiles = [...new Set(tiles.flat())] - - // Add tile assets - for (const tile of tiles) { - const tileInfo = await tileRepository.getById(tile) - if (!tileInfo) continue - - assets.push({ - key: tileInfo.id, - data: '/assets/tiles/' + tileInfo.id + '.png', - group: 'tiles', - updatedAt: tileInfo?.updatedAt || new Date() - } as AssetData) + return true + } catch (error: any) { + gameLogger.error(`Failed to create demo zone: ${error instanceof Error ? error.message : String(error)}`) + return false } - - // Add object assets - for (const zoneObject of zone.zoneObjects) { - if (!zoneObject.object) continue - - assets.push({ - key: zoneObject.object.id, - data: '/assets/objects/' + zoneObject.object.id + '.png', - group: 'objects', - updatedAt: zoneObject.object.updatedAt || new Date() - } as AssetData) - } - - // Filter out duplicate assets - return assets.reduce((acc: AssetData[], current) => { - const x = acc.find((item) => item.key === current.key && item.group === current.group) - if (!x) { - return acc.concat([current]) - } else { - return acc - } - }, []) } } diff --git a/src/socketEvents/character/connect.ts b/src/socketEvents/character/connect.ts index 4a2e142..2beab2c 100644 --- a/src/socketEvents/character/connect.ts +++ b/src/socketEvents/character/connect.ts @@ -3,7 +3,7 @@ import { TSocket } from '../../utilities/types' import CharacterRepository from '../../repositories/characterRepository' type SocketResponseT = { - character_id: number + characterId: number } export default class CharacterConnectEvent { @@ -19,7 +19,7 @@ export default class CharacterConnectEvent { private async handleCharacterConnect(data: SocketResponseT): Promise { console.log('character:connect requested', data) try { - const character = await CharacterRepository.getByUserAndId(this.socket?.user?.id as number, data.character_id) + const character = await CharacterRepository.getByUserAndId(this.socket?.userId!, data.characterId!) if (!character) return this.socket.characterId = character.id diff --git a/src/socketEvents/character/create.ts b/src/socketEvents/character/create.ts index a7e1c92..d8abb6e 100644 --- a/src/socketEvents/character/create.ts +++ b/src/socketEvents/character/create.ts @@ -23,7 +23,7 @@ export default class CharacterCreateEvent { try { data = ZCharacterCreate.parse(data) - const user_id = this.socket.user?.id as number + const user_id = this.socket.userId! // Check if character name already exists const characterExists = await CharacterRepository.getByName(data.name) diff --git a/src/socketEvents/character/delete.ts b/src/socketEvents/character/delete.ts index 9b30b8c..1baab35 100644 --- a/src/socketEvents/character/delete.ts +++ b/src/socketEvents/character/delete.ts @@ -4,7 +4,7 @@ import { Character, Zone } from '@prisma/client' import CharacterRepository from '../../repositories/characterRepository' type TypePayload = { - character_id: number + characterId: number } type TypeResponse = { @@ -23,12 +23,10 @@ export default class CharacterDeleteEvent { } private async handleCharacterDelete(data: TypePayload, callback: (response: TypeResponse) => void): Promise { - // zod validate try { - await CharacterRepository.deleteByUserIdAndId(this.socket.user?.id as number, data.character_id as number) + await CharacterRepository.deleteByUserIdAndId(this.socket.userId!, data.characterId!) - const user_id = this.socket.user?.id as number - const characters: Character[] = (await CharacterRepository.getByUserId(user_id)) as Character[] + const characters: Character[] = (await CharacterRepository.getByUserId(this.socket.userId!)) as Character[] this.socket.emit('character:list', characters) } catch (error: any) { diff --git a/src/socketEvents/character/list.ts b/src/socketEvents/character/list.ts index 53e0722..5b342da 100644 --- a/src/socketEvents/character/list.ts +++ b/src/socketEvents/character/list.ts @@ -2,6 +2,7 @@ import { Socket, Server } from 'socket.io' import { TSocket } from '../../utilities/types' import { Character } from '@prisma/client' import CharacterRepository from '../../repositories/characterRepository' +import { gameLogger } from '../../utilities/logger' export default class CharacterListEvent { constructor( @@ -15,12 +16,10 @@ export default class CharacterListEvent { private async handleCharacterList(data: any): Promise { try { - console.log('character:list requested') - const user_id = this.socket.user?.id as number - const characters: Character[] = (await CharacterRepository.getByUserId(user_id)) as Character[] + const characters: Character[] = (await CharacterRepository.getByUserId(this.socket.userId!)) as Character[] this.socket.emit('character:list', characters) } catch (error: any) { - console.log('character:list error', error) + gameLogger.error('character:list error', error.message) } } } diff --git a/src/socketEvents/chat/gameMaster/alertCommand.ts b/src/socketEvents/chat/gameMaster/alertCommand.ts index ede3552..28921cf 100644 --- a/src/socketEvents/chat/gameMaster/alertCommand.ts +++ b/src/socketEvents/chat/gameMaster/alertCommand.ts @@ -25,7 +25,7 @@ export default class AlertCommandEvent { } // Check if character exists - const character = await CharacterRepository.getByUserAndId(this.socket.user?.id as number, this.socket.characterId as number) + const character = await CharacterRepository.getByUserAndId(this.socket.userId!, this.socket.characterId!) if (!character) { gameLogger.error('chat:alert_command error', 'Character not found') callback(false) diff --git a/src/socketEvents/chat/gameMaster/setTimeCommand.ts b/src/socketEvents/chat/gameMaster/setTimeCommand.ts index 8a34eae..223aeae 100644 --- a/src/socketEvents/chat/gameMaster/setTimeCommand.ts +++ b/src/socketEvents/chat/gameMaster/setTimeCommand.ts @@ -26,7 +26,7 @@ export default class SetTimeCommand { } // Check if character exists - const character = await CharacterRepository.getByUserAndId(this.socket.user?.id as number, this.socket.characterId as number) + const character = await CharacterRepository.getByUserAndId(this.socket.userId!, this.socket.characterId!) if (!character) { gameLogger.error('chat:alert_command error', 'Character not found') callback(false) diff --git a/src/socketEvents/chat/gameMaster/teleportCommand.ts b/src/socketEvents/chat/gameMaster/teleportCommand.ts index 3e38be0..e459d95 100644 --- a/src/socketEvents/chat/gameMaster/teleportCommand.ts +++ b/src/socketEvents/chat/gameMaster/teleportCommand.ts @@ -1,10 +1,10 @@ import { Server } from 'socket.io' -import { ExtendedCharacter, TSocket } from '../../../utilities/types' +import { TSocket } from '../../../utilities/types' import { getArgs, isCommand } from '../../../utilities/chat' import ZoneRepository from '../../../repositories/zoneRepository' -import CharacterManager from '../../../managers/characterManager' import { gameLogger, gameMasterLogger } from '../../../utilities/logger' -import CharacterRepository from '../../../repositories/characterRepository' +import ZoneManager from '../../../managers/zoneManager' +import ZoneCharacter from '../../../models/zoneCharacter' type TypePayload = { message: string @@ -23,13 +23,15 @@ export default class TeleportCommandEvent { private async handleTeleportCommand(data: TypePayload, callback: (response: boolean) => void): Promise { try { // Check if character exists - const character = (await CharacterRepository.getByUserAndId(this.socket.user?.id as number, this.socket.characterId as number)) as ExtendedCharacter - if (!character) { - gameLogger.error('chat:alert_command error', 'Character not found') + const zoneCharacter = ZoneManager.getCharacter(this.socket.characterId!) + if (!zoneCharacter) { + gameLogger.error('chat:send_message error', 'Character not found') callback(false) 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.`) @@ -75,11 +77,11 @@ export default class TeleportCommandEvent { character.positionX = 0 character.positionY = 0 - character.resetMovement = true + zoneCharacter.isMoving = false this.socket.emit('zone:character:teleport', { zone, - characters: CharacterManager.getCharactersInZone(zone) + characters: ZoneManager.getZoneById(zone.id)?.getCharactersInZone() }) this.socket.emit('notification', { title: 'Server message', message: `You have been teleported to ${zone.name}` }) diff --git a/src/socketEvents/chat/gameMaster/toggleFogCommand.ts b/src/socketEvents/chat/gameMaster/toggleFogCommand.ts index f891843..8b0ffec 100644 --- a/src/socketEvents/chat/gameMaster/toggleFogCommand.ts +++ b/src/socketEvents/chat/gameMaster/toggleFogCommand.ts @@ -26,7 +26,7 @@ export default class ToggleFogCommand { } // Check if character exists - const character = await CharacterRepository.getByUserAndId(this.socket.user?.id as number, this.socket.characterId as number) + const character = await CharacterRepository.getByUserAndId(this.socket.userId!, this.socket.characterId!) if (!character) { gameLogger.error('chat:alert_command error', 'Character not found') callback(false) diff --git a/src/socketEvents/chat/gameMaster/toggleRainCommand.ts b/src/socketEvents/chat/gameMaster/toggleRainCommand.ts index 5599c07..3b6a856 100644 --- a/src/socketEvents/chat/gameMaster/toggleRainCommand.ts +++ b/src/socketEvents/chat/gameMaster/toggleRainCommand.ts @@ -26,7 +26,7 @@ export default class ToggleRainCommand { } // Check if character exists - const character = await CharacterRepository.getByUserAndId(this.socket.user?.id as number, this.socket.characterId as number) + const character = await CharacterRepository.getByUserAndId(this.socket.userId!, this.socket.characterId!) if (!character) { gameLogger.error('chat:alert_command error', 'Character not found') callback(false) diff --git a/src/socketEvents/chat/sendMessage.ts b/src/socketEvents/chat/sendMessage.ts index fb94f93..c4e0f1a 100644 --- a/src/socketEvents/chat/sendMessage.ts +++ b/src/socketEvents/chat/sendMessage.ts @@ -3,7 +3,7 @@ import { TSocket } from '../../utilities/types' import ZoneRepository from '../../repositories/zoneRepository' import { isCommand } from '../../utilities/chat' import { gameLogger } from '../../utilities/logger' -import CharacterManager from '../../managers/characterManager' +import ZoneManager from '../../managers/zoneManager' type TypePayload = { message: string @@ -26,13 +26,15 @@ export default class ChatMessageEvent { return } - const character = CharacterManager.getCharacterFromSocket(this.socket) - if (!character) { + const zoneCharacter = ZoneManager.getCharacter(this.socket.characterId!) + if (!zoneCharacter) { gameLogger.error('chat:send_message error', 'Character not found') callback(false) return } + const character = zoneCharacter.character + const zone = await ZoneRepository.getById(character.zoneId) if (!zone) { gameLogger.error('chat:send_message error', 'Zone not found') diff --git a/src/socketEvents/disconnect.ts b/src/socketEvents/disconnect.ts index 4bd5acf..8202e35 100644 --- a/src/socketEvents/disconnect.ts +++ b/src/socketEvents/disconnect.ts @@ -1,7 +1,7 @@ import { Server } from 'socket.io' import { TSocket } from '../utilities/types' -import CharacterManager from '../managers/characterManager' import { gameLogger } from '../utilities/logger' +import ZoneManager from '../managers/zoneManager' export default class DisconnectEvent { constructor( @@ -10,31 +10,34 @@ export default class DisconnectEvent { ) {} public listen(): void { - this.socket.on('disconnect', this.handleDisconnect.bind(this)) + this.socket.on('disconnect', this.handleEvent.bind(this)) } - private async handleDisconnect(data: any): Promise { + private async handleEvent(data: any): Promise { try { - if (!this.socket.user) { + if (!this.socket.userId) { gameLogger.info('User disconnected but had no user set') return } - this.io.emit('user:disconnect', this.socket.user.id) + this.io.emit('user:disconnect', this.socket.userId) - const character = CharacterManager.getCharacterFromSocket(this.socket) - - if (!character) { + const zoneCharacter = ZoneManager.getCharacter(this.socket.characterId!) + if (!zoneCharacter) { gameLogger.info('User disconnected but had no character set') return } - character.resetMovement = true + const character = zoneCharacter.character + + // Save character position and remove from zone + zoneCharacter.isMoving = false + await zoneCharacter.savePosition() + ZoneManager.removeCharacter(this.socket.characterId!) gameLogger.info('User disconnected along with their character') - await CharacterManager.removeCharacter(character) - + // Inform other clients that the character has left this.io.in(character.zoneId.toString()).emit('zone:character:leave', character.id) this.io.emit('character:disconnect', character.id) } catch (error: any) { diff --git a/src/socketEvents/gameMaster/assetManager/sprite/create.ts b/src/socketEvents/gameMaster/assetManager/sprite/create.ts index b7447a7..29fb126 100644 --- a/src/socketEvents/gameMaster/assetManager/sprite/create.ts +++ b/src/socketEvents/gameMaster/assetManager/sprite/create.ts @@ -17,7 +17,7 @@ export default class SpriteCreateEvent { private async handleSpriteCreate(data: undefined, callback: (response: boolean) => void): Promise { try { - const character = await characterRepository.getById(this.socket.characterId as number) + const character = await characterRepository.getById(this.socket.characterId!) if (!character) return callback(false) if (character.role !== 'gm') { diff --git a/src/socketEvents/gameMaster/assetManager/sprite/delete.ts b/src/socketEvents/gameMaster/assetManager/sprite/delete.ts index a97c2e3..9bfc747 100644 --- a/src/socketEvents/gameMaster/assetManager/sprite/delete.ts +++ b/src/socketEvents/gameMaster/assetManager/sprite/delete.ts @@ -2,9 +2,9 @@ import { Server } from 'socket.io' import { TSocket } from '../../../../utilities/types' import fs from 'fs' import prisma from '../../../../utilities/prisma' -import CharacterManager from '../../../../managers/characterManager' import { gameMasterLogger } from '../../../../utilities/logger' import { getPublicPath } from '../../../../utilities/storage' +import CharacterRepository from '../../../../repositories/characterRepository' type Payload = { id: string @@ -25,7 +25,7 @@ export default class GMSpriteDeleteEvent { } private async handleSpriteDelete(data: Payload, callback: (response: boolean) => void): Promise { - const character = CharacterManager.getCharacterFromSocket(this.socket) + const character = await CharacterRepository.getById(this.socket.characterId!) if (character?.role !== 'gm') { return callback(false) } diff --git a/src/socketEvents/gameMaster/assetManager/sprite/list.ts b/src/socketEvents/gameMaster/assetManager/sprite/list.ts index 6cf423c..a504494 100644 --- a/src/socketEvents/gameMaster/assetManager/sprite/list.ts +++ b/src/socketEvents/gameMaster/assetManager/sprite/list.ts @@ -17,7 +17,7 @@ export default class SpriteListEvent { } private async handleSpriteList(data: any, callback: (response: Sprite[]) => void): Promise { - const character = await characterRepository.getById(this.socket.characterId as number) + const character = await characterRepository.getById(this.socket.characterId!) if (!character) return callback([]) if (character.role !== 'gm') { diff --git a/src/socketEvents/gameMaster/assetManager/sprite/update.ts b/src/socketEvents/gameMaster/assetManager/sprite/update.ts index 9800fe9..3e73acd 100644 --- a/src/socketEvents/gameMaster/assetManager/sprite/update.ts +++ b/src/socketEvents/gameMaster/assetManager/sprite/update.ts @@ -4,8 +4,8 @@ import prisma from '../../../../utilities/prisma' import type { Prisma, SpriteAction } from '@prisma/client' import { writeFile, mkdir } from 'node:fs/promises' import sharp from 'sharp' -import CharacterManager from '../../../../managers/characterManager' import { getPublicPath } from '../../../../utilities/storage' +import CharacterRepository from '../../../../repositories/characterRepository' type SpriteActionInput = Omit & { sprites: string[] @@ -38,7 +38,7 @@ export default class SpriteUpdateEvent { } private async handleSpriteUpdate(data: Payload, callback: (success: boolean) => void): Promise { - const character = CharacterManager.getCharacterFromSocket(this.socket) + const character = await CharacterRepository.getById(this.socket.characterId!) if (character?.role !== 'gm') { return callback(false) } diff --git a/src/socketEvents/login.ts b/src/socketEvents/login.ts index e0cf7e1..79fbfd1 100644 --- a/src/socketEvents/login.ts +++ b/src/socketEvents/login.ts @@ -1,6 +1,7 @@ import { Server } from 'socket.io' import { TSocket } from '../utilities/types' import { gameLogger } from '../utilities/logger' +import UserRepository from '../repositories/userRepository' export default class LoginEvent { constructor( @@ -14,13 +15,13 @@ export default class LoginEvent { private handleLogin(): void { try { - if (!this.socket.user) { + if (!this.socket.userId) { gameLogger.warn('Login attempt without user data') return } - this.socket.emit('logged_in', { user: this.socket.user }) - gameLogger.info(`User logged in: ${this.socket.user.id}`) + this.socket.emit('logged_in', { user: UserRepository.getById(this.socket.userId) }) + gameLogger.info(`User logged in: ${this.socket.userId}`) } catch (error: any) { gameLogger.error('login error', error.message) } diff --git a/src/socketEvents/zone/characterJoin.ts b/src/socketEvents/zone/characterJoin.ts index 24b1d14..fe9e2ad 100644 --- a/src/socketEvents/zone/characterJoin.ts +++ b/src/socketEvents/zone/characterJoin.ts @@ -1,14 +1,16 @@ import { Server } from 'socket.io' -import { ExtendedCharacter, TSocket } from '../../utilities/types' +import { TSocket } from '../../utilities/types' import ZoneRepository from '../../repositories/zoneRepository' -import { Character, Zone } from '@prisma/client' -import CharacterManager from '../../managers/characterManager' +import { Zone } from '@prisma/client' import { gameLogger } from '../../utilities/logger' import CharacterRepository from '../../repositories/characterRepository' +import ZoneManager from '../../managers/zoneManager' +import zoneCharacter from '../../models/zoneCharacter' +import zoneManager from '../../managers/zoneManager' interface IResponse { zone: Zone - characters: Character[] + characters: zoneCharacter[] } export default class CharacterJoinEvent { @@ -28,30 +30,42 @@ export default class CharacterJoinEvent { return } - const character = await CharacterRepository.getById(this.socket.characterId as number) + const character = await CharacterRepository.getById(this.socket.characterId) if (!character) { gameLogger.error('zone:character:join error', 'Character not found') return } + /** + * @TODO: If zone is not found, spawn back to the start + */ const zone = await ZoneRepository.getById(character.zoneId) if (!zone) { gameLogger.error('zone:character:join error', 'Zone not found') return } - CharacterManager.initCharacter(character as ExtendedCharacter) + /** + * @TODO: If zone is not found, spawn back to the start + */ + const loadedZone = ZoneManager.getZoneById(zone.id) + if (!loadedZone) { + gameLogger.error('zone:character:join error', 'Loaded zone not found') + return + } + + loadedZone.addCharacter(character) this.socket.join(zone.id.toString()) - // let other clients know of new character - this.io.to(zone.id.toString()).emit('zone:character:join', character) + // Let other clients know of new character + this.io.to(zone.id.toString()).emit('zone:character:join', zoneManager.getCharacter(character.id)) // Log gameLogger.info(`User ${character.id} joined zone ${zone.id}`) - // send over zone and characters to socket - callback({ zone, characters: CharacterManager.getCharactersInZone(zone) }) + // Send over zone and characters to socket + callback({ zone, characters: loadedZone.getCharactersInZone() }) } catch (error: any) { gameLogger.error('zone:character:join error', error.message) this.socket.disconnect() diff --git a/src/socketEvents/zone/characterLeave.ts b/src/socketEvents/zone/characterLeave.ts index 565f904..89802be 100644 --- a/src/socketEvents/zone/characterLeave.ts +++ b/src/socketEvents/zone/characterLeave.ts @@ -1,8 +1,9 @@ import { Server } from 'socket.io' import { TSocket } from '../../utilities/types' import ZoneRepository from '../../repositories/zoneRepository' -import CharacterManager from '../../managers/characterManager' import { gameLogger } from '../../utilities/logger' +import ZoneManager from '../../managers/zoneManager' +import CharacterRepository from '../../repositories/characterRepository' export default class ZoneLeaveEvent { constructor( @@ -16,31 +17,41 @@ export default class ZoneLeaveEvent { private async handleZoneLeave(): Promise { try { - const character = CharacterManager.getCharacterFromSocket(this.socket) + if (!this.socket.characterId) { + gameLogger.error('zone:character:join error', 'Zone requested but no character id set') + return + } + + const character = await CharacterRepository.getById(this.socket.characterId) if (!character) { - gameLogger.error('zone:character:leave error', 'Character not found') - return - } - - if (!character.zoneId) { - gameLogger.error('zone:character:leave error', 'Character not in a zone') + gameLogger.error('zone:character:join error', 'Character not found') return } + /** + * @TODO: If zone is not found, spawn back to the start + */ const zone = await ZoneRepository.getById(character.zoneId) - if (!zone) { - gameLogger.error('zone:character:leave error', 'Zone not found') + gameLogger.error('zone:character:join error', 'Zone not found') return } + const loadedZone = ZoneManager.getZoneById(zone.id) + if (!loadedZone) { + gameLogger.error('zone:character:join error', 'Loaded zone not found') + return + } + + console.log('awee') + this.socket.leave(zone.id.toString()) // let other clients know of character leaving this.io.to(zone.id.toString()).emit('zone:character:leave', character.id) // remove character from zone manager - await CharacterManager.removeCharacter(character) + await loadedZone.removeCharacter(character.id) gameLogger.info('zone:character:leave', `Character ${character.id} left zone ${zone.id}`) } catch (error: any) { diff --git a/src/socketEvents/zone/characterMove.ts b/src/socketEvents/zone/characterMove.ts index 2a98463..aa31830 100644 --- a/src/socketEvents/zone/characterMove.ts +++ b/src/socketEvents/zone/characterMove.ts @@ -1,77 +1,53 @@ import { Server } from 'socket.io' -import { TSocket, ExtendedCharacter } from '../../utilities/types' +import { TSocket, ZoneEventTileWithTeleport } from '../../utilities/types' import { CharacterMoveService } from '../../services/character/characterMoveService' import { ZoneEventTileService } from '../../services/zoneEventTileService' import prisma from '../../utilities/prisma' -import { ZoneEventTile, ZoneEventTileTeleport } from '@prisma/client' import Rotation from '../../utilities/character/rotation' -import CharacterManager from '../../managers/characterManager' import { gameLogger } from '../../utilities/logger' -import QueueManager from '../../managers/queueManager' - -export type ZoneEventTileWithTeleport = ZoneEventTile & { - teleport: ZoneEventTileTeleport -} +import ZoneManager from '../../managers/zoneManager' +import ZoneCharacter from '../../models/zoneCharacter' export default class CharacterMove { - private characterMoveService: CharacterMoveService - private zoneEventTileService: ZoneEventTileService - private nextPath: { [index: number]: { x: number; y: number }[] } = [] - private currentZoneId: { [index: number]: number } = [] + private readonly characterMoveService = new CharacterMoveService() + private readonly zoneEventTileService = new ZoneEventTileService() + private nextPath = new Map() constructor( private readonly io: Server, private readonly socket: TSocket - ) { - this.characterMoveService = new CharacterMoveService() - this.zoneEventTileService = new ZoneEventTileService() - } + ) {} public listen(): void { - this.socket.on('character:initMove', this.handleCharacterMove.bind(this)) + this.socket.on('character:move', this.handleCharacterMove.bind(this)) } private async handleCharacterMove({ positionX, positionY }: { positionX: number; positionY: number }): Promise { - let character = CharacterManager.getCharacterFromSocket(this.socket) - if (!character) { - gameLogger.error('character:move error', 'Character not found') + const zoneCharacter = ZoneManager.getCharacter(this.socket.characterId!) + if (!zoneCharacter?.character) { + gameLogger.error('character:move error', 'Character not found or not initialized') return } - if (!character) { - gameLogger.error('character:move error', 'character has not been initialized?') - return - } - - const path = await this.characterMoveService.calculatePath(character, positionX, positionY) + const path = await this.characterMoveService.calculatePath(zoneCharacter.character, positionX, positionY) if (!path) { - this.io.in(character.zoneId.toString()).emit('character:moveError', 'No valid path found') + this.io.in(zoneCharacter.character.zoneId.toString()).emit('character:moveError', 'No valid path found') return } - if (!character.isMoving && character.resetMovement) { - character.resetMovement = false - } - if (character.isMoving && !character.resetMovement) { - character.resetMovement = true - this.nextPath[character.id] = path - } - if (!character.isMoving && !character.resetMovement) { - character.isMoving = true - this.currentZoneId[character.id] = character.zoneId - await this.moveAlongPath(character, path) + if (!zoneCharacter.isMoving) { + zoneCharacter.isMoving = true + await this.moveAlongPath(zoneCharacter, path) + } else { + this.nextPath.set(zoneCharacter.character.id, path) } } - private async moveAlongPath(character: ExtendedCharacter, path: Array<{ x: number; y: number }>): Promise { + private async moveAlongPath(zoneCharacter: ZoneCharacter, path: Array<{ x: number; y: number }>): Promise { + const { character } = zoneCharacter + for (let i = 0; i < path.length - 1; i++) { - const start = path[i] - const end = path[i + 1] - - if (CharacterManager.hasResetMovement(character)) { - break - } - + const [start, end] = [path[i], path[i + 1]] character.rotation = Rotation.calculate(start.x, start.y, end.x, end.y) const zoneEventTile = await prisma.zoneEventTile.findFirst({ @@ -79,62 +55,44 @@ export default class CharacterMove { zoneId: character.zoneId, positionX: Math.floor(end.x), positionY: Math.floor(end.y) - } + }, + include: { teleport: true } }) - 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 - } - } + if (zoneEventTile?.type === 'BLOCK') break + if (zoneEventTile?.type === 'TELEPORT' && zoneEventTile.teleport) { + await this.handleZoneEventTile(zoneEventTile as ZoneEventTileWithTeleport) + break } this.characterMoveService.updatePosition(character, end) - this.io.in(character.zoneId.toString()).emit('character:move', character) - + this.io.in(character.zoneId.toString()).emit('character:move', zoneCharacter) await this.characterMoveService.applyMovementDelay() } - if (CharacterManager.hasResetMovement(character)) { - character.resetMovement = false - if (this.currentZoneId[character.id] === character.zoneId) { - await this.moveAlongPath(character, this.nextPath[character.id]) - } else { - delete this.currentZoneId[character.id] - character.isMoving = false - } + const nextPath = this.nextPath.get(character.id) + if (nextPath) { + this.nextPath.delete(character.id) + await this.moveAlongPath(zoneCharacter, nextPath) } else { - this.finalizeMovement(character) + this.finalizeMovement(zoneCharacter) } } private async handleZoneEventTile(zoneEventTile: ZoneEventTileWithTeleport): Promise { - const character = CharacterManager.getCharacterFromSocket(this.socket) - if (!character) { + const zoneCharacter = ZoneManager.getCharacter(this.socket.characterId!) + if (!zoneCharacter) { gameLogger.error('character:move error', 'Character not found') return } - const teleport = zoneEventTile.teleport - if (teleport) { - await this.zoneEventTileService.handleTeleport(this.io, this.socket, character, teleport) - return + if (zoneEventTile.teleport) { + await this.zoneEventTileService.handleTeleport(this.io, this.socket, zoneCharacter.character, zoneEventTile.teleport) } } - private finalizeMovement(character: ExtendedCharacter): void { - character.isMoving = false - this.io.in(character.zoneId.toString()).emit('character:move', character) + private finalizeMovement(zoneCharacter: ZoneCharacter): void { + zoneCharacter.isMoving = false + this.io.in(zoneCharacter.character.zoneId.toString()).emit('character:move', zoneCharacter) } } diff --git a/src/socketEvents/weather.ts b/src/socketEvents/zone/weather.ts similarity index 75% rename from src/socketEvents/weather.ts rename to src/socketEvents/zone/weather.ts index 444c7bc..43df57e 100644 --- a/src/socketEvents/weather.ts +++ b/src/socketEvents/zone/weather.ts @@ -1,7 +1,7 @@ import { Server } from 'socket.io' -import { TSocket } from '../utilities/types' -import { gameLogger } from '../utilities/logger' -import WeatherManager from '../managers/weatherManager' +import { TSocket } from '../../utilities/types' +import { gameLogger } from '../../utilities/logger' +import WeatherManager from '../../managers/weatherManager' export default class Weather { constructor( diff --git a/src/utilities/json.ts b/src/utilities/json.ts deleted file mode 100644 index 9440bd7..0000000 --- a/src/utilities/json.ts +++ /dev/null @@ -1,49 +0,0 @@ -import * as fs from 'fs/promises' -import { appLogger } from './logger' - -export async function readJsonFile(filePath: string): Promise { - try { - const fileContent = await fs.readFile(filePath, 'utf-8') - return JSON.parse(fileContent) as T - } catch (error) { - appLogger.error(`Error reading JSON file: ${error instanceof Error ? error.message : String(error)}`) - throw error - } -} - -export async function writeJsonFile(filePath: string, data: T): Promise { - try { - const jsonString = JSON.stringify(data, null, 2) - await fs.writeFile(filePath, jsonString, 'utf-8') - } catch (error) { - appLogger.error(`Error writing JSON file: ${error instanceof Error ? error.message : String(error)}`) - throw error - } -} - -export async function readJsonValue(filePath: string, paramPath: string): Promise { - try { - const jsonContent = await readJsonFile(filePath) - const paramValue = paramPath.split('.').reduce((obj, key) => obj && obj[key], jsonContent) - - if (paramValue === undefined) { - throw new Error(`Parameter ${paramPath} not found in the JSON file`) - } - - return paramValue as T - } catch (error) { - appLogger.error(`Error reading JSON parameter: ${error instanceof Error ? error.message : String(error)}`) - throw error - } -} - -export async function setJsonValue(filePath: string, key: string, value: any): Promise { - try { - const data = await readJsonFile(filePath) - const updatedData = { ...data, [key]: value } - await writeJsonFile(filePath, updatedData) - } catch (error) { - appLogger.error(`Error setting JSON value: ${error instanceof Error ? error.message : String(error)}`) - throw error - } -} diff --git a/src/utilities/logger.ts b/src/utilities/logger.ts index 2c0cbb9..86b33a4 100644 --- a/src/utilities/logger.ts +++ b/src/utilities/logger.ts @@ -32,13 +32,31 @@ const watchLogs = () => { LOG_TYPES.forEach((type) => { const logFile = getRootPath('logs', `${type}.log`) - fs.watchFile(logFile, (curr, prev) => { - if (curr.size > prev.size) { - const stream = fs.createReadStream(logFile, { start: prev.size, end: curr.size }) - stream.on('data', (chunk) => { - console.log(`[${type}]\n${chunk.toString()}`) - }) + // Get initial file size + const stats = fs.statSync(logFile) + let lastPosition = stats.size + + fs.watch(logFile, (eventType) => { + if (eventType !== 'change') { + return } + + fs.stat(logFile, (err, stats) => { + if (err) return + + if (stats.size > lastPosition) { + const stream = fs.createReadStream(logFile, { + start: lastPosition, + end: stats.size + }) + + stream.on('data', (chunk) => { + console.log(`[${type}]\n${chunk.toString()}`) + }) + + lastPosition = stats.size + } + }) }) }) } diff --git a/src/utilities/types.ts b/src/utilities/types.ts index 8fd0e7c..c402a3e 100644 --- a/src/utilities/types.ts +++ b/src/utilities/types.ts @@ -1,8 +1,8 @@ import { Socket } from 'socket.io' -import { Character, User } from '@prisma/client' +import { Character, User, ZoneEventTile, ZoneEventTileTeleport } from '@prisma/client' export type TSocket = Socket & { - user?: User + userId?: number characterId?: number handshake?: { query?: { @@ -18,7 +18,11 @@ export type TSocket = Socket & { export type ExtendedCharacter = Character & { isMoving?: boolean - resetMovement: boolean + resetMovement?: boolean +} + +export type ZoneEventTileWithTeleport = ZoneEventTile & { + teleport: ZoneEventTileTeleport } export type AssetData = {