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