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,140 @@
|
||||
using Theriapolis.Core;
|
||||
using Theriapolis.Core.Data;
|
||||
using Theriapolis.Core.Items;
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Phase 6.5 M4 — Medical Incompatibility scales healing received by a
|
||||
/// hybrid PC at 75% (round down, min 1). Verified end-to-end via the
|
||||
/// healer features wired in M1.
|
||||
/// </summary>
|
||||
public sealed class HybridMedicalIncompatibilityTests
|
||||
{
|
||||
private readonly ContentResolver _content = new(new ContentLoader(TestHelpers.DataDirectory));
|
||||
|
||||
[Fact]
|
||||
public void FieldRepair_OnHybridTarget_ScalesAtSeventyFivePercent()
|
||||
{
|
||||
var enc = MakeEncounter(out var healer, out var ally,
|
||||
healerClass: "claw_wright", isAllyHybrid: true);
|
||||
|
||||
ally.CurrentHp = 5;
|
||||
int beforeHp = ally.CurrentHp;
|
||||
FeatureProcessor.TryFieldRepair(enc, healer, ally);
|
||||
|
||||
// 1d8 + INT mod (claw_wright kit gives INT 14 → +2 mod) → range 3–10.
|
||||
// After 0.75 scale: range 2–7 (rounded down, min 1).
|
||||
int gained = ally.CurrentHp - beforeHp;
|
||||
Assert.True(gained >= 2, $"hybrid should still gain at least 2 HP after scaling; got {gained}");
|
||||
Assert.True(gained <= 7, $"hybrid heal should be 75% of raw range; got {gained}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FieldRepair_OnPurebredTarget_DoesNotScale()
|
||||
{
|
||||
var enc = MakeEncounter(out var healer, out var ally,
|
||||
healerClass: "claw_wright", isAllyHybrid: false);
|
||||
ally.CurrentHp = 5;
|
||||
int beforeHp = ally.CurrentHp;
|
||||
FeatureProcessor.TryFieldRepair(enc, healer, ally);
|
||||
int gained = ally.CurrentHp - beforeHp;
|
||||
// Purebred: full 1d8 + INT 2 → range 3–10.
|
||||
Assert.True(gained >= 3, $"purebred should gain full heal; got {gained}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LayOnPaws_OnHybridTarget_ScalesDeliveredHpButNotPoolCost()
|
||||
{
|
||||
var enc = MakeEncounter(out var healer, out var ally,
|
||||
healerClass: "covenant_keeper", isAllyHybrid: true);
|
||||
FeatureProcessor.EnsureLayOnPawsPoolReady(healer.SourceCharacter!);
|
||||
int poolBefore = healer.SourceCharacter!.LayOnPawsPoolRemaining;
|
||||
|
||||
ally.CurrentHp = ally.MaxHp - 4;
|
||||
FeatureProcessor.TryLayOnPaws(enc, healer, ally, requestHp: 4);
|
||||
|
||||
// Pool cost is the requested 4 (the inefficiency models the body
|
||||
// resisting calibration, not the healer wasting effort).
|
||||
Assert.Equal(poolBefore - 4, healer.SourceCharacter.LayOnPawsPoolRemaining);
|
||||
// But ally only receives 3 HP (4 * 0.75 = 3, floor).
|
||||
int gained = ally.CurrentHp - (ally.MaxHp - 4);
|
||||
Assert.Equal(3, gained);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LayOnPaws_OnPurebredTarget_DeliversFullHp()
|
||||
{
|
||||
var enc = MakeEncounter(out var healer, out var ally,
|
||||
healerClass: "covenant_keeper", isAllyHybrid: false);
|
||||
FeatureProcessor.EnsureLayOnPawsPoolReady(healer.SourceCharacter!);
|
||||
|
||||
ally.CurrentHp = ally.MaxHp - 4;
|
||||
FeatureProcessor.TryLayOnPaws(enc, healer, ally, requestHp: 4);
|
||||
Assert.Equal(ally.MaxHp, ally.CurrentHp);
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
private Encounter MakeEncounter(
|
||||
out Combatant healer, out Combatant ally,
|
||||
string healerClass, bool isAllyHybrid)
|
||||
{
|
||||
var hc = MakeChar(healerClass, new AbilityScores(10, 12, 13, 14, 12, 14));
|
||||
var ac = isAllyHybrid
|
||||
? MakeHybrid()
|
||||
: MakeChar("fangsworn", new AbilityScores(15, 12, 13, 10, 10, 8));
|
||||
healer = Combatant.FromCharacter(hc, 1, "Healer", new Vec2(0, 0), Allegiance.Player);
|
||||
ally = Combatant.FromCharacter(ac, 2, "Ally", new Vec2(1, 0), Allegiance.Allied);
|
||||
return new Encounter(0xCAFEUL, 1, new[] { healer, ally });
|
||||
}
|
||||
|
||||
private Theriapolis.Core.Rules.Character.Character MakeChar(string classId, AbilityScores a)
|
||||
{
|
||||
var b = new CharacterBuilder
|
||||
{
|
||||
Clade = _content.Clades["canidae"],
|
||||
Species = _content.Species["wolf"],
|
||||
ClassDef = _content.Classes[classId],
|
||||
Background = _content.Backgrounds["pack_raised"],
|
||||
BaseAbilities = a,
|
||||
};
|
||||
int n = b.ClassDef.SkillsChoose;
|
||||
foreach (var raw in b.ClassDef.SkillOptions)
|
||||
{
|
||||
if (b.ChosenClassSkills.Count >= n) break;
|
||||
try { b.ChosenClassSkills.Add(SkillIdExtensions.FromJson(raw)); } catch { }
|
||||
}
|
||||
return b.Build(_content.Items);
|
||||
}
|
||||
|
||||
private Theriapolis.Core.Rules.Character.Character MakeHybrid()
|
||||
{
|
||||
var b = new CharacterBuilder
|
||||
{
|
||||
ClassDef = _content.Classes["fangsworn"],
|
||||
Background = _content.Backgrounds["pack_raised"],
|
||||
BaseAbilities = new AbilityScores(15, 14, 13, 12, 10, 8),
|
||||
IsHybridOrigin = true,
|
||||
HybridSireClade = _content.Clades["canidae"],
|
||||
HybridSireSpecies = _content.Species["wolf"],
|
||||
HybridDamClade = _content.Clades["leporidae"],
|
||||
HybridDamSpecies = _content.Species["rabbit"],
|
||||
HybridDominantParent = ParentLineage.Sire,
|
||||
};
|
||||
int n = b.ClassDef.SkillsChoose;
|
||||
foreach (var raw in b.ClassDef.SkillOptions)
|
||||
{
|
||||
if (b.ChosenClassSkills.Count >= n) break;
|
||||
try { b.ChosenClassSkills.Add(SkillIdExtensions.FromJson(raw)); } catch { }
|
||||
}
|
||||
bool ok = b.TryBuildHybrid(_content.Items, out var c, out string err);
|
||||
Assert.True(ok, err);
|
||||
return c!;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user