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>
68 lines
2.9 KiB
C#
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);
|
|
}
|
|
}
|