using Theriapolis.Core.Rules.Combat; namespace Theriapolis.Core.Entities.Ai; /// /// Read-only view of the world the AI behavior consults during a turn. /// Wraps the live + a sight predicate so behaviors /// stay testable in isolation (M5 unit tests pass an always-clear predicate; /// the live game wires in the tactical-tile sampler). /// /// Behaviors call into the resolver to actually mutate the encounter; this /// context is purely for reading the situation. /// public sealed class AiContext { public Encounter Encounter { get; } /// Returns true if the tile at (tx, ty) blocks line-of-sight. public System.Func IsLosBlocked { get; } public AiContext(Encounter encounter, System.Func? isLosBlocked = null) { Encounter = encounter; IsLosBlocked = isLosBlocked ?? LineOfSight.AlwaysClear; } /// Find the closest hostile combatant to the given actor, or null if none. public Combatant? FindClosestHostile(Combatant self) { Combatant? best = null; int bestDist = int.MaxValue; foreach (var c in Encounter.Participants) { if (c.Id == self.Id) continue; if (c.IsDown) continue; if (!IsHostileTo(self.Allegiance, c.Allegiance)) continue; int d = ReachAndCover.EdgeToEdgeChebyshev(self, c); if (d < bestDist) { best = c; bestDist = d; } } return best; } /// /// Phase 6.5 M1 — find the closest *ally* to the given actor (excludes /// self). Allies share the player-side allegiance: Player or Allied. /// Returns null when none qualify (which is the typical M1 case — the /// player is alone — and disables ally-targeted features cleanly). /// public Combatant? FindClosestAlly(Combatant self) { Combatant? best = null; int bestDist = int.MaxValue; bool selfPlayerSide = self.Allegiance == Rules.Character.Allegiance.Player || self.Allegiance == Rules.Character.Allegiance.Allied; if (!selfPlayerSide) return null; foreach (var c in Encounter.Participants) { if (c.Id == self.Id) continue; if (c.IsDown) continue; bool cPlayerSide = c.Allegiance == Rules.Character.Allegiance.Player || c.Allegiance == Rules.Character.Allegiance.Allied; if (!cPlayerSide) continue; int d = ReachAndCover.EdgeToEdgeChebyshev(self, c); if (d < bestDist) { best = c; bestDist = d; } } return best; } /// /// Phase 6.5 M1 — find the lowest-HP friendly target (self or any /// ally), preferring the most damaged. Returns null only when every /// friendly is at full HP. Used as the auto-target for healing /// features when the player presses the heal hotkey without picking /// explicitly. /// public Combatant? FindMostDamagedFriendly(Combatant self) { Combatant? best = null; int bestDeficit = -1; bool selfPlayerSide = self.Allegiance == Rules.Character.Allegiance.Player || self.Allegiance == Rules.Character.Allegiance.Allied; if (!selfPlayerSide) return null; foreach (var c in Encounter.Participants) { if (c.IsDown) continue; bool cPlayerSide = c.Allegiance == Rules.Character.Allegiance.Player || c.Allegiance == Rules.Character.Allegiance.Allied; if (!cPlayerSide) continue; int deficit = c.MaxHp - c.CurrentHp; if (deficit > bestDeficit) { best = c; bestDeficit = deficit; } } return bestDeficit > 0 ? best : null; } /// True if the two allegiances should target each other in combat. public static bool IsHostileTo( Rules.Character.Allegiance a, Rules.Character.Allegiance b) { bool aIsPlayerSide = a == Rules.Character.Allegiance.Player || a == Rules.Character.Allegiance.Allied; bool bIsPlayerSide = b == Rules.Character.Allegiance.Player || b == Rules.Character.Allegiance.Allied; if (aIsPlayerSide && b == Rules.Character.Allegiance.Hostile) return true; if (bIsPlayerSide && a == Rules.Character.Allegiance.Hostile) return true; return false; } }