using System.Collections.Generic;
using UnityEngine;
namespace Heroes.Battle
{
///
/// Pure-C# occupancy and pathfinding model for a battle. Knows which hexes
/// are blocked by obstacles and which are occupied by which stack, and can
/// compute the reachable hexes of a stack (BFS limited by its speed) and the
/// enemy stacks it can attack (adjacent to any hex it can reach).
///
/// Uses "odd-r" offset hex coordinates: odd rows are shifted right, matching
/// .
///
public sealed class BattleGrid
{
// Neighbour offsets (dCol, dRow) for odd-r offset layout, indexed by row parity.
private static readonly Vector2Int[][] Directions =
{
// even rows (row & 1 == 0)
new[]
{
new Vector2Int(1, 0), new Vector2Int(0, -1), new Vector2Int(-1, -1),
new Vector2Int(-1, 0), new Vector2Int(-1, 1), new Vector2Int(0, 1),
},
// odd rows (row & 1 == 1)
new[]
{
new Vector2Int(1, 0), new Vector2Int(1, -1), new Vector2Int(0, -1),
new Vector2Int(-1, 0), new Vector2Int(0, 1), new Vector2Int(1, 1),
},
};
private readonly bool[,] _obstacle;
private readonly UnitStack[,] _stackAt;
private readonly List _stacks;
public int Width => BattlefieldConfig.Width;
public int Height => BattlefieldConfig.Height;
public BattleGrid(bool[,] obstacle, IEnumerable stacks)
{
_obstacle = obstacle;
_stacks = new List();
_stackAt = new UnitStack[Width, Height];
foreach (var stack in stacks)
{
_stacks.Add(stack);
if (!stack.IsAlive) continue;
foreach (int col in stack.OccupiedColumns())
{
if (InBounds(col, stack.Y)) _stackAt[col, stack.Y] = stack;
}
}
}
public bool InBounds(int x, int y) => x >= 0 && x < Width && y >= 0 && y < Height;
public bool IsObstacle(int x, int y)
{
if (!InBounds(x, y)) return true;
return _obstacle != null && _obstacle[x, y];
}
public UnitStack StackAt(int x, int y) => InBounds(x, y) ? _stackAt[x, y] : null;
public static IEnumerable Neighbors(int x, int y)
{
var dirs = Directions[y & 1];
for (int i = 0; i < dirs.Length; i++)
{
yield return new Vector2Int(x + dirs[i].x, y + dirs[i].y);
}
}
/// Can the given stack's whole footprint stand with its anchor at (anchorX, y)?
public bool CanStand(UnitStack stack, int anchorX, int y)
{
foreach (int col in UnitStack.ColumnsFor(stack.Side, stack.HexWidth, anchorX))
{
if (!InBounds(col, y)) return false;
if (IsObstacle(col, y)) return false;
var occupant = _stackAt[col, y];
if (occupant != null && occupant != stack) return false;
}
return true;
}
///
/// Hexes the stack can move its anchor to, mapped to the step-distance to
/// reach them (0 = current hex). Bounded by the stack's speed.
///
public Dictionary ComputeReachable(UnitStack stack, out Dictionary cameFrom)
{
var distance = new Dictionary();
cameFrom = new Dictionary();
var start = new Vector2Int(stack.X, stack.Y);
distance[start] = 0;
var queue = new Queue();
queue.Enqueue(start);
while (queue.Count > 0)
{
var current = queue.Dequeue();
int d = distance[current];
if (d >= stack.Speed) continue;
foreach (var next in Neighbors(current.x, current.y))
{
if (distance.ContainsKey(next)) continue;
if (!CanStand(stack, next.x, next.y)) continue;
distance[next] = d + 1;
cameFrom[next] = current;
queue.Enqueue(next);
}
}
return distance;
}
///
/// Reconstructs the path of anchor hexes from the start to ,
/// excluding the start hex. Empty if the target is the start or unreachable.
///
public static List BuildPath(
Dictionary cameFrom,
Vector2Int start,
Vector2Int target)
{
var path = new List();
if (target == start) return path;
if (!cameFrom.ContainsKey(target)) return path;
var node = target;
while (node != start)
{
path.Add(node);
node = cameFrom[node];
}
path.Reverse();
return path;
}
///
/// For each enemy stack the given stack can reach into melee, the anchor
/// hex from which to strike (chosen as the closest reachable adjacent hex,
/// preferring the current position). Melee reach = the enemy occupies a hex
/// adjacent to a hex the stack can stand on within its movement range.
///
public Dictionary ComputeAttackable(
UnitStack stack,
Dictionary reachable)
{
var result = new Dictionary();
foreach (var enemy in _stacks)
{
if (enemy == stack || enemy.Side == stack.Side || !enemy.IsAlive) continue;
var enemyCells = new HashSet();
foreach (int col in enemy.OccupiedColumns())
{
enemyCells.Add(new Vector2Int(col, enemy.Y));
}
Vector2Int bestStand = default;
int bestDist = int.MaxValue;
foreach (var kvp in reachable)
{
Vector2Int stand = kvp.Key;
int dist = kvp.Value;
if (dist >= bestDist) continue;
if (!IsAdjacentToAny(stack, stand, enemyCells)) continue;
bestStand = stand;
bestDist = dist;
}
if (bestDist != int.MaxValue) result[enemy] = bestStand;
}
return result;
}
private static bool IsAdjacentToAny(UnitStack stack, Vector2Int stand, HashSet enemyCells)
{
foreach (int col in UnitStack.ColumnsFor(stack.Side, stack.HexWidth, stand.x))
{
foreach (var neighbor in Neighbors(col, stand.y))
{
if (enemyCells.Contains(neighbor)) return true;
}
}
return false;
}
}
}