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

83 lines
3.1 KiB
C#

using Theriapolis.Core.Entities;
using Theriapolis.Core.Util;
namespace Theriapolis.Core.Rules.Combat;
/// <summary>
/// Per-tick check used by <see cref="Game.Screens.PlayScreen"/>:
/// "is there a hostile NPC within encounter trigger range that has line of
/// sight?" Returns the closest qualifying actor (or null) so the caller can
/// kick off an encounter.
///
/// Friendly / Neutral proximity is the same shape but uses a tighter radius
/// — see <see cref="FindInteractCandidate"/>.
/// </summary>
public static class EncounterTrigger
{
/// <summary>
/// Find the closest live <em>hostile</em> NPC within
/// <see cref="C.ENCOUNTER_TRIGGER_TILES"/> of the player that the
/// <paramref name="losBlocked"/> predicate can see (no blocking tile
/// between). Returns null if none found.
/// </summary>
public static NpcActor? FindHostileTrigger(
ActorManager actors,
System.Func<int, int, bool>? losBlocked = null)
{
var player = actors.Player;
if (player is null) return null;
var blocked = losBlocked ?? LineOfSight.AlwaysClear;
NpcActor? best = null;
int bestDistSq = C.ENCOUNTER_TRIGGER_TILES * C.ENCOUNTER_TRIGGER_TILES;
foreach (var npc in actors.Npcs)
{
if (!npc.IsAlive) continue;
if (npc.Allegiance != Rules.Character.Allegiance.Hostile) continue;
int distSq = ChebyshevDistSq(player.Position, npc.Position);
if (distSq > bestDistSq) continue;
if (!LineOfSight.HasLine(player.Position, npc.Position, blocked)) continue;
if (best is null || distSq < bestDistSq) { best = npc; bestDistSq = distSq; }
}
return best;
}
/// <summary>
/// Friendly / Neutral NPCs within
/// <see cref="C.INTERACT_PROMPT_TILES"/> of the player. The HUD shows
/// "[F] Talk to ..." for the closest match.
/// </summary>
public static NpcActor? FindInteractCandidate(
ActorManager actors,
System.Func<int, int, bool>? losBlocked = null)
{
var player = actors.Player;
if (player is null) return null;
var blocked = losBlocked ?? LineOfSight.AlwaysClear;
NpcActor? best = null;
int bestDistSq = C.INTERACT_PROMPT_TILES * C.INTERACT_PROMPT_TILES;
foreach (var npc in actors.Npcs)
{
if (!npc.IsAlive) continue;
if (npc.Allegiance != Rules.Character.Allegiance.Friendly &&
npc.Allegiance != Rules.Character.Allegiance.Neutral) continue;
int distSq = ChebyshevDistSq(player.Position, npc.Position);
if (distSq > bestDistSq) continue;
if (!LineOfSight.HasLine(player.Position, npc.Position, blocked)) continue;
if (best is null || distSq < bestDistSq) { best = npc; bestDistSq = distSq; }
}
return best;
}
private static int ChebyshevDistSq(Vec2 a, Vec2 b)
{
int dx = (int)System.Math.Abs(a.X - b.X);
int dy = (int)System.Math.Abs(a.Y - b.Y);
int d = System.Math.Max(dx, dy);
return d * d;
}
}