From 17fa2a8f6e7ec273c549a0b3abc79deac7d9e866 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Sun, 16 Feb 2025 01:29:24 +0100 Subject: [PATCH] More movement improvements --- src/events/map/characterMove.ts | 104 +++++++++++++-------------- src/services/characterMoveService.ts | 18 +++-- 2 files changed, 64 insertions(+), 58 deletions(-) diff --git a/src/events/map/characterMove.ts b/src/events/map/characterMove.ts index 15083a3..961b061 100644 --- a/src/events/map/characterMove.ts +++ b/src/events/map/characterMove.ts @@ -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 { + // 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) 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 { 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 + } } } diff --git a/src/services/characterMoveService.ts b/src/services/characterMoveService.ts index 8a90a6a..eab632f 100644 --- a/src/services/characterMoveService.ts +++ b/src/services/characterMoveService.ts @@ -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 { } 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(