More movement improvements

This commit is contained in:
Dennis Postma 2025-02-16 01:29:24 +01:00
parent cbd6e2c307
commit 17fa2a8f6e
2 changed files with 64 additions and 58 deletions

View File

@ -1,7 +1,6 @@
import { BaseEvent } from '@/application/base/baseEvent'
import { SocketEvent } from '@/application/enums'
import type { MapEventTileWithTeleport } from '@/application/types'
import type { Character } from '@/entities/character'
import MapManager from '@/managers/mapManager'
import MapCharacter from '@/models/mapCharacter'
import CharacterMoveService from '@/services/characterMoveService'
@ -9,9 +8,10 @@ import CharacterMoveService from '@/services/characterMoveService'
export default class CharacterMove extends BaseEvent {
private readonly STEP_DELAY = 100
private readonly THROTTLE_DELAY = 230
private readonly MAX_REQUEST_DISTANCE = 30 // Maximum allowed distance for movement requests
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))
@ -34,12 +34,15 @@ export default class CharacterMove extends BaseEvent {
return
}
// Stop any existing movement before starting a new one
await this.stopExistingMovement(mapCharacter)
// Validate current position against last known position
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)
this.broadcastMovement(character, false)
CharacterMoveService.broadcastMovement(character, false)
return
}
@ -51,53 +54,59 @@ export default class CharacterMove extends BaseEvent {
return
}
// If character is already moving to the same target position, ignore the request
if (mapCharacter.isMoving && mapCharacter.currentPath?.length) {
const lastPathPoint = mapCharacter.currentPath[mapCharacter.currentPath.length - 1]
if (lastPathPoint && Math.abs(lastPathPoint.positionX - positionX) < 1 && Math.abs(lastPathPoint.positionY - positionY) < 1) {
return
}
}
// Cancel any ongoing movement
CharacterMoveService.cancelCurrentMovement(mapCharacter)
// Update last known position
this.lastKnownPosition = { x: currentX, y: currentY }
// Calculate path to target position
const path = await CharacterMoveService.calculatePath(character, Math.floor(positionX), Math.floor(positionY))
if (!path?.length) {
return
}
// Start new movement
// Set new movement state
mapCharacter.isMoving = true
mapCharacter.currentPath = path
this.isProcessingMove = true
// Start the movement
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) return
if (!path?.length || !character) return
let lastMoveTime = Date.now()
let currentTile, nextTile
const movementId = Date.now() // Unique identifier for this movement sequence
try {
for (let i = 0; i < path.length - 1; i++) {
// Check if this movement sequence is still valid
if (!mapCharacter.isMoving || mapCharacter.currentPath !== path) {
this.broadcastMovement(character, false)
return
}
// Ensure minimum time between moves using a single Date.now() call
const timeSinceLastMove = Date.now() - lastMoveTime
if (timeSinceLastMove < this.STEP_DELAY) {
await new Promise((resolve) => setTimeout(resolve, this.STEP_DELAY - timeSinceLastMove))
@ -110,14 +119,15 @@ export default class CharacterMove extends BaseEvent {
return
}
// Update character rotation and position in a single operation
// 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 at the next tile
// 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) {
@ -126,17 +136,15 @@ export default class CharacterMove extends BaseEvent {
}
}
// Broadcast movement
// Only broadcast if this is still the current movement
if (mapCharacter.isMoving && mapCharacter.currentPath === path) {
this.broadcastMovement(character, true)
CharacterMoveService.broadcastMovement(character, true)
}
// Apply movement delay between steps
if (i < path.length - 2) {
await new Promise((resolve) => setTimeout(resolve, this.STEP_DELAY))
}
// Update last known position and move time
this.lastKnownPosition = {
x: nextTile.positionX,
y: nextTile.positionY
@ -144,41 +152,31 @@ export default class CharacterMove extends BaseEvent {
lastMoveTime = Date.now()
}
} finally {
if (mapCharacter.isMoving) {
this.finalizeMovement(mapCharacter)
// Only finalize if this movement is still active
if (mapCharacter.isMoving && mapCharacter.currentPath === path) {
await this.finalizeMovement(mapCharacter)
}
}
}
private finalizeMovement(mapCharacter: MapCharacter): void {
// Clear any existing timeout
private async finalizeMovement(mapCharacter: MapCharacter): Promise<void> {
if (this.movementTimeout) {
clearTimeout(this.movementTimeout)
}
// Clear the current path immediately
mapCharacter.currentPath = null
// Set new timeout for movement state cleanup
this.movementTimeout = setTimeout(() => {
// Ensure the character is still in a valid state
if (!mapCharacter.isMoving || !mapCharacter.currentPath) {
mapCharacter.isMoving = false
// Save the final position and broadcast it
mapCharacter.savePosition().then(() => {
this.broadcastMovement(mapCharacter.character, false)
})
}
}, this.STEP_DELAY * 2) // Increased delay to ensure all movement processing is complete
}
const character = mapCharacter.getCharacter()
if (character) {
await mapCharacter.savePosition()
private broadcastMovement(character: Character, isMoving: boolean): void {
this.io.in(character.map.id).emit(SocketEvent.MAP_CHARACTER_MOVE, {
characterId: character.id,
positionX: character.getPositionX(),
positionY: character.getPositionY(),
rotation: character.getRotation(),
isMoving
})
// 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
}
}
}

View File

@ -1,11 +1,13 @@
import { BaseService } from '@/application/base/baseService'
import config from '@/application/config'
import { SocketEvent } from '@/application/enums'
import type { MapEventTileWithTeleport, UUID } from '@/application/types'
import { Character } from '@/entities/character'
import MapManager from '@/managers/mapManager'
import SocketManager from '@/managers/socketManager'
import MapEventTileRepository from '@/repositories/mapEventTileRepository'
import TeleportService from '@/services/characterTeleportService'
import MapCharacter from "@/models/mapCharacter";
import type { Server } from 'socket.io'
type Position = { positionX: number; positionY: number }
export type Node = Position & { parent?: Node; g: number; h: number; f: number }
@ -75,6 +77,8 @@ class PriorityQueue<T> {
}
class CharacterMoveService extends BaseService {
private io: Server = SocketManager.getIO()
// Rotation lookup table for better performance
private readonly ROTATION_MAP = {
diagonal: {
@ -277,10 +281,14 @@ class CharacterMoveService extends BaseService {
return path
}
public cancelCurrentMovement(mapCharacter: MapCharacter): void {
if (!mapCharacter.isMoving) return
mapCharacter.isMoving = false
mapCharacter.currentPath = null
public broadcastMovement(character: Character, isMoving: boolean): void {
this.io.in(character.map.id).emit(SocketEvent.MAP_CHARACTER_MOVE, {
characterId: character.id,
positionX: character.getPositionX(),
positionY: character.getPositionY(),
rotation: character.getRotation(),
isMoving
})
}
public validateMovementDistance(