/** * Combat Grid System - Spatial Combat with Grid Positions * * This module implements the 5-phase spatial combat system: * - Phase 1: Position Persistence (handled by EncounterRepository) * - Phase 2: Boundary Validation (BUG-001 fix) * - Phase 3: Collision Enforcement * - Phase 4: Movement Economy (speed, terrain costs, dash) * - Phase 5: AoE Integration * * Design Principles: * - "LLM describes, engine validates" - Database is source of truth * - All positions validated against grid bounds * - Movement follows pathfinding strictly * - D&D 5e rules for movement (30ft = 6 squares, 1.5x diagonal cost) * * @module spatial/combat-grid */ import { SpatialEngine, Point, TerrainCostMap } from './engine.js'; import { Position, GridBounds, SizeCategory } from '../../schema/encounter.js'; import { CombatParticipant, CombatState } from '../combat/engine.js'; /** Feet per grid square (D&D 5e standard) */ export declare const FEET_PER_SQUARE = 5; /** Default movement speed in feet (D&D 5e standard for medium humanoids) */ export declare const DEFAULT_MOVEMENT_SPEED = 30; /** Squares of movement for default speed (30ft / 5ft = 6 squares) */ export declare const DEFAULT_MOVEMENT_SQUARES: number; /** Diagonal movement cost multiplier (D&D 5e strict 5-10-5 rule, avg 1.5) */ export declare const DIAGONAL_COST = 1.5; /** Difficult terrain cost multiplier */ export declare const DIFFICULT_TERRAIN_COST = 2; /** * Extended combat participant with spatial properties */ export interface SpatialParticipant extends CombatParticipant { position?: Position; movementSpeed: number; movementRemaining?: number; size: SizeCategory; hasDashed?: boolean; } /** * Extended combat state with spatial properties */ export interface SpatialCombatState extends CombatState { participants: SpatialParticipant[]; gridBounds: GridBounds; terrain?: { obstacles: string[]; difficultTerrain?: string[]; }; } /** * Result of a movement validation */ export interface MovementValidation { valid: boolean; error?: string; path?: Point[]; pathCost?: number; triggersOpportunityAttacks?: string[]; } /** * Result of an AoE calculation */ export interface AoEResult { affectedTiles: Point[]; affectedParticipants: SpatialParticipant[]; blockedByLOS?: Point[]; } /** * Validates that a position is within grid bounds. * * @param position The position to validate * @param bounds The grid bounds to check against * @returns true if position is within bounds * * @example * ```typescript * const bounds = { minX: 0, maxX: 100, minY: 0, maxY: 100 }; * isPositionInBounds({ x: 50, y: 50 }, bounds); // true * isPositionInBounds({ x: -1, y: 50 }, bounds); // false * ``` * * Complexity: O(1) */ export declare function isPositionInBounds(position: Position, bounds: GridBounds): boolean; /** * Validates a position and returns a detailed error message if invalid. * * @param position The position to validate * @param bounds The grid bounds * @param context Optional context for error message (e.g., "move destination") * @returns null if valid, error message string if invalid * * Complexity: O(1) */ export declare function validatePosition(position: Position, bounds: GridBounds, context?: string): string | null; /** * Get all tiles occupied by a creature based on its position and size. * * @param position Top-left corner of creature's space * @param size Creature's size category * @returns Array of all occupied tile keys ("x,y" format) * * @example * ```typescript * getOccupiedTiles({ x: 5, y: 5 }, 'medium'); // ['5,5'] * getOccupiedTiles({ x: 5, y: 5 }, 'large'); // ['5,5', '6,5', '5,6', '6,6'] * ``` * * Complexity: O(n²) where n is the footprint size (max 4 for gargantuan) */ export declare function getOccupiedTiles(position: Position, size: SizeCategory): string[]; /** * Build obstacle set from combat state (participants + terrain). * * @param state The combat state * @param excludeParticipantId Optional participant to exclude (for self-movement) * @returns Set of blocked tile keys ("x,y" format) * * Complexity: O(p * s² + t) where p=participants, s=max size footprint, t=terrain tiles */ export declare function buildObstacleSet(state: SpatialCombatState, excludeParticipantId?: string): Set; /** * Build difficult terrain set from combat state. * * @param state The combat state * @returns Set of difficult terrain tile keys ("x,y" format) * * Complexity: O(d) where d=difficult terrain tiles */ export declare function buildDifficultTerrainSet(state: SpatialCombatState): Set; /** * Check if a destination tile is blocked. * * @param destination Target position * @param size Creature's size * @param obstacles Set of blocked tiles * @returns true if destination is blocked * * Complexity: O(s²) where s=size footprint */ export declare function isDestinationBlocked(destination: Position, size: SizeCategory, obstacles: Set): boolean; /** * Create a terrain cost map for pathfinding. * * @param difficultTerrain Set of difficult terrain tiles * @returns TerrainCostMap for SpatialEngine * * Complexity: O(1) per tile lookup */ export declare function createTerrainCostMap(difficultTerrain: Set): TerrainCostMap; /** * Calculate movement cost along a path. * * @param path Array of points in the path * @param difficultTerrain Set of difficult terrain tiles * @returns Total movement cost in feet * * @example * ```typescript * // Straight line 3 squares = 15 feet * calculatePathCost([{x:0,y:0}, {x:1,y:0}, {x:2,y:0}, {x:3,y:0}], new Set()); * // Returns 15 (3 moves × 5 feet) * * // Diagonal movement = 1.5x cost * calculatePathCost([{x:0,y:0}, {x:1,y:1}], new Set()); * // Returns 7.5 (1 diagonal × 1.5 × 5 feet) * ``` * * Complexity: O(n) where n=path length */ export declare function calculatePathCost(path: Point[], difficultTerrain: Set): number; /** * Convert feet to squares (rounding down). * * @param feet Distance in feet * @returns Distance in grid squares */ export declare function feetToSquares(feet: number): number; /** * Convert squares to feet. * * @param squares Distance in grid squares * @returns Distance in feet */ export declare function squaresToFeet(squares: number): number; /** * Initialize movement for start of turn. * * @param participant The participant starting their turn * @returns Updated participant with reset movement */ export declare function initializeMovement(participant: SpatialParticipant): SpatialParticipant; /** * Apply dash action (doubles remaining movement). * * @param participant The participant dashing * @returns Updated participant with doubled movement */ export declare function applyDash(participant: SpatialParticipant): SpatialParticipant; /** * Validate a movement from current position to destination. * Enforces: * - Boundary validation (Phase 2) * - Collision detection (Phase 3) * - Movement economy (Phase 4) * * @param state The combat state * @param participantId ID of the moving participant * @param destination Target position * @param spatialEngine Optional SpatialEngine instance (creates one if not provided) * @returns MovementValidation result * * @example * ```typescript * const result = validateMovement(state, 'hero-1', { x: 5, y: 3 }); * if (result.valid) { * console.log(`Path found: ${result.path?.length} tiles, cost: ${result.pathCost}ft`); * } else { * console.log(`Movement blocked: ${result.error}`); * } * ``` * * Complexity: O(V log V + E) for A* pathfinding, where V=grid tiles, E=edges */ export declare function validateMovement(state: SpatialCombatState, participantId: string, destination: Position, spatialEngine?: SpatialEngine): MovementValidation; /** * Get all participants within a circular area. * * @param state The combat state * @param center Center point of the circle * @param radiusFeet Radius in feet * @param excludeIds Optional IDs to exclude (e.g., caster) * @returns AoEResult with affected tiles and participants * * @example * ```typescript * // 20ft radius Fireball centered at (10, 10) * const result = getParticipantsInCircle(state, { x: 10, y: 10 }, 20); * console.log(`Fireball hits ${result.affectedParticipants.length} creatures`); * ``` * * Complexity: O(r² + p) where r=radius in squares, p=participants */ export declare function getParticipantsInCircle(state: SpatialCombatState, center: Position, radiusFeet: number, excludeIds?: string[]): AoEResult; /** * Get all participants within a cone area. * * @param state The combat state * @param origin Origin point of the cone * @param direction Direction vector (e.g., {x: 1, y: 0} for East) * @param lengthFeet Length in feet * @param angleDegrees Cone angle in degrees * @param excludeIds Optional IDs to exclude * @returns AoEResult with affected tiles and participants * * @example * ```typescript * // 15ft cone of cold facing North * const result = getParticipantsInCone(state, { x: 5, y: 5 }, { x: 0, y: -1 }, 15, 90); * ``` * * Complexity: O(l² + p) where l=length in squares, p=participants */ export declare function getParticipantsInCone(state: SpatialCombatState, origin: Position, direction: Position, lengthFeet: number, angleDegrees: number, excludeIds?: string[]): AoEResult; /** * Get all participants along a line (e.g., Lightning Bolt). * * @param state The combat state * @param start Start point of the line * @param end End point of the line * @param excludeIds Optional IDs to exclude * @returns AoEResult with affected tiles and participants * * @example * ```typescript * // 100ft Lightning Bolt from (0,0) to (20,0) * const result = getParticipantsInLine(state, { x: 0, y: 0 }, { x: 20, y: 0 }); * ``` * * Complexity: O(d + p) where d=distance in squares, p=participants */ export declare function getParticipantsInLine(state: SpatialCombatState, start: Position, end: Position, excludeIds?: string[]): AoEResult; /** * Check line of sight from caster to target. * * @param state The combat state * @param from Origin point * @param to Target point * @returns true if clear line of sight exists * * Complexity: O(d + o) where d=distance, o=obstacles */ export declare function hasLineOfSight(state: SpatialCombatState, from: Position, to: Position): boolean; /** * CombatGridManager - High-level API for spatial combat operations. * * Provides a unified interface for all spatial combat operations, * integrating all 5 phases into a cohesive system. * * @example * ```typescript * const grid = new CombatGridManager(combatState); * * // Start of turn * grid.startTurn('hero-1'); * * // Validate and execute movement * const moveResult = grid.validateMove('hero-1', { x: 5, y: 3 }); * if (moveResult.valid) { * grid.executeMove('hero-1', { x: 5, y: 3 }); * } * * // Use dash action * grid.dash('hero-1'); * * // Get fireball targets * const targets = grid.getCircleTargets({ x: 10, y: 10 }, 20); * ``` */ export declare class CombatGridManager { private state; private spatialEngine; constructor(state: SpatialCombatState); /** * Get the current combat state. */ getState(): SpatialCombatState; /** * Get grid bounds. */ getBounds(): GridBounds; /** * Initialize movement for a participant's turn. * * @param participantId The participant starting their turn */ startTurn(participantId: string): void; /** * Validate a movement without executing it. * * @param participantId ID of the moving participant * @param destination Target position * @returns MovementValidation result */ validateMove(participantId: string, destination: Position): MovementValidation; /** * Execute a validated movement. * Call validateMove first to ensure movement is valid. * * @param participantId ID of the moving participant * @param destination Target position * @param pathCost Cost in feet (from validation result) * @returns true if movement was executed */ executeMove(participantId: string, destination: Position, pathCost: number): boolean; /** * Apply dash action to double movement. * * @param participantId ID of the participant * @returns true if dash was applied */ dash(participantId: string): boolean; /** * Set initial position for a participant. * Used when placing tokens at encounter start. * * @param participantId ID of the participant * @param position Initial position * @returns null if successful, error message if invalid */ setPosition(participantId: string, position: Position): string | null; /** * Get participants in a circular area (e.g., Fireball). */ getCircleTargets(center: Position, radiusFeet: number, excludeIds?: string[]): AoEResult; /** * Get participants in a cone area (e.g., Burning Hands). */ getConeTargets(origin: Position, direction: Position, lengthFeet: number, angleDegrees: number, excludeIds?: string[]): AoEResult; /** * Get participants along a line (e.g., Lightning Bolt). */ getLineTargets(start: Position, end: Position, excludeIds?: string[]): AoEResult; /** * Check line of sight between two points. */ hasLineOfSight(from: Position, to: Position): boolean; /** * Get remaining movement for a participant. */ getRemainingMovement(participantId: string): number; } //# sourceMappingURL=combat-grid.d.ts.map