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

100 lines
3.6 KiB
C#

using Theriapolis.Core.Data;
using Theriapolis.Core.Rules.Character;
using Theriapolis.Core.Rules.Combat;
using Theriapolis.Core.Rules.Stats;
using Theriapolis.Core.Util;
using Xunit;
namespace Theriapolis.Tests.Combat;
public sealed class ReachAndCoverTests
{
private readonly ContentResolver _content = new(new ContentLoader(TestHelpers.DataDirectory));
[Fact]
public void EdgeToEdge_Adjacent_ReturnsZero()
{
var a = MakeMediumNpc(new Vec2(5, 5));
var b = MakeMediumNpc(new Vec2(6, 5));
Assert.Equal(0, ReachAndCover.EdgeToEdgeChebyshev(a, b));
}
[Fact]
public void EdgeToEdge_OneTileApart_ReturnsOne()
{
var a = MakeMediumNpc(new Vec2(5, 5));
var b = MakeMediumNpc(new Vec2(7, 5)); // 1 empty tile between
Assert.Equal(1, ReachAndCover.EdgeToEdgeChebyshev(a, b));
}
[Fact]
public void EdgeToEdge_LargeAttacker_FootprintCountedCorrectly()
{
var large = MakeLargeNpc(new Vec2(0, 0)); // occupies (0..1, 0..1)
var medium = MakeMediumNpc(new Vec2(3, 0)); // 1 empty tile between (large's right edge = 1, medium = 3)
Assert.Equal(1, ReachAndCover.EdgeToEdgeChebyshev(large, medium));
}
[Fact]
public void IsInReach_MeleeAdjacent_True()
{
var a = MakeMediumNpc(new Vec2(0, 0));
var b = MakeMediumNpc(new Vec2(1, 0));
var attack = new AttackOption { Name = "Bite", Damage = new DamageRoll(1, 4, 0, DamageType.Piercing), ReachTiles = 1 };
Assert.True(ReachAndCover.IsInReach(a, b, attack));
}
[Fact]
public void IsInReach_MeleeOutOfReach_False()
{
var a = MakeMediumNpc(new Vec2(0, 0));
var b = MakeMediumNpc(new Vec2(3, 0));
var attack = new AttackOption { Name = "Bite", Damage = new DamageRoll(1, 4, 0, DamageType.Piercing), ReachTiles = 1 };
Assert.False(ReachAndCover.IsInReach(a, b, attack));
}
[Fact]
public void IsInReach_RangedShortRange_True()
{
var a = MakeMediumNpc(new Vec2(0, 0));
var b = MakeMediumNpc(new Vec2(8, 0));
var bow = new AttackOption { Name = "Bow", Damage = new DamageRoll(1, 6, 0, DamageType.Piercing),
RangeShortTiles = 16, RangeLongTiles = 64 };
Assert.True(ReachAndCover.IsInReach(a, b, bow));
Assert.False(ReachAndCover.IsLongRange(a, b, bow));
}
[Fact]
public void IsInReach_RangedLongRange_TrueWithLongRangeFlag()
{
var a = MakeMediumNpc(new Vec2(0, 0));
var b = MakeMediumNpc(new Vec2(40, 0));
var bow = new AttackOption { Name = "Bow", Damage = new DamageRoll(1, 6, 0, DamageType.Piercing),
RangeShortTiles = 16, RangeLongTiles = 64 };
Assert.True(ReachAndCover.IsInReach(a, b, bow));
Assert.True(ReachAndCover.IsLongRange(a, b, bow));
}
[Fact]
public void StepToward_MovesOneTileTowardGoal()
{
var step = ReachAndCover.StepToward(new Vec2(0, 0), new Vec2(5, 3));
Assert.Equal(1, step.X);
Assert.Equal(1, step.Y);
}
[Fact]
public void StepToward_AtGoal_ReturnsSamePosition()
{
var step = ReachAndCover.StepToward(new Vec2(5, 5), new Vec2(5, 5));
Assert.Equal(5, step.X);
Assert.Equal(5, step.Y);
}
private Combatant MakeMediumNpc(Vec2 pos)
=> Combatant.FromNpcTemplate(_content.Npcs.Templates.First(t => t.Id == "wolf"), id: 1, pos);
private Combatant MakeLargeNpc(Vec2 pos)
=> Combatant.FromNpcTemplate(_content.Npcs.Templates.First(t => t.Id == "bear_brown"), id: 2, pos);
}