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; /// /// 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. /// public sealed class OptionConditionTests { private static ContentResolver Content() => new ContentResolver(new ContentLoader(TestHelpers.DataDirectory)); private static (DialogueRunner runner, PlayerReputation rep, Dictionary 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(); 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 = "" }); 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 = "" }); 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 = "" }); 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 = "" }); 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 = "" }); // 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 = "" }); 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 = "" }); 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 = "" }); var (r, _, flags, _) = Setup(tree); Assert.Empty(r.VisibleOptions()); flags["a"] = 1; Assert.Empty(r.VisibleOptions()); flags["b"] = 1; Assert.Single(r.VisibleOptions()); } }