From 161a9795bc396b2b9855baad50ee45af0a7c58fb Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Sun, 16 Feb 2025 17:12:01 +0100 Subject: [PATCH] Near perfect movement --- src/events/map/characterMove.ts | 26 +--- src/services/characterMoveService.ts | 188 +++++++++++++-------------- 2 files changed, 99 insertions(+), 115 deletions(-) diff --git a/src/events/map/characterMove.ts b/src/events/map/characterMove.ts index 6703a86..c679fb6 100644 --- a/src/events/map/characterMove.ts +++ b/src/events/map/characterMove.ts @@ -7,7 +7,7 @@ import CharacterMoveService from '@/services/characterMoveService' export default class CharacterMove extends BaseEvent { private readonly STEP_DELAY = 100 - private readonly THROTTLE_DELAY = 230 + 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 @@ -20,9 +20,7 @@ export default class CharacterMove extends BaseEvent { private async handleEvent({ positionX, positionY }: { positionX: number; positionY: number }): Promise { try { const mapCharacter = MapManager.getCharacterById(this.socket.characterId!) - if (!mapCharacter?.getCharacter()) { - return - } + if (!mapCharacter?.getCharacter()) return const character = mapCharacter.getCharacter() const currentX = character.getPositionX() @@ -30,14 +28,12 @@ export default class CharacterMove extends BaseEvent { // Enhanced throttling with position tracking const throttleKey = `movement_${this.socket.characterId}` - if (this.isThrottled(throttleKey, this.THROTTLE_DELAY)) { - return - } + if (this.isThrottled(throttleKey, this.THROTTLE_DELAY)) return // Stop any existing movement before starting a new one await this.stopExistingMovement(mapCharacter) - // Validate current position against last known position + // Validate movement const movementValidation = CharacterMoveService.validateMovementDistance(currentX, currentY, this.lastKnownPosition, this.STEP_DELAY, mapCharacter.isMoving) if (!movementValidation.isValid) { @@ -46,30 +42,21 @@ export default class CharacterMove extends BaseEvent { return } - // Validate requested position distance - const requestValidation = CharacterMoveService.validateRequestDistance(currentX, currentY, positionX, positionY, this.MAX_REQUEST_DISTANCE) - - if (!requestValidation.isValid) { - this.logger.warn(`Invalid movement distance detected: ${this.socket.characterId}`) - return - } - + // Calculate and validate path const path = await CharacterMoveService.calculatePath(character, Math.floor(positionX), Math.floor(positionY)) if (!path?.length) { - // Ensure movement state is cleaned up for invalid paths mapCharacter.isMoving = false mapCharacter.currentPath = null CharacterMoveService.broadcastMovement(character, false) return } - // Only set movement state if we have a valid path + // Start movement mapCharacter.currentPath = path mapCharacter.isMoving = true this.isProcessingMove = true - // Start the movement await this.moveAlongPath(mapCharacter) } catch (error: any) { this.logger.error('map:character:move error: ' + error.message) @@ -102,7 +89,6 @@ export default class CharacterMove extends BaseEvent { 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++) { diff --git a/src/services/characterMoveService.ts b/src/services/characterMoveService.ts index eab632f..dcf4e56 100644 --- a/src/services/characterMoveService.ts +++ b/src/services/characterMoveService.ts @@ -12,70 +12,6 @@ import type { Server } from 'socket.io' type Position = { positionX: number; positionY: number } export type Node = Position & { parent?: Node; g: number; h: number; f: number } -class PriorityQueue { - private items: T[] = [] - constructor(private compare: (a: T, b: T) => number) {} - - enqueue(item: T): void { - this.items.push(item) - this.siftUp(this.items.length - 1) - } - - dequeue(): T | undefined { - if (this.items.length === 0) return undefined - - const result = this.items[0] - const last = this.items.pop() - - if (this.items.length > 0 && last !== undefined) { - this.items[0] = last - this.siftDown(0) - } - - return result - } - - get length(): number { - return this.items.length - } - - private siftUp(index: number): void { - while (index > 0) { - const parentIndex = Math.floor((index - 1) / 2) - if (this.compare(this.items[index]!, this.items[parentIndex]!) < 0) { - ;[this.items[index], this.items[parentIndex]] = [this.items[parentIndex]!, this.items[index]!] - index = parentIndex - } else { - break - } - } - } - - private siftDown(index: number): void { - const length = this.items.length - while (true) { - let minIndex = index - const leftChild = 2 * index + 1 - const rightChild = 2 * index + 2 - - if (leftChild < length && this.compare(this.items[leftChild]!, this.items[minIndex]!) < 0) { - minIndex = leftChild - } - if (rightChild < length && this.compare(this.items[rightChild]!, this.items[minIndex]!) < 0) { - minIndex = rightChild - } - - if (minIndex === index) break - ;[this.items[index], this.items[minIndex]] = [this.items[minIndex]!, this.items[index]!] - index = minIndex - } - } - - clear(): void { - this.items = [] - } -} - class CharacterMoveService extends BaseService { private io: Server = SocketManager.getIO() @@ -182,12 +118,14 @@ class CharacterMoveService extends BaseService { private findPath(start: Position, end: Position, grid: number[][]): Node[] { const openList = new PriorityQueue((a, b) => a.f - b.f) const closedSet = new Set() - const MAX_ITERATIONS = 1000 // Prevent infinite loops for impossible paths + const MAX_ITERATIONS = 1000 let iterations = 0 const getKey = (p: Position) => `${p.positionX},${p.positionY}` + const gScoreMap = new Map() - openList.enqueue({ ...start, g: 0, h: 0, f: 0 }) + openList.enqueue({ ...start, g: 0, h: this.getDistance(start, end), f: this.getDistance(start, end) }) + gScoreMap.set(getKey(start), 0) try { while (openList.length > 0 && iterations < MAX_ITERATIONS) { @@ -199,28 +137,34 @@ class CharacterMoveService extends BaseService { return this.reconstructPath(current) } - closedSet.add(getKey(current)) + const currentKey = getKey(current) + closedSet.add(currentKey) const neighbors = this.getValidNeighbors(current, grid, end) for (const neighbor of neighbors) { - if (closedSet.has(getKey(neighbor))) continue + const neighborKey = getKey(neighbor) + if (closedSet.has(neighborKey)) continue - const g = current.g + this.getDistance(current, neighbor) - const h = this.getDistance(neighbor, end) - const f = g + h + const tentativeGScore = current.g + this.getDistance(current, neighbor) - const node: Node = { ...neighbor, g, h, f, parent: current } - openList.enqueue(node) + if (!gScoreMap.has(neighborKey) || tentativeGScore < gScoreMap.get(neighborKey)!) { + gScoreMap.set(neighborKey, tentativeGScore) + const h = this.getDistance(neighbor, end) + const f = tentativeGScore + h + + const node: Node = { ...neighbor, g: tentativeGScore, h, f, parent: current } + openList.enqueue(node) + } } } this.logger.warn(`Path not found after ${iterations} iterations`) - return [] // No path found + return [] } finally { - // Clean up resources - while (openList.length > 0) openList.dequeue() + openList.clear() closedSet.clear() + gScoreMap.clear() } } @@ -233,6 +177,14 @@ class CharacterMoveService extends BaseService { .filter((pos) => this.isValidPosition(pos, grid, end)) } + private isValidPosition(pos: Position, grid: number[][], end: Position): boolean { + if (pos.positionX < 0 || pos.positionY < 0 || pos.positionX >= grid[0]!.length || pos.positionY >= grid.length) { + return false + } + + return grid[pos.positionY]![pos.positionX] === 0 || (pos.positionX === end.positionX && pos.positionY === end.positionY) + } + isValidStep(current: { positionX: number; positionY: number }, next: { positionX: number; positionY: number }): boolean { return Math.abs(next.positionX - current.positionX) <= 1 && Math.abs(next.positionY - current.positionY) <= 1 } @@ -254,16 +206,6 @@ class CharacterMoveService extends BaseService { } } - private isValidPosition(pos: Position, grid: number[][], end: Position): boolean { - return ( - pos.positionX >= 0 && - pos.positionY >= 0 && - pos.positionX < grid[0]!.length && - pos.positionY < grid.length && - (grid[pos.positionY]![pos.positionX] === 0 || (pos.positionX === end.positionX && pos.positionY === end.positionY)) - ) - } - private getDistance(a: Position, b: Position): number { const dx = Math.abs(a.positionX - b.positionX) const dy = Math.abs(a.positionY - b.positionY) @@ -302,7 +244,7 @@ class CharacterMoveService extends BaseService { return { isValid: true, maxAllowedDistance: 0, actualDistance: 0 } } - const maxAllowedDistance = Math.ceil((stepDelay / 1000) * 2) + const maxAllowedDistance = Math.ceil((stepDelay / 1000) * (config.ALLOW_DIAGONAL_MOVEMENT ? 2.5 : 2)) const actualDistance = Math.sqrt(Math.pow(currentX - lastKnownPosition.x, 2) + Math.pow(currentY - lastKnownPosition.y, 2)) return { @@ -311,14 +253,70 @@ class CharacterMoveService extends BaseService { actualDistance } } - - public validateRequestDistance(currentX: number, currentY: number, targetX: number, targetY: number, maxRequestDistance: number): { isValid: boolean; distance: number } { - const requestDistance = Math.sqrt(Math.pow(targetX - currentX, 2) + Math.pow(targetY - currentY, 2)) - return { - isValid: requestDistance <= maxRequestDistance, - distance: requestDistance - } - } } export default new CharacterMoveService() + +class PriorityQueue { + private items: T[] = [] + constructor(private compare: (a: T, b: T) => number) {} + + enqueue(item: T): void { + this.items.push(item) + this.siftUp(this.items.length - 1) + } + + dequeue(): T | undefined { + if (this.items.length === 0) return undefined + + const result = this.items[0] + const last = this.items.pop() + + if (this.items.length > 0 && last !== undefined) { + this.items[0] = last + this.siftDown(0) + } + + return result + } + + get length(): number { + return this.items.length + } + + private siftUp(index: number): void { + while (index > 0) { + const parentIndex = Math.floor((index - 1) / 2) + if (this.compare(this.items[index]!, this.items[parentIndex]!) < 0) { + ;[this.items[index], this.items[parentIndex]] = [this.items[parentIndex]!, this.items[index]!] + index = parentIndex + } else { + break + } + } + } + + private siftDown(index: number): void { + const length = this.items.length + while (true) { + let minIndex = index + const leftChild = 2 * index + 1 + const rightChild = 2 * index + 2 + + if (leftChild < length && this.compare(this.items[leftChild]!, this.items[minIndex]!) < 0) { + minIndex = leftChild + } + if (rightChild < length && this.compare(this.items[rightChild]!, this.items[minIndex]!) < 0) { + minIndex = rightChild + } + + if (minIndex === index) break + ;[this.items[index], this.items[minIndex]] = [this.items[minIndex]!, this.items[index]!] + index = minIndex + } + } + + clear(): void { + this.items = [] + } +}