Files
TheriapolisV3/Theriapolis.Tests/Combat/HybridMedicalIncompatibilityTests.cs
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

141 lines
5.7 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 310.
// After 0.75 scale: range 27 (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 310.
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!;
}
}