Files
TheriapolisV3/Theriapolis.Core/Rules/Combat/ReachAndCover.cs
T
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

68 lines
2.9 KiB
C#

using Theriapolis.Core.Rules.Stats;
using Theriapolis.Core.Util;
namespace Theriapolis.Core.Rules.Combat;
/// <summary>
/// Size-aware spatial helpers for combat. Combatants occupy
/// <see cref="Stats.SizeExtensions.FootprintTiles"/>² tactical tiles
/// anchored at their integer <see cref="Combatant.Position"/>; this helper
/// computes edge-to-edge Chebyshev distance and reach predicates.
/// </summary>
public static class ReachAndCover
{
/// <summary>
/// Edge-to-edge Chebyshev distance — number of empty tiles between two
/// footprints. Adjacent (sharing an edge or corner) returns 0; one
/// empty tile between returns 1; overlapping returns 0.
/// </summary>
public static int EdgeToEdgeChebyshev(Combatant a, Combatant b)
{
int aSize = a.Size.FootprintTiles();
int bSize = b.Size.FootprintTiles();
int aMinX = (int)System.Math.Floor(a.Position.X);
int aMinY = (int)System.Math.Floor(a.Position.Y);
int aMaxX = aMinX + aSize - 1;
int aMaxY = aMinY + aSize - 1;
int bMinX = (int)System.Math.Floor(b.Position.X);
int bMinY = (int)System.Math.Floor(b.Position.Y);
int bMaxX = bMinX + bSize - 1;
int bMaxY = bMinY + bSize - 1;
// Per-axis gap: positive = number of tile-steps to bring edges to
// touching (then -1 because touching = 0 empty tiles between).
int dx = System.Math.Max(0, System.Math.Max(aMinX - bMaxX, bMinX - aMaxX) - 1);
int dy = System.Math.Max(0, System.Math.Max(aMinY - bMaxY, bMinY - aMaxY) - 1);
return System.Math.Max(dx, dy);
}
/// <summary>True if <paramref name="defender"/> is within the attack's reach (melee) or short range (ranged).</summary>
public static bool IsInReach(Combatant attacker, Combatant defender, AttackOption attack)
{
int dist = EdgeToEdgeChebyshev(attacker, defender);
if (attack.IsRanged)
return dist <= attack.RangeLongTiles;
return dist <= attack.ReachTiles;
}
/// <summary>True if the defender sits past short range (disadvantage on the attack).</summary>
public static bool IsLongRange(Combatant attacker, Combatant defender, AttackOption attack)
{
if (!attack.IsRanged) return false;
int dist = EdgeToEdgeChebyshev(attacker, defender);
return dist > attack.RangeShortTiles && dist <= attack.RangeLongTiles;
}
/// <summary>
/// One step of greedy movement toward <paramref name="goal"/>. Returns
/// the new position one tile closer in 8-connected (Chebyshev) space.
/// Movement budget is ignored — the caller is responsible for charging it.
/// </summary>
public static Vec2 StepToward(Vec2 from, Vec2 goal)
{
int dx = System.Math.Sign(goal.X - from.X);
int dy = System.Math.Sign(goal.Y - from.Y);
return new Vec2((int)from.X + dx, (int)from.Y + dy);
}
}