forked from noxious/server
178 lines
6.2 KiB
TypeScript
178 lines
6.2 KiB
TypeScript
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<void> {
|
|
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<void> {
|
|
// 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<void> {
|
|
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<void> {
|
|
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
|
|
}
|
|
}
|
|
}
|