b451f83174
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>
186 lines
7.5 KiB
C#
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!;
|
|
}
|
|
}
|