using System.Collections.Generic; using UnityEngine; namespace MazeGen { /// /// Manages the combat grid where combatants are positioned during a fight. /// Handles placement, collision detection, occupancy tracking, and pathfinding. /// Grid cells are based on maze room spacing for consistency. /// public class CombatGrid { /// Grid dimensions in squares. private Vector2Int _gridSize; /// Grid cell size in world units (matches maze room spacing). private float _cellSize; /// Tracks which combatants occupy each grid position. private Dictionary> _occupancy; /// All combatants on this grid. private List _allCombatants; /// /// Initialize the grid with specified dimensions. /// Default size is 11x7 squares (standard arena size). /// public CombatGrid(Vector2Int gridSize, float cellSize = 1f) { _gridSize = gridSize; _cellSize = cellSize; _occupancy = new Dictionary>(); _allCombatants = new List(); // Pre-allocate all cells for (int x = 0; x < _gridSize.x; x++) { for (int y = 0; y < _gridSize.y; y++) { Vector2Int pos = new Vector2Int(x, y); _occupancy[pos] = new List(); } } } public Vector2Int GridSize => _gridSize; public float CellSize => _cellSize; /// /// Place all combatants on the grid. Agents on left side, monsters on right side. /// public void PlaceAllCombatants(IReadOnlyList combatants) { _allCombatants.Clear(); _occupancy.Clear(); // Re-initialize occupancy for (int x = 0; x < _gridSize.x; x++) { for (int y = 0; y < _gridSize.y; y++) { Vector2Int pos = new Vector2Int(x, y); _occupancy[pos] = new List(); } } // Separate agents and monsters var agents = new List(); var monsters = new List(); foreach (var c in combatants) { if (c.IsAgent) agents.Add(c); else monsters.Add(c); } // Place agents on the left side PlaceTeam(agents, isAgents: true); // Place monsters on the right side PlaceTeam(monsters, isAgents: false); } /// /// Place a team (agents or monsters) on their respective side of the grid. /// private void PlaceTeam(List team, bool isAgents) { if (team.Count == 0) return; // Calculate starting column and direction int startColumn = isAgents ? 1 : _gridSize.x - 2; int columnDirection = isAgents ? 1 : -1; int combatantsPlaced = 0; int currentColumn = startColumn; while (combatantsPlaced < team.Count && currentColumn >= 0 && currentColumn < _gridSize.x) { // Try to fill this column from top to bottom for (int row = 0; row < _gridSize.y && combatantsPlaced < team.Count; row++) { Vector2Int position = new Vector2Int(currentColumn, row); if (IsPositionValid(position, team[combatantsPlaced])) { PlaceCombatant(team[combatantsPlaced], position); combatantsPlaced++; } } currentColumn += columnDirection; } // Fallback: if not all placed, fill remaining positions if (combatantsPlaced < team.Count) { for (int x = 0; x < _gridSize.x && combatantsPlaced < team.Count; x++) { for (int y = 0; y < _gridSize.y && combatantsPlaced < team.Count; y++) { Vector2Int position = new Vector2Int(x, y); if (_occupancy[position].Count == 0 && IsPositionValid(position, team[combatantsPlaced])) { PlaceCombatant(team[combatantsPlaced], position); combatantsPlaced++; } } } } } /// /// Place a single combatant at the specified grid position. /// Only call after validating with IsPositionValid(). /// public void PlaceCombatant(Combatant combatant, Vector2Int position) { if (combatant == null) return; // Remove from old position if already placed if (_allCombatants.Contains(combatant)) { foreach (var cellCombatants in _occupancy.Values) { cellCombatants.Remove(combatant); } } // Add to new position combatant.GridPosition = position; if (!_allCombatants.Contains(combatant)) _allCombatants.Add(combatant); if (_occupancy.ContainsKey(position)) { if (!_occupancy[position].Contains(combatant)) _occupancy[position].Add(combatant); } } /// /// Check if a position is valid for a combatant to occupy. /// Considers grid boundaries, size, and occupancy rules. /// public bool IsPositionValid(Vector2Int position, Combatant combatant) { if (combatant == null) return false; // Check grid bounds if (position.x < 0 || position.x >= _gridSize.x || position.y < 0 || position.y >= _gridSize.y) return false; // Get the footprint this combatant would occupy Vector2Int footprint = combatant.GetFootprintSize(); // Check if entire footprint fits in grid if (position.x + footprint.x > _gridSize.x || position.y + footprint.y > _gridSize.y) return false; // Check occupancy based on size if (combatant.Size == MonsterSize.Large) { // Large creatures can't share a grid with anything for (int x = position.x; x < position.x + footprint.x; x++) { for (int y = position.y; y < position.y + footprint.y; y++) { if (x < 0 || x >= _gridSize.x || y < 0 || y >= _gridSize.y) return false; if (_occupancy.ContainsKey(new Vector2Int(x, y)) && _occupancy[new Vector2Int(x, y)].Count > 0) return false; } } } else if (combatant.Size == MonsterSize.Medium) { // Medium creatures need empty space if (_occupancy.ContainsKey(position) && _occupancy[position].Count > 0) return false; } else if (combatant.Size == MonsterSize.Small) { // Small creatures can stack (up to 4 per square) if (_occupancy.ContainsKey(position)) { int creaturesInSquare = 0; foreach (var c in _occupancy[position]) { creaturesInSquare += c.Size == MonsterSize.Small ? 1 : 4; } if (creaturesInSquare >= 4) return false; } } return true; } /// /// Try to move a combatant to a new position atomically. /// Returns true if successful, false if invalid move. /// public bool TryMoveCombatant(Combatant combatant, Vector2Int newPosition) { if (!IsPositionValid(newPosition, combatant)) return false; PlaceCombatant(combatant, newPosition); return true; } /// /// Remove a combatant from the grid (e.g., when they die). /// public void RemoveCombatant(Combatant combatant) { if (combatant == null) return; _allCombatants.Remove(combatant); foreach (var cellCombatants in _occupancy.Values) { cellCombatants.Remove(combatant); } } /// /// Get all combatants currently on the grid. /// public IReadOnlyList GetAllCombatants() => _allCombatants.AsReadOnly(); /// /// Get combatants at a specific grid position. /// public IReadOnlyList GetCombatantsAt(Vector2Int position) { if (_occupancy.ContainsKey(position)) return _occupancy[position].AsReadOnly(); return new List().AsReadOnly(); } /// /// Get all reachable positions from a starting position within movement range. /// Uses BFS (Breadth-First Search) for pathfinding. /// public HashSet GetReachablePositions(Combatant actor, int maxMovement) { var reachable = new HashSet(); var queue = new Queue<(Vector2Int pos, int distance)>(); queue.Enqueue((actor.GridPosition, 0)); reachable.Add(actor.GridPosition); while (queue.Count > 0) { var (currentPos, currentDistance) = queue.Dequeue(); if (currentDistance >= maxMovement) continue; // Check all 4 cardinal directions Vector2Int[] directions = new Vector2Int[] { new Vector2Int(1, 0), // Right new Vector2Int(-1, 0), // Left new Vector2Int(0, 1), // Up new Vector2Int(0, -1) // Down }; foreach (var direction in directions) { Vector2Int nextPos = currentPos + direction; if (!reachable.Contains(nextPos) && IsPositionValid(nextPos, actor)) { reachable.Add(nextPos); queue.Enqueue((nextPos, currentDistance + 1)); } } } return reachable; } /// /// Convert a grid position to world coordinates (for visualization). /// public Vector2 GridToWorldPosition(Vector2Int gridPos) { return new Vector2(gridPos.x * _cellSize, gridPos.y * _cellSize); } /// /// Convert world coordinates to grid position. /// public Vector2Int WorldToGridPosition(Vector2 worldPos) { return new Vector2Int( Mathf.RoundToInt(worldPos.x / _cellSize), Mathf.RoundToInt(worldPos.y / _cellSize) ); } } }