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