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>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user