using Theriapolis.Core.Data; using Theriapolis.Core.Items; using Theriapolis.Core.Rules.Character; using Theriapolis.Core.Rules.Stats; using Xunit; namespace Theriapolis.Tests.Dungeons; /// /// Phase 7 M2 — central consumable-dispatch tests + Phase 6.5 M4 carryover /// (Hybrid Medical Incompatibility scaling on healing potions). /// public sealed class ConsumableHandlerTests { private readonly ContentResolver _content = new(new ContentLoader(TestHelpers.DataDirectory)); // ── Healing potion ─────────────────────────────────────────────────── [Fact] public void Consume_HealingPotion_RestoresHp() { var pc = MakePurebred(); pc.CurrentHp = 1; var potion = _content.Items["healing_potion"]; var result = ConsumableHandler.Consume(potion, pc, seed: 0xFEEDUL); Assert.True(result.IsSuccess); Assert.Equal(ConsumeResult.ResultKind.Healed, result.Kind); Assert.True(result.HealedAmount >= 4, // 2d4+2 minimum = 4 $"healing potion should heal ≥ 4 HP, healed {result.HealedAmount}"); Assert.True(pc.CurrentHp > 1); } [Fact] public void Consume_HealingPotion_OnHybridPc_AppliesMedicalIncompatibility() { var pcPure = MakePurebred(); var pcHybrid = MakeHybrid(); // Pin both at 1 HP; same seed so the dice are identical. pcPure.CurrentHp = 1; pcHybrid.CurrentHp = 1; // Boost MaxHp so neither caps to MaxHp. pcPure.MaxHp = 100; pcHybrid.MaxHp = 100; var potion = _content.Items["healing_potion"]; var pureResult = ConsumableHandler.Consume(potion, pcPure, seed: 42UL); var hybridResult = ConsumableHandler.Consume(potion, pcHybrid, seed: 42UL); // Hybrid should heal 75% (round down, min 1) of the same dice roll. // 0.75 * 4 = 3, 0.75 * 8 = 6, 0.75 * 10 = 7. So hybrid amount < pure // amount whenever the pure roll is ≥ 4 (which 2d4+2 always is). Assert.True(hybridResult.IsSuccess); Assert.True(hybridResult.WasScaledForHybrid); Assert.True(hybridResult.HealedAmount < pureResult.HealedAmount, $"hybrid {hybridResult.HealedAmount} should be < pure {pureResult.HealedAmount}"); Assert.True(hybridResult.HealedAmount >= 1, "min-heal floor is 1 even for hybrids"); } [Fact] public void Consume_HealingPotion_DeterministicForSameSeed() { var pcA = MakePurebred(); pcA.CurrentHp = 1; pcA.MaxHp = 100; var pcB = MakePurebred(); pcB.CurrentHp = 1; pcB.MaxHp = 100; var potion = _content.Items["healing_potion"]; var a = ConsumableHandler.Consume(potion, pcA, seed: 0xC0FFEEUL); var b = ConsumableHandler.Consume(potion, pcB, seed: 0xC0FFEEUL); Assert.Equal(a.HealedAmount, b.HealedAmount); } // ── Scent masks ────────────────────────────────────────────────────── [Fact] public void Consume_ScentMaskBasic_OnHybridPc_SetsBasicTier() { var pc = MakeHybrid(); var mask = _content.Items["scent_mask_basic"]; var result = ConsumableHandler.Consume(mask, pc, seed: 0); Assert.True(result.IsSuccess); Assert.Equal(ConsumeResult.ResultKind.MaskApplied, result.Kind); Assert.Equal(ScentMaskTier.Basic, result.MaskTier); Assert.True(result.MaskHadEffect); Assert.Equal(ScentMaskTier.Basic, pc.Hybrid!.ActiveMaskTier); } [Fact] public void Consume_ScentMaskMilitary_OnHybridPc_SetsMilitaryTier() { var pc = MakeHybrid(); var mask = _content.Items["scent_mask_military"]; var result = ConsumableHandler.Consume(mask, pc, seed: 0); Assert.Equal(ScentMaskTier.Military, result.MaskTier); Assert.Equal(ScentMaskTier.Military, pc.Hybrid!.ActiveMaskTier); } [Fact] public void Consume_ScentMaskDeepCover_OnHybridPc_SetsDeepCoverTier() { var pc = MakeHybrid(); var mask = _content.Items["scent_mask_deep_cover"]; var result = ConsumableHandler.Consume(mask, pc, seed: 0); Assert.Equal(ScentMaskTier.DeepCover, result.MaskTier); Assert.Equal(ScentMaskTier.DeepCover, pc.Hybrid!.ActiveMaskTier); } [Fact] public void Consume_ScentMask_OnPurebredPc_SucceedsWithoutEffect() { var pc = MakePurebred(); var mask = _content.Items["scent_mask_basic"]; var result = ConsumableHandler.Consume(mask, pc, seed: 0); Assert.True(result.IsSuccess); Assert.False(result.MaskHadEffect); } // ── Rejection paths ────────────────────────────────────────────────── [Fact] public void Consume_NonConsumable_Rejects() { var pc = MakePurebred(); var weapon = _content.Items["fang_knife"]; // kind = "weapon" var result = ConsumableHandler.Consume(weapon, pc, seed: 0); Assert.Equal(ConsumeResult.ResultKind.Rejected, result.Kind); } [Fact] public void Consume_UnknownConsumableKind_ReturnsUnrecognized() { var pc = MakePurebred(); var unknown = new ItemDef { Id = "fake_consumable", Kind = "consumable", ConsumableKind = "tea_party" }; var result = ConsumableHandler.Consume(unknown, pc, seed: 0); Assert.Equal(ConsumeResult.ResultKind.Unrecognized, result.Kind); Assert.Equal("fake_consumable", result.UnrecognizedItemId); } // ── Helpers ────────────────────────────────────────────────────────── private Character MakePurebred() { var b = new CharacterBuilder { Clade = _content.Clades["canidae"], Species = _content.Species["wolf"], ClassDef = _content.Classes["fangsworn"], Background = _content.Backgrounds["pack_raised"], BaseAbilities = new AbilityScores(15, 14, 13, 10, 12, 8), }; 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 Character MakeHybrid() { var b = new CharacterBuilder { ClassDef = _content.Classes["fangsworn"], Background = _content.Backgrounds["pack_raised"], BaseAbilities = new AbilityScores(15, 14, 13, 10, 12, 8), IsHybridOrigin = true, HybridSireClade = _content.Clades["canidae"], HybridSireSpecies = _content.Species["wolf"], HybridDamClade = _content.Clades["leporidae"], HybridDamSpecies = _content.Species["hare"], 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 { } } Assert.True(b.TryBuildHybrid(_content.Items, out var ch, out _)); return ch!; } }