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)
);
}
}
}