From e21c03ee3b878198e6986e31678e5da807252c42 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Wed, 12 Feb 2025 02:44:59 +0100 Subject: [PATCH] Moving almost works --- src/events/map/characterMove.ts | 61 +++++++++-- src/services/characterMoveService.ts | 153 ++++++++++++++++++++------- 2 files changed, 165 insertions(+), 49 deletions(-) diff --git a/src/events/map/characterMove.ts b/src/events/map/characterMove.ts index 203c411..d6db149 100644 --- a/src/events/map/characterMove.ts +++ b/src/events/map/characterMove.ts @@ -9,9 +9,11 @@ import TeleportService from '@/services/characterTeleportService' export default class CharacterMove extends BaseEvent { private readonly characterService = CharacterService - private readonly STEP_DELAY = 150 // Milliseconds between each tile movement - private readonly THROTTLE_DELAY = 230 // Minimum time between movement requests + private readonly STEP_DELAY = 100 + private readonly THROTTLE_DELAY = 230 + private readonly MAX_REQUEST_DISTANCE = 30 // Maximum allowed distance for movement requests private movementTimeout: NodeJS.Timeout | null = null + private lastKnownPosition: { x: number; y: number } | null = null public listen(): void { this.socket.on(SocketEvent.MAP_CHARACTER_MOVE, this.handleEvent.bind(this)) @@ -25,14 +27,41 @@ export default class CharacterMove extends BaseEvent { return } + const character = mapCharacter.getCharacter() + const currentX = character.getPositionX() + const currentY = character.getPositionY() + + // Validate current position against last known position + const movementValidation = this.characterService.validateMovementDistance(currentX, currentY, this.lastKnownPosition, this.STEP_DELAY, mapCharacter.isMoving) + + if (!movementValidation.isValid) { + this.logger.warn(`Suspicious movement detected: ${this.socket.characterId}`) + // Force position reset + character.setPositionX(this.lastKnownPosition!.x).setPositionY(this.lastKnownPosition!.y) + this.broadcastMovement(character, false) + return + } + + // Validate requested position distance + const requestValidation = this.characterService.validateRequestDistance(currentX, currentY, positionX, positionY, this.MAX_REQUEST_DISTANCE) + + if (!requestValidation.isValid) { + this.logger.warn(`Invalid movement distance detected: ${this.socket.characterId}`) + return + } + // Cancel any ongoing movement this.cancelCurrentMovement(mapCharacter) - // Throttle rapid movement requests - if (this.isThrottled(`movement_${this.socket.characterId}`, this.THROTTLE_DELAY)) { + // Enhanced throttling with position tracking + const throttleKey = `movement_${this.socket.characterId}` + if (this.isThrottled(throttleKey, this.THROTTLE_DELAY)) { return } + // Update last known position + this.lastKnownPosition = { x: currentX, y: currentY } + // Calculate path to target position const path = await this.characterService.calculatePath(mapCharacter.character, Math.floor(positionX), Math.floor(positionY)) @@ -51,10 +80,9 @@ export default class CharacterMove extends BaseEvent { } private cancelCurrentMovement(mapCharacter: MapCharacter): void { - if (mapCharacter.isMoving) { - mapCharacter.isMoving = false - mapCharacter.currentPath = null - } + if (!mapCharacter.isMoving) return + mapCharacter.isMoving = false + mapCharacter.currentPath = null } private async moveAlongPath(mapCharacter: MapCharacter): Promise { @@ -64,11 +92,20 @@ export default class CharacterMove extends BaseEvent { if (!path?.length) return try { + let lastMoveTime = Date.now() + for (let i = 0; i < path.length - 1; i++) { if (!mapCharacter.isMoving || mapCharacter.currentPath !== path) { return } + // Ensure minimum time between moves + const currentTime = Date.now() + const timeSinceLastMove = currentTime - lastMoveTime + if (timeSinceLastMove < this.STEP_DELAY) { + await new Promise((resolve) => setTimeout(resolve, this.STEP_DELAY - timeSinceLastMove)) + } + const [currentTile, nextTile] = [path[i], path[i + 1]] if (!currentTile || !nextTile) { @@ -102,9 +139,15 @@ export default class CharacterMove extends BaseEvent { this.broadcastMovement(character, true) // Apply movement delay between steps - await new Promise((resolve) => setTimeout(resolve, this.STEP_DELAY)) if (i < path.length - 2) { + await new Promise((resolve) => setTimeout(resolve, this.STEP_DELAY)) } + + this.lastKnownPosition = { + x: character.getPositionX(), + y: character.getPositionY() + } + lastMoveTime = Date.now() } } finally { if (mapCharacter.isMoving && mapCharacter.currentPath === path) { diff --git a/src/services/characterMoveService.ts b/src/services/characterMoveService.ts index f4fe447..2f1186d 100644 --- a/src/services/characterMoveService.ts +++ b/src/services/characterMoveService.ts @@ -6,8 +6,40 @@ import MapManager from '@/managers/mapManager' 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.items.sort(this.compare) + } + + dequeue(): T | undefined { + return this.items.shift() + } + + get length(): number { + return this.items.length + } +} + class CharacterMoveService extends BaseService { - private readonly MOVEMENT_DELAY_MS = 90 + // Rotation lookup table for better performance + private readonly ROTATION_MAP = { + diagonal: { + 'up-left': 7, + 'down-right': 3, + 'up-right': 5, + 'down-left': 1 + }, + straight: { + left: 6, + right: 2, + down: 4, + up: 0 + } + } private readonly DIRECTIONS = [ { x: 0, y: -1 }, // Up @@ -21,6 +53,10 @@ class CharacterMoveService extends BaseService { ] public async calculatePath(character: Character, targetX: number, targetY: number): Promise { + if (!this.validateInput(character, targetX, targetY)) { + return null + } + const map = MapManager.getMapById(character.map.id) const grid = await map?.getGrid() @@ -48,72 +84,80 @@ class CharacterMoveService extends BaseService { return this.findPath(start, end, grid) } - public calculateRotation(X1: number, Y1: number, X2: number, Y2: number): number { - if (config.ALLOW_DIAGONAL_MOVEMENT) { - // Check diagonal movements - if (X1 > X2 && Y1 > Y2) { - return 7 - } else if (X1 < X2 && Y1 < Y2) { - return 3 - } else if (X1 > X2 && Y1 < Y2) { - return 5 - } else if (X1 < X2 && Y1 > Y2) { - return 1 - } + private validateInput(character: Character, targetX: number, targetY: number): boolean { + if (!character || !character.map) { + this.logger.error('Invalid character or map data') + return false } - // Non-diagonal movements - if (X1 > X2) { - return 6 - } else if (X1 < X2) { - return 2 - } else if (Y1 < Y2) { - return 4 - } else if (Y1 > Y2) { - return 0 + if (!Number.isFinite(targetX) || !Number.isFinite(targetY)) { + this.logger.error('Invalid target coordinates') + return false } - return 0 // Default case + return true } - public async applyMovementDelay(): Promise { - await new Promise((resolve) => setTimeout(resolve, this.MOVEMENT_DELAY_MS)) + public calculateRotation(X1: number, Y1: number, X2: number, Y2: number): number { + if (config.ALLOW_DIAGONAL_MOVEMENT) { + // Check diagonal movements using lookup table + if (X1 > X2 && Y1 > Y2) return this.ROTATION_MAP.diagonal['up-left'] + if (X1 < X2 && Y1 < Y2) return this.ROTATION_MAP.diagonal['down-right'] + if (X1 > X2 && Y1 < Y2) return this.ROTATION_MAP.diagonal['up-right'] + if (X1 < X2 && Y1 > Y2) return this.ROTATION_MAP.diagonal['down-left'] + } + + // Non-diagonal movements using lookup table + if (X1 > X2) return this.ROTATION_MAP.straight.left + if (X1 < X2) return this.ROTATION_MAP.straight.right + if (Y1 < Y2) return this.ROTATION_MAP.straight.down + if (Y1 > Y2) return this.ROTATION_MAP.straight.up + + return this.ROTATION_MAP.straight.up // Default case } private findPath(start: Position, end: Position, grid: number[][]): Node[] { - const openList: Node[] = [{ ...start, g: 0, h: 0, f: 0 }] + const openList = new PriorityQueue((a, b) => a.f - b.f) + openList.enqueue({ ...start, g: 0, h: 0, f: 0 }) const closedSet = new Set() const getKey = (p: Position) => `${p.positionX},${p.positionY}` while (openList.length > 0) { - const current = openList.reduce((min, node) => (node.f < min.f ? node : min)) - if (current.positionX === end.positionX && current.positionY === end.positionY) return this.reconstructPath(current) + const current = openList.dequeue() + if (!current) break + + if (current.positionX === end.positionX && current.positionY === end.positionY) { + return this.reconstructPath(current) + } - openList.splice(openList.indexOf(current), 1) closedSet.add(getKey(current)) - const neighbors = this.DIRECTIONS.slice(0, config.ALLOW_DIAGONAL_MOVEMENT ? 8 : 4) - .map((dir) => ({ positionX: current.positionX + dir.x, positionY: current.positionY + dir.y })) - .filter((pos) => this.isValidPosition(pos, grid, end)) + const neighbors = this.getValidNeighbors(current, grid, end) for (const neighbor of neighbors) { if (closedSet.has(getKey(neighbor))) continue const g = current.g + this.getDistance(current, neighbor) - const existing = openList.find((node) => node.positionX === neighbor.positionX && node.positionY === neighbor.positionY) + const h = this.getDistance(neighbor, end) + const f = g + h - if (!existing || g < existing.g) { - const h = this.getDistance(neighbor, end) - const node: Node = { ...neighbor, g, h, f: g + h, parent: current } - if (!existing) openList.push(node) - else Object.assign(existing, node) - } + const node: Node = { ...neighbor, g, h, f, parent: current } + openList.enqueue(node) } } return [] // No path found } + private getValidNeighbors(current: Position, grid: number[][], end: Position): Position[] { + return this.DIRECTIONS.slice(0, config.ALLOW_DIAGONAL_MOVEMENT ? 8 : 4) + .map((dir) => ({ + positionX: current.positionX + dir.x, + positionY: current.positionY + dir.y + })) + .filter((pos) => this.isValidPosition(pos, grid, end)) + } + 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)) } @@ -127,11 +171,40 @@ class CharacterMoveService extends BaseService { private reconstructPath(endNode: Node): Node[] { const path: Node[] = [] - for (let current: Node | undefined = endNode; current; current = current.parent) { + let current: Node | undefined = endNode + while (current) { path.unshift(current) + current = current.parent } return path } + + public validateMovementDistance(currentX: number, currentY: number, lastKnownPosition: { x: number; y: number } | null, stepDelay: number, isMoving: boolean): { isValid: boolean; maxAllowedDistance: number; actualDistance: number } { + if (!lastKnownPosition || !isMoving) { + return { isValid: true, maxAllowedDistance: 0, actualDistance: 0 } + } + + const maxAllowedDistance = Math.ceil((stepDelay / 1000) * 2) + const actualDistance = Math.sqrt(Math.pow(currentX - lastKnownPosition.x, 2) + Math.pow(currentY - lastKnownPosition.y, 2)) + + return { + isValid: actualDistance <= maxAllowedDistance, + maxAllowedDistance, + 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 + } + } + + public 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 + } } export default new CharacterMoveService()