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;
    }
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden of behavioral disclosure. While it mentions 'obstacle/cover detection,' it doesn't explain what the tool returns (e.g., a boolean result, detailed blockage info, or error conditions), how it handles edge cases (e.g., invalid positions), or any performance implications. For a complex tool with 13 parameters, this leaves significant gaps in understanding its behavior.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence: 'Check line of sight between positions with obstacle/cover detection.' It's front-loaded with the core purpose and wastes no words. Every part of the sentence adds value, making it highly concise and well-structured.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the complexity (13 parameters, nested objects, no output schema, and no annotations), the description is inadequate. It doesn't explain the return value, error handling, or how parameters interact (e.g., whether 'fromId' overrides 'from'). For a tool that likely returns critical game-state information, more context is needed to use it effectively.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, meaning none of the 13 parameters are documented in the schema. The description only vaguely references 'positions' and 'obstacle/cover detection,' which doesn't compensate for the lack of parameter documentation. Key parameters like 'encounterId', 'fromId', 'toId', 'lighting', and various sensory ranges are left unexplained, making it difficult for an agent to use this tool correctly.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: 'Check line of sight between positions with obstacle/cover detection.' It specifies the verb ('check') and resource ('line of sight'), and the mention of 'obstacle/cover detection' adds useful context. However, it doesn't explicitly differentiate from sibling tools like 'check_cover' or 'measure_distance', which could have overlapping functionality in a gaming context.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives. With sibling tools like 'check_cover' and 'measure_distance' present, the agent has no indication whether this tool is for combat scenarios, map exploration, or specific game mechanics. There's no mention of prerequisites, context (e.g., during an encounter), or exclusions.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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/mnehmos.chatrpg.game'

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