Files
Christopher Wiebe b451f83174 Initial commit: Theriapolis baseline at port/godot branch point
Captures the pre-Godot-port state of the codebase. This is the rollback
anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md).
All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:40:51 -07:00

107 lines
4.4 KiB
C#

using Theriapolis.Core.Rules.Combat;
namespace Theriapolis.Core.Entities.Ai;
/// <summary>
/// Read-only view of the world the AI behavior consults during a turn.
/// Wraps the live <see cref="Encounter"/> + 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.
/// </summary>
public sealed class AiContext
{
public Encounter Encounter { get; }
/// <summary>Returns true if the tile at (tx, ty) blocks line-of-sight.</summary>
public System.Func<int, int, bool> IsLosBlocked { get; }
public AiContext(Encounter encounter, System.Func<int, int, bool>? isLosBlocked = null)
{
Encounter = encounter;
IsLosBlocked = isLosBlocked ?? LineOfSight.AlwaysClear;
}
/// <summary>Find the closest hostile combatant to the given actor, or null if none.</summary>
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;
}
/// <summary>
/// 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).
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>True if the two allegiances should target each other in combat.</summary>
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;
}
}