forked from noxious/server
155 lines
5.9 KiB
TypeScript
155 lines
5.9 KiB
TypeScript
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<string, NodeJS.Timeout> = new Map()
|
|
private lastMovementTime: Map<string, number> = 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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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
|
|
})
|
|
}
|
|
}
|