From e8d100e063079b7bdfb77d5966bf3c66ed9bf93c Mon Sep 17 00:00:00 2001
From: Dennis Postma <dennis@directonline.io>
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<number, symbol>();
-
-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<void> {
+    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<void> {
+    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<number, symbol> = new Map()
+
+  public async updatePosition(character: ExtendedCharacter, position: { x: number; y: number }, newZoneId?: number): Promise<void> {
+    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<Array<{ x: number; y: number }> | 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<void> {
+    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<boolean> {
+    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<any | null> {
+    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<void> {
+    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)
+  }
+}