import type { MapEventTileWithTeleport } from '#application/types' import { BaseEvent } from '#application/base/baseEvent' import MapManager from '#managers/mapManager' import MapCharacter from '#models/mapCharacter' import MapEventTileRepository from '#repositories/mapEventTileRepository' import CharacterService from '#services/characterMoveService' import TeleportService from '#services/characterTeleportService' export default class CharacterMove extends BaseEvent { private readonly characterService = CharacterService private readonly MOVEMENT_CANCEL_DELAY = 250 private readonly MOVEMENT_THROTTLE = 100 // Minimum time between movement requests private movementTimeouts: Map = new Map() private lastMovementTime: Map = new Map() // Track last movement time for each character public listen(): void { this.socket.on('map:character:move', this.handleEvent.bind(this)) } private async handleEvent({ positionX, positionY }: { positionX: number; positionY: number }): Promise { const mapCharacter = MapManager.getCharacterById(this.socket.characterId!) if (!mapCharacter?.getCharacter()) { this.logger.error('map:character:move error: Character not found or not initialized') return } // Implement request throttling const now = Date.now() const lastMove = this.lastMovementTime.get(this.socket.characterId!) || 0 if (now - lastMove < this.MOVEMENT_THROTTLE) { this.logger.debug('Movement request throttled') return } this.lastMovementTime.set(this.socket.characterId!, now) // Clear any existing movement timeout const existingTimeout = this.movementTimeouts.get(this.socket.characterId!) if (existingTimeout) { clearTimeout(existingTimeout) this.movementTimeouts.delete(this.socket.characterId!) } // If already moving, cancel current movement if (mapCharacter.isMoving) { mapCharacter.isMoving = false mapCharacter.currentPath = null // Add small delay before starting new movement await new Promise((resolve) => { const timeout = setTimeout(resolve, this.MOVEMENT_CANCEL_DELAY) this.movementTimeouts.set(this.socket.characterId!, timeout) }) } // Validate target position is within reasonable range const currentX = mapCharacter.character.positionX const currentY = mapCharacter.character.positionY const distance = Math.sqrt(Math.pow(positionX - currentX, 2) + Math.pow(positionY - currentY, 2)) if (distance > 20) { // Maximum allowed distance this.io.in(mapCharacter.character.map.id).emit('map:character:moveError', 'Target position too far') return } const path = await this.characterService.calculatePath(mapCharacter.character, positionX, positionY) if (!path?.length) { this.io.in(mapCharacter.character.map.id).emit('map:character:moveError', 'No valid path found') return } // Start new movement mapCharacter.isMoving = true mapCharacter.currentPath = path await this.moveAlongPath(mapCharacter, path) } private async moveAlongPath(mapCharacter: MapCharacter, path: Array<{ positionX: number; positionY: number }>): Promise { const character = mapCharacter.getCharacter() try { for (let i = 0; i < path.length - 1; i++) { if (!mapCharacter.isMoving || mapCharacter.currentPath !== path) { return } const [start, end] = [path[i], path[i + 1]] if (!start || !end) { this.logger.error('Invalid path step detected') break } // Validate each step if (Math.abs(end.positionX - start.positionX) > 1 || Math.abs(end.positionY - start.positionY) > 1) { this.logger.error('Invalid path step detected') break } character.setRotation(CharacterService.calculateRotation(start.positionX, start.positionY, end.positionX, end.positionY)) const mapEventTileRepository = new MapEventTileRepository() const mapEventTile = await mapEventTileRepository.getEventTileByMapIdAndPosition(character.getMap().getId(), Math.floor(end.positionX), Math.floor(end.positionY)) if (mapEventTile?.type === 'BLOCK') break if (mapEventTile?.type === 'TELEPORT' && mapEventTile.teleport) { await this.handleTeleportMapEventTile(mapEventTile as MapEventTileWithTeleport) return } // Update position first character.setPositionX(end.positionX).setPositionY(end.positionY) // Then emit with the same properties this.io.in(character.map.id).emit('map:character:move', { characterId: character.id, positionX: character.getPositionX(), positionY: character.getPositionY(), rotation: character.getRotation(), isMoving: true }) await this.characterService.applyMovementDelay() } } finally { if (mapCharacter.isMoving && mapCharacter.currentPath === path) { this.finalizeMovement(mapCharacter) } } } private async handleTeleportMapEventTile(mapEventTile: MapEventTileWithTeleport): Promise { if (mapEventTile.getTeleport()) { await TeleportService.teleportCharacter(this.socket.characterId!, { targetMapId: mapEventTile.getTeleport()!.getToMap().getId(), targetX: mapEventTile.getTeleport()!.getToPositionX(), targetY: mapEventTile.getTeleport()!.getToPositionY(), rotation: mapEventTile.getTeleport()!.getToRotation() }) } } private finalizeMovement(mapCharacter: MapCharacter): void { mapCharacter.isMoving = false this.io.in(mapCharacter.character.map.id).emit('map:character:move', { characterId: mapCharacter.character.id, positionX: mapCharacter.character.positionX, positionY: mapCharacter.character.positionY, rotation: mapCharacter.character.rotation, isMoving: mapCharacter.isMoving }) } }