summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop.Tests
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-22 16:42:26 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-22 16:42:26 +0800
commit4f512323e521bf57dee7b2bd5e060b69b7b6d029 (patch)
treeb4f51622fa34617a9efef3d452af99d025761883 /src/LivingVillage.Desktop.Tests
parent9821b54ff2a77539898b62471d79dc4685b59cad (diff)
downloadliving-village-4f512323e521bf57dee7b2bd5e060b69b7b6d029.tar.gz
feat(desktop): P31 step4 story progress display + evidence shots
- Task panel (T key): a storyline row per occupation, e.g. ju-qing-xian: shui-wei-yu-shou-cheng, zou-fang-zhong (line name + stage name); absent for occupation-free saves. - Occupation select page: minimal hint line showing the selected occupation's storyline name. - Evidence hook LV_STORY_SHOT=1 (+LV_STORY_OCCUPATION, LV_STORY_STAGE): boots a real world through the standard bootstrap, advances the story via the real dialogue pipeline (chooseDialogueWith bias + Story.onDialogue + StoryAnnal append), opens the task panel and saves one back-buffer frame, then exits. 4 Xvfb shots (farmer/fisher/peddler/scholar) captured under art/ (gitignored, not committed). - 4 new desktop tests (156 -> 160): panel storyline lines, bias + story advance through M5Interaction, no-occupation path stays story-free.
Diffstat (limited to 'src/LivingVillage.Desktop.Tests')
-rw-r--r--src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj1
-rw-r--r--src/LivingVillage.Desktop.Tests/P31StoryUiTests.fs105
2 files changed, 106 insertions, 0 deletions
diff --git a/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj b/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj
index a0c98b0..d4fa1f5 100644
--- a/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj
+++ b/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj
@@ -10,6 +10,7 @@
<Compile Include="M6aTests.fs" />
<Compile Include="M6bTests.fs" />
<Compile Include="P16UiTests.fs" />
+ <Compile Include="P31StoryUiTests.fs" />
<Compile Include="PrototypeTests.fs" />
<Compile Include="MapGenTests.fs" />
<Compile Include="PerformanceSummaryTests.fs" />
diff --git a/src/LivingVillage.Desktop.Tests/P31StoryUiTests.fs b/src/LivingVillage.Desktop.Tests/P31StoryUiTests.fs
new file mode 100644
index 0000000..562794d
--- /dev/null
+++ b/src/LivingVillage.Desktop.Tests/P31StoryUiTests.fs
@@ -0,0 +1,105 @@
+namespace LivingVillage.Desktop.Tests
+
+open System
+open Microsoft.VisualStudio.TestTools.UnitTesting
+open LivingVillage.Kernel
+open LivingVillage.Kernel.Sim
+open LivingVillage.Desktop
+
+module private P31Harness =
+
+ let atNpc (id: int) (world: World) : World =
+ let npc = world.Npcs.[id]
+ { world with Avatar = { world.Avatar with Pos = npc.Pos } }
+
+ let nearWorld () : World = Sim.initialWorldN 42UL 4 |> atNpc 0
+
+ let npcIndex (NpcId id) : int = id
+
+[<TestClass>]
+type P31StoryUiTests () =
+
+ // ---- 任务面板:剧情线名 + 进度段名 ----
+
+ [<TestMethod>]
+ member _.TaskPanelShowsTheStorylineNameAndStage () =
+ let world = Sim.initialWorldN 42UL 4
+ let state = { Occupation.stateOf Occupation.Fisher with StoryStage = 2 }
+ let view = { M5Interaction.initial with Task = Some state }
+ let _, taskView = M5Interaction.apply ShowTaskPanel world view
+ let lines = M5Interaction.panelLines world taskView
+ Assert.IsTrue(lines |> List.contains "剧情线:水位与收成,走访中", $"lines={lines}")
+
+ [<TestMethod>]
+ member _.TaskPanelShowsConcludedStageAndNoOccupationStaysStoryFree () =
+ let world = Sim.initialWorldN 42UL 4
+ let concluded = { Occupation.stateOf Occupation.Scholar with StoryStage = 3 }
+ let view = { M5Interaction.initial with Task = Some concluded }
+ let _, taskView = M5Interaction.apply ShowTaskPanel world view
+ Assert.IsTrue(
+ M5Interaction.panelLines world taskView
+ |> List.contains "剧情线:庙前的争论,已收束")
+ let _, noneView = M5Interaction.apply ShowTaskPanel world M5Interaction.initial
+ Assert.IsFalse(
+ M5Interaction.panelLines world noneView
+ |> List.exists (fun line -> line.Contains("剧情线")))
+
+ // ---- 对话接线:回应偏差进记忆 + 剧情线推进 + 年鉴写入 ----
+
+ [<TestMethod>]
+ member _.DialogueThroughM5AppliesBiasAndAdvancesTheStory () =
+ let world = { P31Harness.nearWorld () with Tick = 5L * Sim.ticksPerDay }
+ let state = Occupation.stateOf Occupation.Fisher
+ let view = { M5Interaction.initial with Task = Some state; Seed = 42UL }
+ let personality = world.Npcs.[0].Mind.Personality
+ let openedWorld, openedView = M5Interaction.apply Interact world view
+ match openedView.Menu with
+ | None -> Assert.Fail("dialogue menu should open next to a villager")
+ | Some menu ->
+ let finalWorld, finalView = M5Interaction.apply Intent1 openedWorld openedView
+ // 剧情线推进一段(tick 第 5 日 ≥ 开线日)
+ let task = finalView.Task.Value
+ Assert.AreEqual<int>(1, task.StoryStage)
+ // 年鉴写入剧情条目
+ Assert.IsTrue(
+ finalWorld.Annals
+ |> List.exists (fun entry ->
+ match entry.Kind with
+ | StoryAnnal -> entry.Summary.Contains("接了件事")
+ | _ -> false))
+ // 回应偏差可观测:偏差后的回应与纯函数口径一致
+ let biased = Occupation.biasResponse (Some Occupation.Fisher) SmallTalk personality
+ let unbiased = Sim.responseFor SmallTalk personality
+ if biased <> unbiased then
+ Assert.IsTrue(
+ finalWorld.Annals
+ |> List.exists (fun entry ->
+ match entry.Kind with
+ | DialogueAnnal outcome -> outcome.Response = biased
+ | _ -> false),
+ "the biased response must be the one recorded")
+
+ [<TestMethod>]
+ member _.NoOccupationDialogueKeepsTheHistoricResponsePath () =
+ let world = { P31Harness.nearWorld () with Tick = 5L * Sim.ticksPerDay }
+ let view = M5Interaction.initial
+ let openedWorld, openedView = M5Interaction.apply Interact world view
+ match openedView.Menu with
+ | None -> Assert.Fail("dialogue menu should open next to a villager")
+ | Some menu ->
+ let personality = openedWorld.Npcs.[P31Harness.npcIndex menu.Target].Mind.Personality
+ let finalWorld, _ = M5Interaction.apply Intent1 openedWorld openedView
+ let expected = Sim.responseFor SmallTalk personality
+ Assert.IsTrue(
+ finalWorld.Npcs.[0].Mind.Memory
+ |> List.exists (fun memory ->
+ match memory.Kind with
+ | Dialogue (_, SmallTalk, response) -> response = expected
+ | _ -> false))
+ // 无职业不产生剧情条目,剧情线状态不存在
+ Assert.IsFalse(
+ finalWorld.Annals
+ |> List.exists (fun entry ->
+ match entry.Kind with
+ | StoryAnnal -> true
+ | _ -> false))