1
0
forked from noxious/server
noxious_server/src/events/map/characterMove.ts

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
}
}
}