calculate_movement
Calculate movement paths, reachable squares, or adjacent squares with terrain support for RPG encounters in ChatRPG.
Instructions
Calculate movement paths, reachable squares, or adjacent squares with terrain support
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| mode | No | path | |
| encounterId | No | ||
| from | Yes | ||
| to | No | ||
| movement | No | ||
| gridWidth | No | ||
| gridHeight | No | ||
| creaturesBlock | No |
Implementation Reference
- src/modules/spatial.ts:1716-1923 (handler)The core handler function for 'calculate_movement' tool. It supports three modes: 'path' (A* pathfinding to target), 'reach' (flood-fill reachable squares within movement budget), and 'adjacent' (checks 8 surrounding squares). Integrates with encounter terrain and participant positions for realistic D&D 5e movement costs.export function calculateMovement(input: CalculateMovementInput): string { const content: string[] = []; const mode = input.mode || 'path'; // Get encounter state if provided let encounter: ReturnType<typeof getEncounterState> | undefined; let terrain: Map<string, string> = new Map(); let creatures: Map<string, string> = new Map(); if (input.encounterId) { encounter = getEncounterState(input.encounterId); if (!encounter) { throw new Error(`Encounter not found: ${input.encounterId}`); } // Build terrain map if (encounter.terrain) { for (const pos of encounter.terrain.obstacles || []) { terrain.set(pos, 'obstacle'); } for (const pos of encounter.terrain.difficultTerrain || []) { terrain.set(pos, 'difficultTerrain'); } for (const pos of encounter.terrain.water || []) { terrain.set(pos, 'water'); } } // Build creature positions map for (const p of encounter.participants || []) { if (p.position) { creatures.set(`${p.position.x},${p.position.y}`, p.name); } } } const gridWidth = encounter?.terrain?.width || input.gridWidth || 20; const gridHeight = encounter?.terrain?.height || input.gridHeight || 20; const fromPos = { x: input.from.x, y: input.from.y }; switch (mode) { case 'path': { const toPos = { x: input.to!.x, y: input.to!.y }; content.push(centerText('PATH CALCULATION', MOVEMENT_DISPLAY_WIDTH)); content.push(''); content.push(`From: (${fromPos.x}, ${fromPos.y})`); content.push(`To: (${toPos.x}, ${toPos.y})`); content.push(''); content.push(BOX.LIGHT.H.repeat(MOVEMENT_DISPLAY_WIDTH)); content.push(''); // Same position check if (fromPos.x === toPos.x && fromPos.y === toPos.y) { content.push('Distance: 0 feet'); content.push(''); content.push('Already at destination!'); return createBox('PATH', content); } // Simple A* pathfinding const path = findPath(fromPos, toPos, gridWidth, gridHeight, terrain, creatures, input.creaturesBlock); if (path.length === 0) { content.push('STATUS: BLOCKED'); content.push(''); content.push('No path available - destination is unreachable.'); return createBox('PATH', content); } // Calculate total cost let totalCost = 0; let hasDifficultTerrain = false; for (let i = 1; i < path.length; i++) { const pos = path[i]; const key = `${pos.x},${pos.y}`; const terrainType = terrain.get(key) || 'normal'; const cost = TERRAIN_COSTS[terrainType] || 1; if (cost === 2) hasDifficultTerrain = true; totalCost += cost * 5; } content.push(`Distance: ${totalCost} feet`); if (hasDifficultTerrain) { content.push('(includes difficult terrain)'); } content.push(''); // Show path content.push(centerText('PATH WAYPOINTS', MOVEMENT_DISPLAY_WIDTH)); content.push(''); for (let i = 0; i < path.length; i++) { const pos = path[i]; const key = `${pos.x},${pos.y}`; const terrainType = terrain.get(key); let marker = ''; if (i === 0) marker = ' (start)'; else if (i === path.length - 1) marker = ' (end)'; else if (terrainType === 'difficultTerrain') marker = ' [difficult]'; else if (terrainType === 'water') marker = ' [water]'; content.push(` ${i + 1}. (${pos.x}, ${pos.y})${marker}`); } return createBox('PATH', content); } case 'reach': { const movementBudget = input.movement || 30; content.push(centerText('REACHABLE SQUARES', MOVEMENT_DISPLAY_WIDTH)); content.push(''); content.push(`From: (${fromPos.x}, ${fromPos.y})`); content.push(`Movement: ${movementBudget} feet`); content.push(''); content.push(BOX.LIGHT.H.repeat(MOVEMENT_DISPLAY_WIDTH)); content.push(''); // Flood fill to find reachable squares const reachable = findReachable(fromPos, movementBudget, gridWidth, gridHeight, terrain, creatures, input.creaturesBlock); let hasDifficultTerrain = false; for (const pos of reachable) { const key = `${pos.x},${pos.y}`; if (terrain.get(key) === 'difficultTerrain' || terrain.get(key) === 'water') { hasDifficultTerrain = true; break; } } content.push(`Reachable: ${reachable.length} squares`); if (hasDifficultTerrain) { content.push('(movement reduced by difficult terrain)'); } content.push(''); // List reachable positions (limited to prevent huge output) const maxShow = 20; content.push(centerText('POSITIONS', MOVEMENT_DISPLAY_WIDTH)); content.push(''); for (let i = 0; i < Math.min(reachable.length, maxShow); i++) { const pos = reachable[i]; content.push(` (${pos.x}, ${pos.y})`); } if (reachable.length > maxShow) { content.push(` ... and ${reachable.length - maxShow} more`); } return createBox('REACHABLE', content); } case 'adjacent': { content.push(centerText('ADJACENT SQUARES', MOVEMENT_DISPLAY_WIDTH)); content.push(''); content.push(`Position: (${fromPos.x}, ${fromPos.y})`); content.push(''); content.push(BOX.LIGHT.H.repeat(MOVEMENT_DISPLAY_WIDTH)); content.push(''); // Find 8 adjacent squares const adjacent: Array<{ x: number; y: number; status: string }> = []; const directions = [ { dx: -1, dy: -1 }, { dx: 0, dy: -1 }, { dx: 1, dy: -1 }, { dx: -1, dy: 0 }, { dx: 1, dy: 0 }, { dx: -1, dy: 1 }, { dx: 0, dy: 1 }, { dx: 1, dy: 1 }, ]; for (const dir of directions) { const nx = fromPos.x + dir.dx; const ny = fromPos.y + dir.dy; // Check bounds if (nx < 0 || nx >= gridWidth || ny < 0 || ny >= gridHeight) { continue; } const key = `${nx},${ny}`; let status = 'open'; if (terrain.get(key) === 'obstacle') { status = 'blocked (obstacle)'; } else if (creatures.has(key)) { status = `occupied (${creatures.get(key)})`; } else if (terrain.get(key) === 'difficultTerrain') { status = 'difficult terrain'; } else if (terrain.get(key) === 'water') { status = 'water'; } adjacent.push({ x: nx, y: ny, status }); } content.push(`Adjacent: ${adjacent.length} squares`); content.push(''); content.push(centerText('POSITIONS', MOVEMENT_DISPLAY_WIDTH)); content.push(''); for (const pos of adjacent) { content.push(` (${pos.x}, ${pos.y}) - ${pos.status}`); } return createBox('ADJACENT', content); } default: throw new Error(`Unknown mode: ${mode}`); } }
- src/modules/spatial.ts:1675-1693 (schema)Zod input validation schema for the calculate_movement tool, defining parameters like mode (path/reach/adjacent), positions, movement budget, grid size, and options for terrain/creature blocking.export const calculateMovementSchema = z.object({ mode: MovementModeSchema.default('path'), encounterId: z.string().optional(), from: MovementPositionSchema, to: MovementPositionSchema.optional(), movement: z.number().min(0).default(30), gridWidth: z.number().min(5).max(100).default(20), gridHeight: z.number().min(5).max(100).default(20), creaturesBlock: z.boolean().default(false), }).refine((data) => { // Path mode requires 'to' position if (data.mode === 'path' && !data.to) { return false; } return true; }, { message: 'Path mode requires "to" position', });
- src/registry.ts:715-732 (registration)Registration of the 'calculate_movement' tool in the central tool registry. Converts Zod schema to JSON Schema and wraps the handler with validation and error handling.calculate_movement: { name: 'calculate_movement', description: 'Calculate movement paths, reachable squares, or adjacent squares with terrain support', inputSchema: toJsonSchema(calculateMovementSchema), handler: async (args) => { try { const validated = calculateMovementSchema.parse(args); const result = calculateMovement(validated); return success(result); } catch (err) { if (err instanceof z.ZodError) { const messages = err.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', '); return error(`Validation failed: ${messages}`); } const message = err instanceof Error ? err.message : String(err); return error(message); } },