b451f83174
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>
107 lines
4.4 KiB
C#
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;
|
|
}
|
|
}
|