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>
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
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!;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user