diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-22 16:42:26 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-22 16:42:26 +0800 |
| commit | 4f512323e521bf57dee7b2bd5e060b69b7b6d029 (patch) | |
| tree | b4f51622fa34617a9efef3d452af99d025761883 /src/LivingVillage.Desktop.Tests/P31StoryUiTests.fs | |
| parent | 9821b54ff2a77539898b62471d79dc4685b59cad (diff) | |
| download | living-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/P31StoryUiTests.fs')
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/P31StoryUiTests.fs | 105 |
1 files changed, 105 insertions, 0 deletions
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)) |
