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>
161 lines
6.0 KiB
C#
161 lines
6.0 KiB
C#
using Theriapolis.Core;
|
|
using Theriapolis.Core.Data;
|
|
using Theriapolis.Core.Entities;
|
|
using Theriapolis.Core.Rules.Character;
|
|
using Theriapolis.Core.Rules.Dialogue;
|
|
using Theriapolis.Core.Rules.Reputation;
|
|
using Theriapolis.Core.Rules.Stats;
|
|
using Xunit;
|
|
|
|
namespace Theriapolis.Tests.Dialogue;
|
|
|
|
/// <summary>
|
|
/// Phase 6 M3 — option visibility predicates: rep_at_least, has_flag,
|
|
/// has_item, ability_min, and their negations. The runner hides options
|
|
/// whose conditions fail; the visible-option iterator must skip them.
|
|
/// </summary>
|
|
public sealed class OptionConditionTests
|
|
{
|
|
private static ContentResolver Content() =>
|
|
new ContentResolver(new ContentLoader(TestHelpers.DataDirectory));
|
|
|
|
private static (DialogueRunner runner, PlayerReputation rep, Dictionary<string,int> flags, Character pc)
|
|
Setup(DialogueDef tree)
|
|
{
|
|
var content = Content();
|
|
var pc = new CharacterBuilder()
|
|
.WithClade(content.Clades["canidae"])
|
|
.WithSpecies(content.Species["wolf"])
|
|
.WithClass(content.Classes["fangsworn"])
|
|
.WithBackground(content.Backgrounds["pack_raised"])
|
|
.WithAbilities(new AbilityScores(13, 12, 14, 10, 14, 11))
|
|
.ChooseSkill(SkillId.Athletics)
|
|
.ChooseSkill(SkillId.Perception)
|
|
.Build();
|
|
var npc = new NpcActor(content.Residents["generic_innkeeper"]) { Id = 1, RoleTag = "test.npc" };
|
|
var rep = new PlayerReputation();
|
|
var flags = new Dictionary<string, int>();
|
|
var ctx = new DialogueContext(npc, pc, rep, flags, content);
|
|
return (new DialogueRunner(tree, ctx, 0xCAFEBABEUL), rep, flags, pc);
|
|
}
|
|
|
|
private static DialogueDef OneNode(params DialogueOptionDef[] opts)
|
|
=> new DialogueDef
|
|
{
|
|
Id = "test_opts",
|
|
Root = "n",
|
|
Nodes = new[]
|
|
{
|
|
new DialogueNodeDef { Id = "n", Speaker = "npc", Text = "?", Options = opts },
|
|
},
|
|
};
|
|
|
|
[Fact]
|
|
public void HasFlag_HidesUntilSet()
|
|
{
|
|
var tree = OneNode(
|
|
new DialogueOptionDef { Text = "Visible only after gifted",
|
|
Conditions = new[] { new DialogueConditionDef { Kind = "has_flag", Flag = "gifted" } },
|
|
Next = "<end>" });
|
|
var (r, _, flags, _) = Setup(tree);
|
|
Assert.Empty(r.VisibleOptions());
|
|
flags["gifted"] = 1;
|
|
Assert.Single(r.VisibleOptions());
|
|
}
|
|
|
|
[Fact]
|
|
public void NotHasFlag_VisibleUntilSet()
|
|
{
|
|
var tree = OneNode(
|
|
new DialogueOptionDef { Text = "Visible until done",
|
|
Conditions = new[] { new DialogueConditionDef { Kind = "not_has_flag", Flag = "done" } },
|
|
Next = "<end>" });
|
|
var (r, _, flags, _) = Setup(tree);
|
|
Assert.Single(r.VisibleOptions());
|
|
flags["done"] = 1;
|
|
Assert.Empty(r.VisibleOptions());
|
|
}
|
|
|
|
[Fact]
|
|
public void RepAtLeast_GatesOnFactionStanding()
|
|
{
|
|
var tree = OneNode(
|
|
new DialogueOptionDef { Text = "Friendly only",
|
|
Conditions = new[] { new DialogueConditionDef { Kind = "rep_at_least", Faction = "covenant_enforcers", Value = 25 } },
|
|
Next = "<end>" });
|
|
var (r, rep, _, _) = Setup(tree);
|
|
Assert.Empty(r.VisibleOptions());
|
|
rep.Factions.Set("covenant_enforcers", 25);
|
|
Assert.Single(r.VisibleOptions());
|
|
rep.Factions.Set("covenant_enforcers", 24);
|
|
Assert.Empty(r.VisibleOptions());
|
|
}
|
|
|
|
[Fact]
|
|
public void RepAtLeast_NoFaction_ChecksEffectiveDisposition()
|
|
{
|
|
var tree = OneNode(
|
|
new DialogueOptionDef { Text = "Personal only",
|
|
Conditions = new[] { new DialogueConditionDef { Kind = "rep_at_least", Value = 30 } },
|
|
Next = "<end>" });
|
|
var (r, rep, _, _) = Setup(tree);
|
|
Assert.Empty(r.VisibleOptions());
|
|
// Pump personal disposition above threshold.
|
|
rep.PersonalFor("test.npc").Apply(new RepEvent { Kind = RepEventKind.Aid, RoleTag = "test.npc", Magnitude = 60 });
|
|
Assert.Single(r.VisibleOptions());
|
|
}
|
|
|
|
[Fact]
|
|
public void AbilityMin_GatesOnAbilityMod()
|
|
{
|
|
var tree = OneNode(
|
|
new DialogueOptionDef { Text = "Wise option",
|
|
Conditions = new[] { new DialogueConditionDef { Kind = "ability_min", Ability = "WIS", Value = 2 } },
|
|
Next = "<end>" });
|
|
// Setup gave the PC WIS=14 → mod=+2; passes.
|
|
var (r, _, _, _) = Setup(tree);
|
|
Assert.Single(r.VisibleOptions());
|
|
|
|
// Higher threshold fails.
|
|
var tree2 = OneNode(
|
|
new DialogueOptionDef { Text = "Sage option",
|
|
Conditions = new[] { new DialogueConditionDef { Kind = "ability_min", Ability = "WIS", Value = 5 } },
|
|
Next = "<end>" });
|
|
var (r2, _, _, _) = Setup(tree2);
|
|
Assert.Empty(r2.VisibleOptions());
|
|
}
|
|
|
|
[Fact]
|
|
public void HasItem_DependsOnInventory()
|
|
{
|
|
var content = Content();
|
|
var tree = OneNode(
|
|
new DialogueOptionDef { Text = "Show me your knife",
|
|
Conditions = new[] { new DialogueConditionDef { Kind = "has_item", Id = "fang_knife" } },
|
|
Next = "<end>" });
|
|
var (r, _, _, pc) = Setup(tree);
|
|
Assert.Empty(r.VisibleOptions());
|
|
pc.Inventory.Add(content.Items["fang_knife"]);
|
|
Assert.Single(r.VisibleOptions());
|
|
}
|
|
|
|
[Fact]
|
|
public void MultipleConditions_AreAnded()
|
|
{
|
|
var tree = OneNode(
|
|
new DialogueOptionDef { Text = "Both",
|
|
Conditions = new[]
|
|
{
|
|
new DialogueConditionDef { Kind = "has_flag", Flag = "a" },
|
|
new DialogueConditionDef { Kind = "has_flag", Flag = "b" },
|
|
},
|
|
Next = "<end>" });
|
|
var (r, _, flags, _) = Setup(tree);
|
|
Assert.Empty(r.VisibleOptions());
|
|
flags["a"] = 1;
|
|
Assert.Empty(r.VisibleOptions());
|
|
flags["b"] = 1;
|
|
Assert.Single(r.VisibleOptions());
|
|
}
|
|
}
|