Skip to main content
Glama

measure_distance

Calculate distance between two points using D&D 5e grid mechanics for tactical positioning in role-playing games.

Instructions

Measure distance between two positions using D&D 5e grid mechanics

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
encounterIdNo
fromYes
toYes
modeNoeuclidean
includeElevationNo

Implementation Reference

  • Core handler function implementing the distance measurement logic for D&D 5e grid mechanics, supporting multiple modes, entity lookups, elevation, and ASCII visualization.
    export function measureDistance(input: MeasureDistanceInput): MeasureDistanceResult { let fromPos: any = input.from; let toPos: any = input.to; let fromName: string | undefined; let toName: string | undefined; // Handle character/creature ID lookups if (typeof input.from === 'string') { if (!input.encounterId) { throw new Error('encounterId is required for character position lookup'); } const participant = getEncounterParticipant(input.encounterId, input.from); if (!participant) { throw new Error(`Character/creature not found in encounter: ${input.from}`); } fromPos = participant.position; fromName = participant.name; } if (typeof input.to === 'string') { if (!input.encounterId) { throw new Error('encounterId is required for character position lookup'); } const participant = getEncounterParticipant(input.encounterId, input.to); if (!participant) { throw new Error(`Character/creature not found in encounter: ${input.to}`); } toPos = participant.position; toName = participant.name; } // Calculate deltas const dx = Math.abs(toPos.x - fromPos.x); const dy = Math.abs(toPos.y - fromPos.y); const dz = input.includeElevation ? Math.abs(toPos.z - fromPos.z) : 0; let distanceSquares: number; let distanceFeet: number; switch (input.mode) { case 'euclidean': // True geometric distance: √(dx² + dy² + dz²) × 5 distanceSquares = Math.sqrt(dx * dx + dy * dy + dz * dz); distanceFeet = Math.round(distanceSquares * 5); distanceSquares = Math.round(distanceSquares); break; case 'grid_5e': // Simplified D&D: diagonals count as 5ft // Distance = max(dx, dy, dz) × 5 distanceSquares = Math.max(dx, dy, dz); distanceFeet = distanceSquares * 5; break; case 'grid_alt': // Variant rule: diagonals alternate 5ft/10ft // Calculate diagonal and straight movement const minDim = Math.min(dx, dy); const maxDim = Math.max(dx, dy); const straight = maxDim - minDim; // Diagonal movement: alternate 5/10/5/10 let diagonalCost = 0; for (let i = 0; i < minDim; i++) { diagonalCost += (i % 2 === 0) ? 5 : 10; } // Add straight movement and elevation distanceFeet = diagonalCost + (straight * 5) + (dz * 5); distanceSquares = Math.max(dx, dy, dz); break; default: throw new Error(`Unknown distance mode: ${input.mode}`); } // Format markdown output const markdown = formatDistanceResult({ from: fromPos, to: toPos, fromName, toName, mode: input.mode, distanceFeet, distanceSquares, includeElevation: input.includeElevation, dx, dy, dz, }); return { distanceFeet, distanceSquares, markdown, fromName, toName, }; }
  • Zod schema defining input validation for the measure_distance tool, including positions or entity IDs, distance mode, and elevation option.
    export const measureDistanceSchema = z.object({ encounterId: z.string().optional(), from: z.union([z.string(), PositionSchema]), to: z.union([z.string(), PositionSchema]), mode: z.enum(['euclidean', 'grid_5e', 'grid_alt']).default('euclidean'), includeElevation: z.boolean().default(true), });
  • Tool registration entry in the central registry, including description, schema conversion, and wrapper handler for input validation and error handling.
    measure_distance: { name: 'measure_distance', description: 'Measure distance between two positions using D&D 5e grid mechanics', inputSchema: toJsonSchema(measureDistanceSchema), handler: async (args) => { try { const validated = measureDistanceSchema.parse(args); const result = measureDistance(validated); return success(result.markdown); } 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); } }, },
  • Type definitions inferred from the schema and result interface for the measure_distance tool.
    export type MeasureDistanceInput = z.infer<typeof measureDistanceSchema>; export interface MeasureDistanceResult { distanceFeet: number; distanceSquares: number; markdown: string; fromName?: string; toName?: string; } /** * Calculate distance between two positions * Supports multiple distance calculation modes * When using character/creature IDs (strings), encounterId must be provided */ export function measureDistance(input: MeasureDistanceInput): MeasureDistanceResult { let fromPos: any = input.from; let toPos: any = input.to; let fromName: string | undefined; let toName: string | undefined; // Handle character/creature ID lookups if (typeof input.from === 'string') { if (!input.encounterId) { throw new Error('encounterId is required for character position lookup'); } const participant = getEncounterParticipant(input.encounterId, input.from); if (!participant) { throw new Error(`Character/creature not found in encounter: ${input.from}`); } fromPos = participant.position; fromName = participant.name; } if (typeof input.to === 'string') { if (!input.encounterId) { throw new Error('encounterId is required for character position lookup'); } const participant = getEncounterParticipant(input.encounterId, input.to); if (!participant) { throw new Error(`Character/creature not found in encounter: ${input.to}`); } toPos = participant.position; toName = participant.name; } // Calculate deltas const dx = Math.abs(toPos.x - fromPos.x); const dy = Math.abs(toPos.y - fromPos.y); const dz = input.includeElevation ? Math.abs(toPos.z - fromPos.z) : 0; let distanceSquares: number; let distanceFeet: number; switch (input.mode) { case 'euclidean': // True geometric distance: √(dx² + dy² + dz²) × 5 distanceSquares = Math.sqrt(dx * dx + dy * dy + dz * dz); distanceFeet = Math.round(distanceSquares * 5); distanceSquares = Math.round(distanceSquares); break; case 'grid_5e': // Simplified D&D: diagonals count as 5ft // Distance = max(dx, dy, dz) × 5 distanceSquares = Math.max(dx, dy, dz); distanceFeet = distanceSquares * 5; break; case 'grid_alt': // Variant rule: diagonals alternate 5ft/10ft // Calculate diagonal and straight movement const minDim = Math.min(dx, dy); const maxDim = Math.max(dx, dy); const straight = maxDim - minDim; // Diagonal movement: alternate 5/10/5/10 let diagonalCost = 0; for (let i = 0; i < minDim; i++) { diagonalCost += (i % 2 === 0) ? 5 : 10; } // Add straight movement and elevation distanceFeet = diagonalCost + (straight * 5) + (dz * 5); distanceSquares = Math.max(dx, dy, dz); break; default: throw new Error(`Unknown distance mode: ${input.mode}`); } // Format markdown output const markdown = formatDistanceResult({ from: fromPos, to: toPos, fromName, toName, mode: input.mode, distanceFeet, distanceSquares, includeElevation: input.includeElevation, dx, dy, dz, }); return { distanceFeet, distanceSquares, markdown, fromName, toName, }; }
  • Helper function to format the distance calculation result into a boxed markdown visualization with ASCII path diagram.
    function formatDistanceResult(params: { from: { x: number; y: number; z: number }; to: { x: number; y: number; z: number }; fromName?: string; toName?: string; mode: string; distanceFeet: number; distanceSquares: number; includeElevation: boolean; dx: number; dy: number; dz: number; }): string { const content: string[] = []; const box = BOX.LIGHT; // Title const modeNames = { 'euclidean': 'Euclidean (True Distance)', 'grid_5e': 'Grid 5e (Simplified)', 'grid_alt': 'Grid Alternate (5/10 Diagonal)', }; content.push('DISTANCE MEASUREMENT'); content.push(modeNames[params.mode as keyof typeof modeNames]); content.push(''); content.push('─'.repeat(40)); content.push(''); // Positions content.push('FROM → TO'); let positionDisplay = `(${params.from.x}, ${params.from.y}, ${params.from.z}) → (${params.to.x}, ${params.to.y}, ${params.to.z})`; if (params.fromName || params.toName) { const fromLabel = params.fromName || `(${params.from.x}, ${params.from.y}, ${params.from.z})`; const toLabel = params.toName || `(${params.to.x}, ${params.to.y}, ${params.to.z})`; positionDisplay = `${fromLabel} → ${toLabel}`; } content.push(positionDisplay); content.push(''); // Visual path representation const arrow = drawPath(params.from, params.to); content.push(`Path: ${arrow}`); content.push(''); // Movement breakdown if (params.dx > 0 || params.dy > 0 || params.dz > 0) { content.push(`Movement: ${params.dx}x, ${params.dy}y, ${params.dz}z`); content.push(''); } content.push('─'.repeat(40)); content.push(''); // Result content.push(`DISTANCE: ${params.distanceFeet} feet`); content.push(`(${params.distanceSquares} squares @ 5ft each)`); // Notes if (!params.includeElevation && params.to.z !== params.from.z) { content.push(''); content.push('* Elevation difference ignored *'); } return createBox('DISTANCE', content, undefined, 'HEAVY'); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Mnehmos/ChatRPG'

If you have feedback or need assistance with the MCP directory API, please join our Discord server