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; /// /// 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. /// 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!; } }