Skip to main content
Glama

measure_distance

Calculate distance between two positions using D&D 5e grid mechanics for RPG encounters.

Instructions

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

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
encounterIdNo
fromYes
toYes
modeNoeuclidean
includeElevationNo

Implementation Reference

  • Registers the 'measure_distance' tool in the central registry, using imported measureDistance handler and measureDistanceSchema for validation.
    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);
        }
      },
  • Core handler function that calculates distance between positions using various D&D 5e grid modes (euclidean, grid_5e, grid_alt), handles participant lookups, elevation, and formats ASCII output.
    export function measureDistance(input: MeasureDistanceInput): MeasureDistanceResult {
      let fromPos: Position = input.from as Position;
      let toPos: Position = input.to as Position;
      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 ?? 0) - (fromPos.z ?? 0)) : 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 for input validation of measure_distance tool, supporting position coordinates or participant IDs.
    export const measureDistanceSchema = z.object({
      encounterId: z.string().optional(),
      from: z.union([z.string(), PositionSchema]),
      to: z.union([z.string(), PositionSchema]),
      mode: DistanceModeSchema.default('euclidean'),
      includeElevation: z.boolean().default(true),
    });
  • Helper function that formats the distance calculation result into a detailed ASCII art box with positions, path visualization, breakdown, and results.
    function formatDistanceResult(params: {
      from: Position;
      to: Position;
      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 ?? 0}) → (${params.to.x}, ${params.to.y}, ${params.to.z ?? 0})`;
      if (params.fromName || params.toName) {
        const fromLabel = params.fromName || `(${params.from.x}, ${params.from.y}, ${params.from.z ?? 0})`;
        const toLabel = params.toName || `(${params.to.x}, ${params.to.y}, ${params.to.z ?? 0})`;
        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 ?? 0) !== (params.from.z ?? 0)) {
        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