import { BaseEvent } from '@/application/base/baseEvent' import { SocketEvent } from '@/application/enums' import type { MapEventTileWithTeleport } from '@/application/types' import MapManager from '@/managers/mapManager' import MapCharacter from '@/models/mapCharacter' import CharacterMoveService from '@/services/characterMoveService' export default class CharacterMove extends BaseEvent { private readonly STEP_DELAY = 100 private readonly THROTTLE_DELAY = 200 private readonly MAX_REQUEST_DISTANCE = 30 private movementTimeout: NodeJS.Timeout | null = null private lastKnownPosition: { x: number; y: number } | null = null private isProcessingMove = false public listen(): void { this.socket.on(SocketEvent.MAP_CHARACTER_MOVE, this.handleEvent.bind(this)) } private async handleEvent([positionX, positionY]: [number, number]): Promise { try { const mapCharacter = MapManager.getCharacterById(this.socket.characterId!) if (!mapCharacter?.getCharacter()) return const character = mapCharacter.getCharacter() const currentX = character.getPositionX() const currentY = character.getPositionY() // Enhanced throttling with position tracking const throttleKey = `movement_${this.socket.characterId}` if (this.isThrottled(throttleKey, this.THROTTLE_DELAY)) return // Stop any existing movement before starting a new one await this.stopExistingMovement(mapCharacter) // Validate movement const movementValidation = CharacterMoveService.validateMovementDistance(currentX, currentY, this.lastKnownPosition, this.STEP_DELAY, mapCharacter.isMoving) if (!movementValidation.isValid) { character.setPositionX(this.lastKnownPosition!.x).setPositionY(this.lastKnownPosition!.y) CharacterMoveService.broadcastMovement(character, false) return } // Calculate and validate path const path = await CharacterMoveService.calculatePath(character, Math.floor(positionX), Math.floor(positionY)) if (!path?.length) { mapCharacter.isMoving = false mapCharacter.currentPath = null CharacterMoveService.broadcastMovement(character, false) return } // Start movement mapCharacter.currentPath = path mapCharacter.isMoving = true this.isProcessingMove = true await this.moveAlongPath(mapCharacter) } catch (error: any) { this.logger.error('map:character:move error: ' + error.message) } finally { this.isProcessingMove = false } } private async stopExistingMovement(mapCharacter: MapCharacter): Promise { // Clear existing movement timeout if (this.movementTimeout) { clearTimeout(this.movementTimeout) this.movementTimeout = null } // Wait for any ongoing movement processing to complete if (this.isProcessingMove) { await new Promise((resolve) => setTimeout(resolve, this.STEP_DELAY)) } // Only clear the path, keep isMoving state for continuous animation mapCharacter.currentPath = null } private async moveAlongPath(mapCharacter: MapCharacter): Promise { const character = mapCharacter.getCharacter() const path = mapCharacter.currentPath if (!path?.length || !character) return let lastMoveTime = Date.now() let currentTile, nextTile try { for (let i = 0; i < path.length - 1; i++) { // Check if this movement sequence is still valid if (!mapCharacter.isMoving || mapCharacter.currentPath !== path) { return } const timeSinceLastMove = Date.now() - lastMoveTime if (timeSinceLastMove < this.STEP_DELAY) { await new Promise((resolve) => setTimeout(resolve, this.STEP_DELAY - timeSinceLastMove)) } currentTile = path[i] nextTile = path[i + 1] if (!currentTile || !nextTile || !CharacterMoveService.isValidStep(currentTile, nextTile)) { return } // Update character position and rotation character .setRotation(CharacterMoveService.calculateRotation(currentTile.positionX, currentTile.positionY, nextTile.positionX, nextTile.positionY)) .setPositionX(nextTile.positionX) .setPositionY(nextTile.positionY) // Check for map events const mapEventTile = await CharacterMoveService.checkMapEvents(character.getMap().getId(), nextTile) if (mapEventTile) { if (mapEventTile.type === 'BLOCK') break if (mapEventTile.type === 'TELEPORT' && mapEventTile.teleport) { // Force clear movement state before teleport mapCharacter.isMoving = false mapCharacter.currentPath = null this.lastKnownPosition = null // Reset last known position await CharacterMoveService.handleTeleportMapEventTile(character.id, mapEventTile as MapEventTileWithTeleport) return } } // Only broadcast if this is still the current movement if (mapCharacter.isMoving && mapCharacter.currentPath === path) { CharacterMoveService.broadcastMovement(character, true) } if (i < path.length - 2) { await new Promise((resolve) => setTimeout(resolve, this.STEP_DELAY)) } this.lastKnownPosition = { x: nextTile.positionX, y: nextTile.positionY } lastMoveTime = Date.now() } } finally { // Only finalize if this movement is still active if (mapCharacter.isMoving && mapCharacter.currentPath === path) { await this.finalizeMovement(mapCharacter) } } } private async finalizeMovement(mapCharacter: MapCharacter): Promise { if (this.movementTimeout) { clearTimeout(this.movementTimeout) } mapCharacter.currentPath = null const character = mapCharacter.getCharacter() if (character) { await mapCharacter.savePosition() // Set a timeout to stop movement animation if no new movement is received this.movementTimeout = setTimeout(() => { if (!mapCharacter.currentPath) { mapCharacter.isMoving = false CharacterMoveService.broadcastMovement(character, false) } }, this.THROTTLE_DELAY) // Use THROTTLE_DELAY to match input timing } } }