diff --git a/src/events/character/Move.ts b/src/events/character/Move.ts index 495a0e7..5fa2b04 100644 --- a/src/events/character/Move.ts +++ b/src/events/character/Move.ts @@ -1,145 +1,78 @@ import { Server } from 'socket.io' -import { TSocket } from '../../utilities/Types' +import { TSocket, ExtendedCharacter } from '../../utilities/Types' import ZoneManager from '../../managers/ZoneManager' import prisma from '../../utilities/Prisma' -import { AStar, type Node } from '../../utilities/Player/AStar' +import { AStar } from '../../utilities/Player/AStar' import Rotation from '../../utilities/Player/Rotation' -import { ExtendedCharacter as Character } from '../../utilities/Types' -interface SocketResponse { - position_x: number - position_y: number -} - -const characterMoveTokens = new Map() +const moveTokens = new Map() export default function setupCharacterMove(socket: TSocket, io: Server) { - socket.on('character:move', async (data: SocketResponse) => { - try { - console.log('character:move requested', data) + socket.on('character:move', async ({ position_x, position_y }: { position_x: number; position_y: number }) => { + const { character } = socket + if (!character) return console.error('character:move error', 'Character not found') - if (!socket.character) { - console.error('character:move error', 'Character not found') - return - } + moveTokens.set(character.id, Symbol('moveToken')) + const grid = await ZoneManager.getGrid(character.zoneId) + if (!grid?.length) return console.error('character:move error', 'Grid not found or empty') - const oldMoveToken = characterMoveTokens.get(socket.character.id) - if (oldMoveToken) { - characterMoveTokens.delete(socket.character.id) - } + const start = { x: Math.floor(character.position_x), y: Math.floor(character.position_y) } + const end = { x: Math.floor(position_x), y: Math.floor(position_y) } - const moveToken = Symbol('moveToken') - characterMoveTokens.set(socket.character.id, moveToken) + if (isObstacle(end, grid)) return socket.emit('character:moveError', 'Destination is an obstacle') - const grid = await ZoneManager.getGrid(socket.character.zoneId) + const path = AStar.findPath(start, end, grid) + if (!path.length) return socket.emit('character:moveError', 'No valid path found') - if (!grid || grid.length === 0) { - console.error('character:move error', 'Grid not found or empty') - return - } - - const start = { x: Math.floor(socket.character.position_x), y: Math.floor(socket.character.position_y) } - const end = { x: Math.floor(data.position_x), y: Math.floor(data.position_y) } - - console.log('Pathfinding from', start, 'to', end) - console.log('Grid dimensions:', grid.length, 'x', grid[0].length) - - // Check if the destination is an obstacle - if (isObstacle(end, grid)) { - console.log('character:move error', 'Destination is an obstacle') - socket.emit('character:moveError', 'Destination is an obstacle') - return - } - - const path = AStar.findPath(start, end, grid) - - if (path.length > 0) { - socket.character.isMoving = true - io.in(socket.character.zoneId.toString()).emit('character:moved', socket.character) - - moveAlongPath(socket, io, path, grid, moveToken).catch(console.error) - } else { - console.log('character:move error', 'No valid path found') - socket.emit('character:moveError', 'No valid path found') - } - } catch (error) { - console.error('character:move error', error) - if (socket.character) { - socket.character.isMoving = false - io.in(socket.character.zoneId.toString()).emit('character:moved', socket.character) - } - } + character.isMoving = true + io.in(character.zoneId.toString()).emit('character:moved', character) + moveAlongPath(socket, io, path, grid).catch(console.error) }) } -async function moveAlongPath(socket: TSocket, io: Server, path: Node[], grid: number[][], moveToken: Symbol) { - if (!socket.character) return +async function moveAlongPath(socket: TSocket, io: Server, path: Array<{ x: number; y: number }>, grid: number[][]) { + const { character } = socket + if (!character) return - const totalSteps = path.length - const updateInterval = 50 // milliseconds between updates - - for (let step = 0; step < totalSteps - 1; step++) { - if (characterMoveTokens.get(socket.character.id) !== moveToken) { - console.log('Movement cancelled for character', socket.character.id) - return - } + const moveToken = moveTokens.get(character.id) + const stepDuration = 250 + const updateInterval = 50 + for (let i = 0; i < path.length - 1; i++) { const startTime = Date.now() - const stepDuration = 250 // 250ms per tile - while (Date.now() - startTime < stepDuration) { - if (characterMoveTokens.get(socket.character.id) !== moveToken) { - console.log('Movement cancelled for character', socket.character.id) - return - } + if (moveTokens.get(character.id) !== moveToken) return const progress = (Date.now() - startTime) / stepDuration - const currentPosition = interpolatePosition(path[step], path[step + 1], progress) + const current = interpolatePosition(path[i], path[i + 1], progress) - if (isObstacle(currentPosition, grid)) { - console.log('Obstacle encountered at', currentPosition) - break - } + if (isObstacle(current, grid)) break - const rotation = Rotation.calculate(path[step].x, path[step].y, path[step + 1].x, path[step + 1].y) - - await updateCharacterPosition(socket.character, currentPosition.x, currentPosition.y, rotation) - - ZoneManager.updateCharacterInZone(socket.character.zoneId, socket.character) - io.in(socket.character.zoneId.toString()).emit('character:moved', socket.character) - - await new Promise(resolve => setTimeout(resolve, updateInterval)) + await updateCharacter(character, current, Rotation.calculate(path[i].x, path[i].y, path[i + 1].x, path[i + 1].y)) + io.in(character.zoneId.toString()).emit('character:moved', character) + await new Promise((resolve) => setTimeout(resolve, updateInterval)) } } - if (socket.character && characterMoveTokens.get(socket.character.id) === moveToken) { - socket.character.isMoving = false - characterMoveTokens.delete(socket.character.id) - io.in(socket.character.zoneId.toString()).emit('character:moved', socket.character) + if (moveTokens.get(character.id) === moveToken) { + character.isMoving = false + moveTokens.delete(character.id) + io.in(character.zoneId.toString()).emit('character:moved', character) } } -function interpolatePosition(start: Node, end: Node, progress: number): Node { - return { - f: 0, g: 0, h: 0, - x: start.x + (end.x - start.x) * progress, - y: start.y + (end.y - start.y) * progress - } -} +const interpolatePosition = (start: { x: number; y: number }, end: { x: number; y: number }, progress: number) => ({ + x: start.x + (end.x - start.x) * progress, + y: start.y + (end.y - start.y) * progress +}) -function isObstacle(position: { x: number; y: number }, grid: number[][]): boolean { - const x = Math.floor(position.x) - const y = Math.floor(position.y) - return grid[y] && grid[y][x] === 1 -} - -async function updateCharacterPosition(character: Character, x: number, y: number, rotation: number) { - character.position_x = x - character.position_y = y - character.rotation = rotation +const isObstacle = ({ x, y }: { x: number; y: number }, grid: number[][]) => grid[Math.floor(y)]?.[Math.floor(x)] === 1 +async function updateCharacter(character: ExtendedCharacter, { x, y }: { x: number; y: number }, rotation: number) { + Object.assign(character, { position_x: x, position_y: y, rotation }) + ZoneManager.updateCharacterInZone(character.zoneId, character) await prisma.character.update({ where: { id: character.id }, data: { position_x: x, position_y: y, rotation } }) -} \ No newline at end of file +} diff --git a/src/events/chat/SendMessage.ts b/src/events/chat/SendMessage.ts index 106777b..e46ec8b 100644 --- a/src/events/chat/SendMessage.ts +++ b/src/events/chat/SendMessage.ts @@ -20,7 +20,6 @@ export default function (socket: TSocket, io: Server) { const zone = await ZoneRepository.getById(character.zoneId) if (!zone) return - callback(true) io.to(zone.id.toString()).emit('chat:message', { diff --git a/src/utilities/Chat.ts b/src/utilities/Chat.ts index 07b0feb..ad2ece6 100644 --- a/src/utilities/Chat.ts +++ b/src/utilities/Chat.ts @@ -5,8 +5,7 @@ export function isCommand(message: string, command?: string) { return message.startsWith(':') } -export function getArgs(command: string, message: string): string[] | undefined -{ +export function getArgs(command: string, message: string): string[] | undefined { if (!isCommand(message, command)) return return message.split(`:${command} `)[1].split(' ') -} \ No newline at end of file +} diff --git a/src/utilities/Player/AStar.ts b/src/utilities/Player/AStar.ts index afb9687..f246f42 100644 --- a/src/utilities/Player/AStar.ts +++ b/src/utilities/Player/AStar.ts @@ -1,55 +1,45 @@ -interface Position { - x: number - y: number -} - -export interface Node extends Position { - parent?: Node - g: number - h: number - f: number -} +type Position = { x: number; y: number } +export type Node = Position & { parent?: Node; g: number; h: number; f: number } export class AStar { - private static readonly ORTHOGONAL_DIRECTIONS: Position[] = [ - { x: 0, y: -1 }, { x: 0, y: 1 }, { x: -1, y: 0 }, { x: 1, y: 0 } + private static readonly DIRECTIONS = [ + { x: 0, y: -1 }, + { x: 0, y: 1 }, + { x: -1, y: 0 }, + { x: 1, y: 0 }, + { x: -1, y: -1 }, + { x: -1, y: 1 }, + { x: 1, y: -1 }, + { x: 1, y: 1 } ] - private static readonly DIAGONAL_DIRECTIONS: Position[] = [ - { x: -1, y: -1 }, { x: -1, y: 1 }, { x: 1, y: -1 }, { x: 1, y: 1 } - ] - - static findPath(start: Position, end: Position, grid: number[][], allowDiagonal: boolean = false): Node[] { - const openList: Node[] = [] + static findPath(start: Position, end: Position, grid: number[][], allowDiagonal = false): Node[] { + const openList: Node[] = [{ ...start, g: 0, h: 0, f: 0 }] const closedSet = new Set() - - const startNode: Node = { ...start, g: 0, h: 0, f: 0 } - openList.push(startNode) + const getKey = (p: Position) => `${p.x},${p.y}` while (openList.length > 0) { - const currentNode = this.getLowestFScoreNode(openList) + const current = openList.reduce((min, node) => (node.f < min.f ? node : min)) + if (current.x === end.x && current.y === end.y) return this.reconstructPath(current) - if (this.isEndNode(currentNode, end)) { - return this.reconstructPath(currentNode) - } + openList.splice(openList.indexOf(current), 1) + closedSet.add(getKey(current)) - this.removeNodeFromOpenList(openList, currentNode) - closedSet.add(this.nodeToString(currentNode)) + const neighbors = this.DIRECTIONS.slice(0, allowDiagonal ? 8 : 4) + .map((dir) => ({ x: current.x + dir.x, y: current.y + dir.y })) + .filter((pos) => this.isValidPosition(pos, grid, end)) - for (const neighbor of this.getValidNeighbors(currentNode, grid, end, allowDiagonal)) { - if (closedSet.has(this.nodeToString(neighbor))) continue + for (const neighbor of neighbors) { + if (closedSet.has(getKey(neighbor))) continue - const tentativeGScore = currentNode.g + this.getDistance(currentNode, neighbor) + const g = current.g + this.getDistance(current, neighbor) + const existing = openList.find((node) => node.x === neighbor.x && node.y === neighbor.y) - if (!this.isInOpenList(openList, neighbor) || tentativeGScore < neighbor.g) { - neighbor.parent = currentNode - neighbor.g = tentativeGScore - neighbor.h = this.heuristic(neighbor, end) - neighbor.f = neighbor.g + neighbor.h - - if (!this.isInOpenList(openList, neighbor)) { - openList.push(neighbor) - } + 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) } } } @@ -57,76 +47,21 @@ export class AStar { return [] // No path found } - private static getLowestFScoreNode(nodes: Node[]): Node { - return nodes.reduce((min, node) => (node.f < min.f ? node : min)) - } - - private static isEndNode(node: Node, end: Position): boolean { - return node.x === end.x && node.y === end.y - } - - private static removeNodeFromOpenList(openList: Node[], node: Node): void { - const index = openList.findIndex((n) => n.x === node.x && n.y === node.y) - if (index !== -1) openList.splice(index, 1) - } - - private static nodeToString(node: Position): string { - return `${node.x},${node.y}` - } - - private static getValidNeighbors(node: Node, grid: number[][], end: Position, allowDiagonal: boolean): Node[] { - const directions = allowDiagonal - ? [...this.ORTHOGONAL_DIRECTIONS, ...this.DIAGONAL_DIRECTIONS] - : this.ORTHOGONAL_DIRECTIONS - - return directions - .map((dir) => ({ - x: node.x + dir.x, - y: node.y + dir.y, - g: 0, - h: 0, - f: 0 - })) - .filter((pos) => this.isValidPosition(pos, grid, end)) - } - private static isValidPosition(pos: Position, grid: number[][], end: Position): boolean { - const { x, y } = pos - - if (!grid || grid.length === 0 || !Array.isArray(grid[0])) { - return false; - } - - const height = grid.length; - const width = grid[0].length; - - return x >= 0 && x < width && y >= 0 && y < height && - (grid[y][x] === 0 || (x === end.x && y === end.y)); - } - - private static isInOpenList(openList: Node[], node: Position): boolean { - return openList.some((n) => n.x === node.x && n.y === node.y) + return pos.x >= 0 && pos.y >= 0 && pos.x < grid[0].length && pos.y < grid.length && (grid[pos.y][pos.x] === 0 || (pos.x === end.x && pos.y === end.y)) } private static getDistance(a: Position, b: Position): number { - const dx = Math.abs(a.x - b.x) - const dy = Math.abs(a.y - b.y) + const dx = Math.abs(a.x - b.x), + dy = Math.abs(a.y - b.y) return Math.sqrt(dx * dx + dy * dy) } - private static heuristic(node: Position, goal: Position): number { - return this.getDistance(node, goal) - } - private static reconstructPath(endNode: Node): Node[] { const path: Node[] = [] - let currentNode: Node | undefined = endNode - - while (currentNode) { - path.unshift(currentNode) - currentNode = currentNode.parent + for (let current: Node | undefined = endNode; current; current = current.parent) { + path.unshift(current) } - return path } -} \ No newline at end of file +} diff --git a/src/utilities/Types.ts b/src/utilities/Types.ts index a254d53..1a77c37 100644 --- a/src/utilities/Types.ts +++ b/src/utilities/Types.ts @@ -31,4 +31,4 @@ export type TAsset = { // export type TCharacter = Socket & { // user?: User // character?: Character -// } \ No newline at end of file +// }