import { Server } from 'socket.io'; import { TSocket, ExtendedCharacter } from '../../utilities/types'; import ZoneManager from '../../managers/zoneManager'; import prisma from '../../utilities/prisma'; import { AStar } from '../../utilities/player/aStar'; import Rotation from '../../utilities/player/rotation'; import ZoneRepository from '../../repositories/zoneRepository'; import { Character } from '@prisma/client'; const moveTokens = new Map(); export default function setupCharacterMove(socket: TSocket, io: Server) { socket.on('character:initMove', handleCharacterMove(socket, io)); } const handleCharacterMove = (socket: TSocket, io: Server) => async ({ positionX, positionY }: { positionX: number; positionY: number }) => { const { character } = socket; if (!character) return console.error('character:move error', 'Character not found'); const grid = await ZoneManager.getGrid(character.zoneId); if (!grid?.length) return console.error('character:move error', 'Grid not found or empty'); const start = { x: Math.floor(character.positionX), y: Math.floor(character.positionY) }; const end = { x: Math.floor(positionX), y: Math.floor(positionY) }; if (isObstacle(end, grid)) return socket.emit('character:moveError', 'Destination is an obstacle'); const path = AStar.findPath(start, end, grid); if (!path.length) return socket.emit('character:moveError', 'No valid path found'); moveTokens.set(character.id, Symbol('moveToken')); character.isMoving = true; io.in(character.zoneId.toString()).emit('character:move', character); moveAlongPath(socket, io, path, grid).catch(console.error); }; async function moveAlongPath(socket: TSocket, io: Server, path: Array<{ x: number; y: number }>, grid: number[][]) { const { character } = socket; if (!character) 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 start = path[i]; const end = path[i + 1]; while (Date.now() - startTime < stepDuration) { if (moveTokens.get(character.id) !== moveToken) return; const progress = (Date.now() - startTime) / stepDuration; const current = interpolatePosition(start, end, progress); if (isObstacle(current, grid)) { await updateCharacterPosition(character, start, Rotation.calculate(start.x, start.y, end.x, end.y), socket, io); return; } const tp = await prisma.zoneEventTile.findFirst({ where: { zoneId: character.zoneId, type: 'TELEPORT', positionX: current.x, positionY: current.y }, include: { teleport: true } }); if (tp?.teleport) { await handleTeleport(socket, io, character, tp.teleport, start, end); return; } await updateCharacterPosition(character, current, Rotation.calculate(start.x, start.y, end.x, end.y), socket, io); await new Promise(resolve => setTimeout(resolve, updateInterval)); } } if (moveTokens.get(character.id) === moveToken) { await updateCharacterPosition(character, path[path.length - 1], character.rotation, socket, io); character.isMoving = false; moveTokens.delete(character.id); } } async function handleTeleport(socket: TSocket, io: Server, character: ExtendedCharacter, teleport: any, start: { x: number; y: number }, end: { x: number; y: number }) { if (teleport.toZoneId === character.zoneId) return; const zone = await ZoneRepository.getById(teleport.toZoneId); if (!zone) return; character.isMoving = false; character.zoneId = teleport.toZoneId; moveTokens.delete(character.id); socket.leave(character.zoneId.toString()); socket.join(teleport.toZoneId.toString()); await updateCharacterPosition( character, { x: teleport.toPositionX, y: teleport.toPositionY }, Rotation.calculate(start.x, start.y, end.x, end.y), socket, io, teleport.toZoneId ); } async function updateCharacterPosition( character: ExtendedCharacter, position: { x: number; y: number }, rotation: number, socket: TSocket, io: Server, newZoneId?: number ) { const oldZoneId = character.zoneId; Object.assign(character, { positionX: position.x, positionY: position.y, rotation, zoneId: newZoneId || character.zoneId }); if (newZoneId && newZoneId !== oldZoneId) { io.to(oldZoneId.toString()).emit('zone:character:leave', character); io.to(newZoneId.toString()).emit('zone:character:join', character); ZoneManager.removeCharacterFromZone(oldZoneId, character as Character); ZoneManager.addCharacterToZone(newZoneId, character as Character); } else { ZoneManager.updateCharacterInZone(character.zoneId, character); } await prisma.character.update({ where: { id: character.id }, data: { positionX: position.x, positionY: position.y, rotation, zoneId: character.zoneId } }); io.in(character.zoneId.toString()).emit('character:move', character); socket.emit('character:dataUpdated', character); } 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 }); const isObstacle = ({ x, y }: { x: number; y: number }, grid: number[][]) => { const gridX = Math.floor(x); const gridY = Math.floor(y); return grid[gridY]?.[gridX] === 1 || grid[gridY]?.[Math.ceil(x)] === 1 || grid[Math.ceil(y)]?.[gridX] === 1 || grid[Math.ceil(y)]?.[Math.ceil(x)] === 1; };