Files
TheriapolisV3/Theriapolis.Tests/Dungeons/ConsumableHandlerTests.cs
T
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

186 lines
7.5 KiB
C#

using Theriapolis.Core.Data;
using Theriapolis.Core.Items;
using Theriapolis.Core.Rules.Character;
using Theriapolis.Core.Rules.Stats;
using Xunit;
namespace Theriapolis.Tests.Dungeons;
/// <summary>
/// Phase 7 M2 — central consumable-dispatch tests + Phase 6.5 M4 carryover
/// (Hybrid Medical Incompatibility scaling on healing potions).
/// </summary>
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!;
}
}