Skip to main content
Glama

check_line_of_sight

Determine if positions have clear visibility by detecting obstacles and cover in ChatRPG's role-playing environment.

Instructions

Check line of sight between positions with obstacle/cover detection

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
encounterIdNo
fromNo
toNo
fromIdNo
toIdNo
obstaclesNo
creaturesNo
creaturesBlockNo
lightingNo
darkvisionNo
sensesNo
blindsightRangeNo
tremorsenseRangeNo

Implementation Reference

  • Registration of the 'check_line_of_sight' tool, including name, description, schema conversion, and wrapper handler that validates input and calls the core checkLineOfSight function.
    check_line_of_sight: {
      name: 'check_line_of_sight',
      description: 'Check line of sight between positions with obstacle/cover detection',
      inputSchema: toJsonSchema(checkLineOfSightSchema),
      handler: async (args) => {
        try {
          const validated = checkLineOfSightSchema.parse(args);
          const result = checkLineOfSight(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);
        }
      },
    },
  • Zod schema for validating inputs to the check_line_of_sight tool, including positions (coords or IDs), obstacles, creatures, lighting, and special senses.
    export const checkLineOfSightSchema = z.object({
      encounterId: z.string().optional(),
    
      // Position can be coordinates or character ID
      from: PositionSchema.optional(),
      to: PositionSchema.optional(),
      fromId: z.string().optional(),
      toId: z.string().optional(),
    
      // Obstacles (manual or from encounter)
      obstacles: z.array(ObstacleSchema).optional(),
    
      // Creature blocking
      creatures: z.array(CreatureBlockSchema).optional(),
      creaturesBlock: z.boolean().default(false),
    
      // Lighting and senses
      lighting: LightSchema.optional(),
      darkvision: z.number().optional(),
      senses: z.array(SenseSchema).optional(),
      blindsightRange: z.number().optional(),
      tremorsenseRange: z.number().optional(),
    }).refine((data) => {
      // Must have from position or fromId
      const hasFrom = data.from !== undefined || data.fromId !== undefined;
      // Must have to position or toId
      const hasTo = data.to !== undefined || data.toId !== undefined;
      return hasFrom && hasTo;
    }, {
      message: 'Must provide from/to positions or fromId/toId',
    }).refine((data) => {
      // If using IDs, must have encounterId
      if ((data.fromId || data.toId) && !data.encounterId) {
        return false;
      }
      return true;
    }, {
      message: 'encounterId required when using fromId/toId',
    });
  • Core implementation of check_line_of_sight: resolves observer/target positions, detects blocking/cover from obstacles/creatures, accounts for senses/lighting, computes cover bonuses, generates detailed ASCII visualization.
    export function checkLineOfSight(input: CheckLineOfSightInput): string {
      const content: string[] = [];
    
      // Resolve positions
      let fromPos: Position;
      let toPos: Position;
      let fromName: string | undefined;
      let toName: string | undefined;
    
      if (input.fromId && input.encounterId) {
        const participant = getEncounterParticipant(input.encounterId, input.fromId);
        if (!participant) {
          throw new Error(`Participant not found: ${input.fromId}`);
        }
        fromPos = participant.position || { x: 0, y: 0, z: 0 };
        fromName = participant.name;
      } else if (input.from) {
        fromPos = { x: input.from.x, y: input.from.y, z: input.from.z ?? 0 };
      } else {
        throw new Error('From position required');
      }
    
      if (input.toId && input.encounterId) {
        const participant = getEncounterParticipant(input.encounterId, input.toId);
        if (!participant) {
          throw new Error(`Participant not found: ${input.toId}`);
        }
        toPos = participant.position || { x: 0, y: 0, z: 0 };
        toName = participant.name;
      } else if (input.to) {
        toPos = { x: input.to.x, y: input.to.y, z: input.to.z ?? 0 };
      } else {
        throw new Error('To position required');
      }
    
      // Calculate distance
      const dx = toPos.x - fromPos.x;
      const dy = toPos.y - fromPos.y;
      const dz = (toPos.z ?? 0) - (fromPos.z ?? 0);
      const distance = Math.sqrt(dx * dx + dy * dy + dz * dz) * 5;
    
      // Build header
      content.push(centerText('LINE OF SIGHT', LOS_DISPLAY_WIDTH));
      content.push('');
    
      // Show positions
      if (fromName) {
        content.push(`From: ${fromName} (${fromPos.x}, ${fromPos.y}, ${fromPos.z ?? 0})`);
      } else {
        content.push(`From: (${fromPos.x}, ${fromPos.y}, ${fromPos.z ?? 0})`);
      }
    
      if (toName) {
        content.push(`To: ${toName} (${toPos.x}, ${toPos.y}, ${toPos.z ?? 0})`);
      } else {
        content.push(`To: (${toPos.x}, ${toPos.y}, ${toPos.z ?? 0})`);
      }
    
      content.push(`Distance: ${Math.round(distance)} feet`);
      content.push('');
      content.push(BOX.LIGHT.H.repeat(LOS_DISPLAY_WIDTH));
      content.push('');
    
      // Handle same position
      if (distance === 0) {
        content.push(centerText('CLEAR - SAME POSITION', LOS_DISPLAY_WIDTH));
        content.push('');
        content.push('Observer and target at same position.');
        return createBox('LINE OF SIGHT', content);
      }
    
      // Collect all obstacles to check
      const allObstacles = [...(input.obstacles || [])];
    
      // Get terrain obstacles from encounter if available
      // (For now, just use provided obstacles - encounter terrain integration pending)
    
      // Check for blocking and cover
      let coverLevel: CoverLevel = 'none';
      let blockingObstacle: typeof allObstacles[0] | undefined;
      let blockingCreature: { size: string } | undefined;
    
      // Check static obstacles
      for (const obstacle of allObstacles) {
        const obstacleCover = calculateCoverFromObstacle(obstacle, fromPos, toPos);
        if (obstacleCover !== null) {
          coverLevel = determineCoverLevel(coverLevel, obstacleCover);
          if (obstacleCover !== 'none') {
            blockingObstacle = obstacle;
          }
        }
      }
    
      // Check creature blocking
      if (input.creaturesBlock && input.creatures) {
        for (const creature of input.creatures) {
          if (isOnLine(fromPos, toPos, creature)) {
            const creatureCover = getCreatureCover(creature.size);
            coverLevel = determineCoverLevel(coverLevel, creatureCover);
            if (creatureCover !== 'none') {
              blockingCreature = creature;
            }
          }
        }
      }
    
      // Handle special senses
      if (input.senses?.includes('blindsight') && input.blindsightRange) {
        if (distance <= input.blindsightRange) {
          content.push('BLINDSIGHT Active');
          content.push('');
          content.push(`blindsight range: ${input.blindsightRange} ft (target within range)`);
          content.push('Can perceive through normal obstacles.');
          content.push('');
          content.push(BOX.LIGHT.H.repeat(LOS_DISPLAY_WIDTH));
          content.push('');
        }
      }
    
      if (input.senses?.includes('tremorsense') && input.tremorsenseRange) {
        content.push('TREMORSENSE NOTES');
        content.push('');
        if ((toPos.z ?? 0) > 0) {
          content.push(`Target is airborne (z=${toPos.z ?? 0}) - flying creatures`);
          content.push('cannot be detected by tremorsense.');
        } else if (distance <= input.tremorsenseRange) {
          content.push(`Range: ${input.tremorsenseRange} ft (target within range)`);
        }
        content.push('');
        content.push(BOX.LIGHT.H.repeat(LOS_DISPLAY_WIDTH));
        content.push('');
      }
    
      // Handle lighting
      if (input.lighting === 'darkness') {
        content.push('LIGHTING: DARKNESS');
        content.push('');
        if (input.darkvision) {
          if (distance <= input.darkvision) {
            content.push(`Darkvision range: ${input.darkvision} ft`);
            content.push('Target within darkvision range (dim light for you).');
          } else {
            content.push(`Darkvision range: ${input.darkvision} ft`);
            content.push(`Target at ${Math.round(distance)} ft - heavily obscured!`);
          }
        } else {
          content.push('No darkvision - target is heavily obscured.');
        }
        content.push('');
        content.push(BOX.LIGHT.H.repeat(LOS_DISPLAY_WIDTH));
        content.push('');
      }
    
      // Display result
      content.push(centerText('RESULT', LOS_DISPLAY_WIDTH));
      content.push('');
    
      if (coverLevel === 'total') {
        content.push('STATUS: BLOCKED');
        content.push('');
        content.push('Total cover - no line of sight!');
        if (blockingObstacle) {
          content.push(`Blocked by: ${blockingObstacle.type} at (${blockingObstacle.x}, ${blockingObstacle.y})`);
        }
      } else if (coverLevel === 'three_quarters') {
        content.push('STATUS: THREE-QUARTERS COVER');
        content.push('');
        content.push('Target has three-quarters cover.');
        content.push(`AC Bonus: +${COVER_BONUSES.three_quarters}`);
        content.push(`Dex Save Bonus: +${COVER_BONUSES.three_quarters}`);
        if (blockingObstacle) {
          content.push(`Cover from: ${blockingObstacle.type}`);
        }
        if (blockingCreature) {
          content.push(`Cover from: ${blockingCreature.size} creature`);
        }
      } else if (coverLevel === 'half') {
        content.push('STATUS: HALF COVER');
        content.push('');
        content.push('Target has half cover.');
        content.push(`AC Bonus: +${COVER_BONUSES.half}`);
        content.push(`Dex Save Bonus: +${COVER_BONUSES.half}`);
        if (blockingObstacle) {
          content.push(`Cover from: ${blockingObstacle.type}`);
        }
        if (blockingCreature) {
          content.push(`Cover from: ${blockingCreature.size} creature`);
        }
      } else {
        content.push('STATUS: CLEAR');
        content.push('');
        content.push('Line of sight is unobstructed.');
      }
    
      const title = coverLevel === 'total' ? 'BLOCKED' : 'LINE OF SIGHT';
      return createBox(title, content);
    }
  • Utility function to get numerical AC/Dex save bonuses from cover levels (0, +2, +5, Infinity for total).
    export function getCoverBonus(cover: string): number {
      switch (cover) {
        case 'half':
          return COVER_BONUSES.half;
        case 'three_quarters':
          return COVER_BONUSES.three_quarters;
        case 'total':
        case 'full':
          return Infinity; // Target can't be directly targeted
        default:
          return 0;
      }
    }
  • Core geometry helper: determines if an obstacle/creature intersects the line of sight using line projection and grid tolerance.
    function isOnLine(
      from: Position,
      to: Position,
      point: Position
    ): boolean {
      // Calculate distance from point to line
      const dx = to.x - from.x;
      const dy = to.y - from.y;
      const dz = (to.z ?? 0) - (from.z ?? 0);
    
      const length = Math.sqrt(dx * dx + dy * dy + dz * dz);
      if (length === 0) return false;
    
      // Vector from 'from' to 'point'
      const px = point.x - from.x;
      const py = point.y - from.y;
      const pz = (point.z ?? 0) - (from.z ?? 0);
    
      // Project point onto line
      const t = (px * dx + py * dy + pz * dz) / (length * length);
    
      // Check if projection is between from and to
      if (t < 0 || t > 1) return false;
    
      // Calculate closest point on line
      const closestX = from.x + t * dx;
      const closestY = from.y + t * dy;
      const closestZ = (from.z ?? 0) + t * dz;
    
      // Calculate distance from point to closest point on line
      const distX = point.x - closestX;
      const distY = point.y - closestY;
      const distZ = (point.z ?? 0) - closestZ;
      const dist = Math.sqrt(distX * distX + distY * distY + distZ * distZ);
    
      // Within 1 grid square (5 feet / 5 = 1 unit) tolerance
      return dist <= 1;
    }

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