From e8d100e063079b7bdfb77d5966bf3c66ed9bf93c Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Mon, 26 Aug 2024 19:09:35 +0200 Subject: [PATCH] New method for events + TP work --- package-lock.json | 6 +- src/events/gameMaster/zoneEditor/update.ts | 16 +- src/events/zone/characterJoin.ts | 4 +- src/events/zone/characterMove.ts | 158 ------------------ src/events/zone/characterMoveEvent.ts | 70 ++++++++ src/managers/zoneManager.ts | 32 ++-- src/server.ts | 34 +++- .../character/characterMoveService.ts | 66 ++++++++ .../{ => character}/characterService.ts | 0 src/services/character/movementValidator.ts | 17 ++ src/services/character/teleportService.ts | 37 ++++ src/utilities/{player => character}/aStar.ts | 0 .../{player => character}/rotation.ts | 0 src/utilities/logger.ts | 3 +- src/utilities/socketEmitter.ts | 29 ++++ 15 files changed, 282 insertions(+), 190 deletions(-) delete mode 100644 src/events/zone/characterMove.ts create mode 100644 src/events/zone/characterMoveEvent.ts create mode 100644 src/services/character/characterMoveService.ts rename src/services/{ => character}/characterService.ts (100%) create mode 100644 src/services/character/movementValidator.ts create mode 100644 src/services/character/teleportService.ts rename src/utilities/{player => character}/aStar.ts (100%) rename src/utilities/{player => character}/rotation.ts (100%) create mode 100644 src/utilities/socketEmitter.ts diff --git a/package-lock.json b/package-lock.json index 812db1f..0f57a3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2129,9 +2129,9 @@ "license": "MIT" }, "node_modules/safe-stable-stringify": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", - "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", "license": "MIT", "engines": { "node": ">=10" diff --git a/src/events/gameMaster/zoneEditor/update.ts b/src/events/gameMaster/zoneEditor/update.ts index de4df14..48e4c26 100644 --- a/src/events/gameMaster/zoneEditor/update.ts +++ b/src/events/gameMaster/zoneEditor/update.ts @@ -73,15 +73,17 @@ export default function (socket: TSocket, io: Server) { type: zoneEventTile.type, positionX: zoneEventTile.positionX, positionY: zoneEventTile.positionY, - ...(zoneEventTile.type === 'TELEPORT' && zoneEventTile.teleport ? { - teleport: { - create: { - toZoneId: zoneEventTile.teleport.toZoneId, - toPositionX: zoneEventTile.teleport.toPositionX, - toPositionY: zoneEventTile.teleport.toPositionY + ...(zoneEventTile.type === 'TELEPORT' && zoneEventTile.teleport + ? { + teleport: { + create: { + toZoneId: zoneEventTile.teleport.toZoneId, + toPositionX: zoneEventTile.teleport.toPositionX, + toPositionY: zoneEventTile.teleport.toPositionY + } } } - } : {}) + : {}) })) }, zoneObjects: { diff --git a/src/events/zone/characterJoin.ts b/src/events/zone/characterJoin.ts index f934fa0..a115e97 100644 --- a/src/events/zone/characterJoin.ts +++ b/src/events/zone/characterJoin.ts @@ -24,7 +24,7 @@ export default function (socket: TSocket, io: Server) { try { console.log(`---User ${socket.character?.id} has requested zone.`) - if (!socket.character) return; + if (!socket.character) return if (!data.zoneId) { console.log(`---Zone id not provided.`) @@ -55,7 +55,7 @@ export default function (socket: TSocket, io: Server) { callback({ zone, characters: ZoneManager.getCharactersInZone(zone.id) }) } catch (error: any) { logger.error(`Error requesting zone: ${error.message}`) - socket.disconnect(); + socket.disconnect() } }) } diff --git a/src/events/zone/characterMove.ts b/src/events/zone/characterMove.ts deleted file mode 100644 index 11d8bdd..0000000 --- a/src/events/zone/characterMove.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { Server } from 'socket.io'; -import { TSocket, ExtendedCharacter } from '../../utilities/types'; -import ZoneManager from '../../managers/zoneManager'; -import prisma from '../../utilities/prisma'; -import { AStar } from '../../utilities/player/aStar'; -import Rotation from '../../utilities/player/rotation'; -import ZoneRepository from '../../repositories/zoneRepository'; -import { Character } from '@prisma/client'; - -const moveTokens = new Map(); - -export default function setupCharacterMove(socket: TSocket, io: Server) { - socket.on('character:initMove', handleCharacterMove(socket, io)); -} - -const handleCharacterMove = (socket: TSocket, io: Server) => async ({ positionX, positionY }: { positionX: number; positionY: number }) => { - const { character } = socket; - if (!character) return console.error('character:move error', 'Character not found'); - - const grid = await ZoneManager.getGrid(character.zoneId); - if (!grid?.length) return console.error('character:move error', 'Grid not found or empty'); - - const start = { x: Math.floor(character.positionX), y: Math.floor(character.positionY) }; - const end = { x: Math.floor(positionX), y: Math.floor(positionY) }; - - if (isObstacle(end, grid)) return socket.emit('character:moveError', 'Destination is an obstacle'); - - const path = AStar.findPath(start, end, grid); - if (!path.length) return socket.emit('character:moveError', 'No valid path found'); - - moveTokens.set(character.id, Symbol('moveToken')); - character.isMoving = true; - io.in(character.zoneId.toString()).emit('character:move', character); - moveAlongPath(socket, io, path, grid).catch(console.error); -}; - -async function moveAlongPath(socket: TSocket, io: Server, path: Array<{ x: number; y: number }>, grid: number[][]) { - const { character } = socket; - if (!character) return; - - const moveToken = moveTokens.get(character.id); - const stepDuration = 250; - const updateInterval = 50; - - for (let i = 0; i < path.length - 1; i++) { - const startTime = Date.now(); - const start = path[i]; - const end = path[i + 1]; - - while (Date.now() - startTime < stepDuration) { - if (moveTokens.get(character.id) !== moveToken) return; - - const progress = (Date.now() - startTime) / stepDuration; - const current = interpolatePosition(start, end, progress); - - if (isObstacle(current, grid)) { - await updateCharacterPosition(character, start, Rotation.calculate(start.x, start.y, end.x, end.y), socket, io); - return; - } - - const tp = await prisma.zoneEventTile.findFirst({ - where: { zoneId: character.zoneId, type: 'TELEPORT', positionX: current.x, positionY: current.y }, - include: { teleport: true } - }); - - if (tp?.teleport) { - await handleTeleport(socket, io, character, tp.teleport, start, end); - return; - } - - await updateCharacterPosition(character, current, Rotation.calculate(start.x, start.y, end.x, end.y), socket, io); - await new Promise(resolve => setTimeout(resolve, updateInterval)); - } - } - - if (moveTokens.get(character.id) === moveToken) { - await updateCharacterPosition(character, path[path.length - 1], character.rotation, socket, io); - character.isMoving = false; - moveTokens.delete(character.id); - } -} - -async function handleTeleport(socket: TSocket, io: Server, character: ExtendedCharacter, teleport: any, start: { x: number; y: number }, end: { x: number; y: number }) { - if (teleport.toZoneId === character.zoneId) return; - - const zone = await ZoneRepository.getById(teleport.toZoneId); - if (!zone) return; - - character.isMoving = false; - character.zoneId = teleport.toZoneId; - - moveTokens.delete(character.id); - - socket.leave(character.zoneId.toString()); - socket.join(teleport.toZoneId.toString()); - - socket.emit('zone:teleport', { zone, characters: ZoneManager.getCharactersInZone(zone.id) }); - - await updateCharacterPosition( - character, - { x: teleport.toPositionX, y: teleport.toPositionY }, - Rotation.calculate(start.x, start.y, end.x, end.y), - socket, - io, - teleport.toZoneId - ); -} - -async function updateCharacterPosition( - character: ExtendedCharacter, - position: { x: number; y: number }, - rotation: number, - socket: TSocket, - io: Server, - newZoneId?: number -) { - const oldZoneId = character.zoneId; - - Object.assign(character, { - positionX: position.x, - positionY: position.y, - rotation, - zoneId: newZoneId || character.zoneId - }); - - if (newZoneId && newZoneId !== oldZoneId) { - io.to(oldZoneId.toString()).emit('zone:character:leave', character); - io.to(newZoneId.toString()).emit('zone:character:join', character); - ZoneManager.removeCharacterFromZone(oldZoneId, character as Character); - ZoneManager.addCharacterToZone(newZoneId, character as Character); - } else { - ZoneManager.updateCharacterInZone(character.zoneId, character); - } - - await prisma.character.update({ - where: { id: character.id }, - data: { - positionX: position.x, - positionY: position.y, - rotation, - zoneId: character.zoneId - } - }); - - io.in(character.zoneId.toString()).emit('character:move', character); - socket.emit('character:dataUpdated', character); -} - -const interpolatePosition = (start: { x: number; y: number }, end: { x: number; y: number }, progress: number) => ({ - x: start.x + (end.x - start.x) * progress, - y: start.y + (end.y - start.y) * progress -}); - -const isObstacle = ({ x, y }: { x: number; y: number }, grid: number[][]) => { - const gridX = Math.floor(x); - const gridY = Math.floor(y); - return grid[gridY]?.[gridX] === 1 || grid[gridY]?.[Math.ceil(x)] === 1 || grid[Math.ceil(y)]?.[gridX] === 1 || grid[Math.ceil(y)]?.[Math.ceil(x)] === 1; -}; \ No newline at end of file diff --git a/src/events/zone/characterMoveEvent.ts b/src/events/zone/characterMoveEvent.ts new file mode 100644 index 0000000..2c9a87e --- /dev/null +++ b/src/events/zone/characterMoveEvent.ts @@ -0,0 +1,70 @@ +import { Server } from 'socket.io' +import { TSocket, ExtendedCharacter } from '../../utilities/types' +import { CharacterMoveService } from '../../services/character/characterMoveService' +import { TeleportService } from '../../services/character/teleportService' +import { MovementValidator } from '../../services/character/movementValidator' +import { SocketEmitter } from '../../utilities/socketEmitter' + +export default class CharacterMoveEvent { + private characterMoveService: CharacterMoveService + private teleportService: TeleportService + private movementValidator: MovementValidator + private socketEmitter: SocketEmitter + + constructor( + private readonly io: Server, + private readonly socket: TSocket + ) { + this.characterMoveService = new CharacterMoveService() + this.teleportService = new TeleportService() + this.movementValidator = new MovementValidator() + this.socketEmitter = new SocketEmitter(io, socket) + } + + public listen(): void { + this.socket.on('character:initMove', this.handleCharacterMove.bind(this)) + } + + private async handleCharacterMove({ positionX, positionY }: { positionX: number; positionY: number }): Promise { + const { character } = this.socket + if (!character) { + console.error('character:move error', 'Character not found') + return + } + + const path = await this.characterMoveService.calculatePath(character, positionX, positionY) + if (!path) { + this.socketEmitter.emitMoveError('No valid path found') + return + } + + await this.moveAlongPath(character, path) + } + + private async moveAlongPath(character: ExtendedCharacter, path: Array<{ x: number; y: number }>): Promise { + for (const position of path) { + if (!(await this.movementValidator.isValidMove(character, position))) { + break + } + + const teleport = await this.teleportService.checkForTeleport(character, position) + if (teleport) { + await this.characterMoveService.updatePosition(character, position, teleport.toZoneId) + await this.teleportService.handleTeleport(this.socket, character, teleport) + break + } + + await this.characterMoveService.updatePosition(character, position) + this.socketEmitter.emitCharacterMove(character) + + await this.characterMoveService.applyMovementDelay() + } + + this.finalizeMovement(character) + } + + private finalizeMovement(character: ExtendedCharacter): void { + character.isMoving = false + this.socketEmitter.emitCharacterMove(character) + } +} diff --git a/src/managers/zoneManager.ts b/src/managers/zoneManager.ts index dd68d44..8dbd542 100644 --- a/src/managers/zoneManager.ts +++ b/src/managers/zoneManager.ts @@ -57,42 +57,42 @@ class ZoneManager { private isPositionWalkable(zoneId: number, x: number, y: number): boolean { const loadedZone = this.loadedZones.find((lz) => lz.zone.id === zoneId) if (!loadedZone) { - console.log(`Zone ${zoneId} not found in loadedZones`); - return false; + console.log(`Zone ${zoneId} not found in loadedZones`) + return false } if (!loadedZone.grid) { - console.log(`Grid for zone ${zoneId} is undefined`); - return false; + console.log(`Grid for zone ${zoneId} is undefined`) + return false } if (!loadedZone.grid[y]) { - console.log(`Row ${y} in grid for zone ${zoneId} is undefined`); - return false; + console.log(`Row ${y} in grid for zone ${zoneId} is undefined`) + return false } - return loadedZone.grid[y][x] === 0; + return loadedZone.grid[y][x] === 0 } public addCharacterToZone(zoneId: number, character: Character) { - console.log(`Adding character ${character.id} to zone ${zoneId}`); - console.log(`Character position: x=${character.positionX}, y=${character.positionY}`); + console.log(`Adding character ${character.id} to zone ${zoneId}`) + console.log(`Character position: x=${character.positionX}, y=${character.positionY}`) const loadedZone = this.loadedZones.find((loadedZone) => { return loadedZone.zone.id === zoneId }) if (!loadedZone) { - console.log(`Zone ${zoneId} not found in loadedZones`); - return; + console.log(`Zone ${zoneId} not found in loadedZones`) + return } if (this.isPositionWalkable(zoneId, character.positionX, character.positionY)) { loadedZone.characters.push(character) - console.log(`Character ${character.id} added to zone ${zoneId}`); + console.log(`Character ${character.id} added to zone ${zoneId}`) } else { // set position to 0,0 if not walkable - console.log(`Position (${character.positionX}, ${character.positionY}) is not walkable in zone ${zoneId}`); - character.positionX = 0; - character.positionY = 0; - loadedZone.characters.push(character); + console.log(`Position (${character.positionX}, ${character.positionY}) is not walkable in zone ${zoneId}`) + character.positionX = 0 + character.positionY = 0 + loadedZone.characters.push(character) } } diff --git a/src/server.ts b/src/server.ts index 162d254..5781a4e 100644 --- a/src/server.ts +++ b/src/server.ts @@ -37,6 +37,18 @@ export class Server { * Start the server */ public async start() { + // Read log file and print to console for debugging + const logFile = path.join(__dirname, '../logs/app.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(chunk.toString()) + }) + } + }) + // Check prisma connection try { await prisma.$connect() @@ -91,9 +103,25 @@ export class Server { if (file.isDirectory()) { await this.loadEventHandlers(fullPath, socket) - } else if (file.isFile()) { - const module = await import(fullPath) - module.default(socket, this.io) + } else if (file.isFile() && file.name.endsWith('.ts')) { + try { + const module = await import(fullPath) + 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(socket, this.io) + } + } else { + logger.warn(`Unrecognized export in ${file.name}`) + } + } catch (error: any) { + logger.error(`Error loading event handler ${file.name}: ${error.message}`) + } } } } diff --git a/src/services/character/characterMoveService.ts b/src/services/character/characterMoveService.ts new file mode 100644 index 0000000..8ba815d --- /dev/null +++ b/src/services/character/characterMoveService.ts @@ -0,0 +1,66 @@ +import { ExtendedCharacter } from '../../utilities/types' +import { AStar } from '../../utilities/character/aStar' +import ZoneManager from '../../managers/zoneManager' +import prisma from '../../utilities/prisma' +import Rotation from '../../utilities/character/rotation' + +export class CharacterMoveService { + private moveTokens: Map = new Map() + + public async updatePosition(character: ExtendedCharacter, position: { x: number; y: number }, newZoneId?: number): Promise { + const oldZoneId = character.zoneId + + Object.assign(character, { + positionX: position.x, + positionY: position.y, + rotation: Rotation.calculate(character.positionX, character.positionY, position.x, position.y), + zoneId: newZoneId || character.zoneId + }) + + if (newZoneId && newZoneId !== oldZoneId) { + ZoneManager.removeCharacterFromZone(oldZoneId, character) + ZoneManager.addCharacterToZone(newZoneId, character) + } else { + ZoneManager.updateCharacterInZone(character.zoneId, character) + } + + await prisma.character.update({ + where: { id: character.id }, + data: { + positionX: position.x, + positionY: position.y, + rotation: character.rotation, + zoneId: character.zoneId + } + }) + } + + public async calculatePath(character: ExtendedCharacter, targetX: number, targetY: number): Promise | null> { + const grid = await ZoneManager.getGrid(character.zoneId) + if (!grid?.length) { + console.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 startMovement(characterId: number): void { + this.moveTokens.set(characterId, Symbol('moveToken')) + } + + public stopMovement(characterId: number): void { + this.moveTokens.delete(characterId) + } + + public isMoving(characterId: number): boolean { + return this.moveTokens.has(characterId) + } + + public async applyMovementDelay(): Promise { + await new Promise((resolve) => setTimeout(resolve, 250)) // 50ms delay between steps + } +} diff --git a/src/services/characterService.ts b/src/services/character/characterService.ts similarity index 100% rename from src/services/characterService.ts rename to src/services/character/characterService.ts diff --git a/src/services/character/movementValidator.ts b/src/services/character/movementValidator.ts new file mode 100644 index 0000000..5c70193 --- /dev/null +++ b/src/services/character/movementValidator.ts @@ -0,0 +1,17 @@ +import { ExtendedCharacter } from '../../utilities/types' +import ZoneManager from '../../managers/zoneManager' + +export class MovementValidator { + public async isValidMove(character: ExtendedCharacter, position: { x: number; y: number }): Promise { + const grid = await ZoneManager.getGrid(character.zoneId) + if (!grid?.length) return false + + return !this.isObstacle(position, grid) + } + + private isObstacle({ x, y }: { x: number; y: number }, grid: number[][]): boolean { + const gridX = Math.floor(x) + const gridY = Math.floor(y) + return grid[gridY]?.[gridX] === 1 || grid[gridY]?.[Math.ceil(x)] === 1 || grid[Math.ceil(y)]?.[gridX] === 1 || grid[Math.ceil(y)]?.[Math.ceil(x)] === 1 + } +} diff --git a/src/services/character/teleportService.ts b/src/services/character/teleportService.ts new file mode 100644 index 0000000..f6894b8 --- /dev/null +++ b/src/services/character/teleportService.ts @@ -0,0 +1,37 @@ +import { ExtendedCharacter, TSocket } from '../../utilities/types' +import prisma from '../../utilities/prisma' +import ZoneRepository from '../../repositories/zoneRepository' +import ZoneManager from '../../managers/zoneManager' + +export class TeleportService { + public async checkForTeleport(character: ExtendedCharacter, position: { x: number; y: number }): Promise { + return prisma.zoneEventTile.findFirst({ + where: { + zoneId: character.zoneId, + type: 'TELEPORT', + positionX: Math.floor(position.x), + positionY: Math.floor(position.y) + }, + include: { teleport: true } + }) + } + + public async handleTeleport(socket: TSocket, character: ExtendedCharacter, teleport: any): Promise { + if (teleport.toZoneId === character.zoneId) return + + const zone = await ZoneRepository.getById(teleport.toZoneId) + if (!zone) return + + character.zoneId = teleport.toZoneId + character.positionX = teleport.toPositionX + character.positionY = teleport.toPositionY + + socket.leave(character.zoneId.toString()) + socket.join(teleport.toZoneId.toString()) + + socket.emit('zone:teleport', { + zone, + characters: ZoneManager.getCharactersInZone(zone.id) + }) + } +} diff --git a/src/utilities/player/aStar.ts b/src/utilities/character/aStar.ts similarity index 100% rename from src/utilities/player/aStar.ts rename to src/utilities/character/aStar.ts diff --git a/src/utilities/player/rotation.ts b/src/utilities/character/rotation.ts similarity index 100% rename from src/utilities/player/rotation.ts rename to src/utilities/character/rotation.ts diff --git a/src/utilities/logger.ts b/src/utilities/logger.ts index 3f5bea8..ba99f3d 100644 --- a/src/utilities/logger.ts +++ b/src/utilities/logger.ts @@ -14,7 +14,8 @@ const logger = pino({ return { level: label.toUpperCase() } } }, - timestamp: pino.stdTimeFunctions.isoTime + timestamp: pino.stdTimeFunctions.isoTime, + base: null // This will prevent hostname and pid from being included }) export default logger diff --git a/src/utilities/socketEmitter.ts b/src/utilities/socketEmitter.ts new file mode 100644 index 0000000..d3358c6 --- /dev/null +++ b/src/utilities/socketEmitter.ts @@ -0,0 +1,29 @@ +import { Server } from 'socket.io' +import { TSocket, ExtendedCharacter } from './types' + +export class SocketEmitter { + constructor( + private readonly io: Server, + private readonly socket: TSocket + ) {} + + public emitMoveError(message: string): void { + this.socket.emit('character:moveError', message) + } + + public emitCharacterMove(character: ExtendedCharacter): void { + this.io.in(character.zoneId.toString()).emit('character:move', character) + } + + public emitCharacterLeave(character: ExtendedCharacter, zoneId: number): void { + this.io.to(zoneId.toString()).emit('zone:character:leave', character) + } + + public emitCharacterJoin(character: ExtendedCharacter): void { + this.io.to(character.zoneId.toString()).emit('zone:character:join', character) + } + + public emitCharacterDataUpdated(character: ExtendedCharacter): void { + this.socket.emit('character:dataUpdated', character) + } +}