diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-21 10:12:20 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-21 10:12:20 +0800 |
| commit | 9df2da41289f4d0852f86e86eae81ca7f7e7b598 (patch) | |
| tree | bca1a071e3369a9e2ebbf5e424da3dd45547f2be /src/LivingVillage.Desktop.Tests | |
| parent | c932969539b00a42937f0aa5ed1e10b4e94932a3 (diff) | |
| download | living-village-9df2da41289f4d0852f86e86eae81ca7f7e7b598.tar.gz | |
fix(desktop): 修复 HUD 缺字并统一轻量面板与情境提示
字符表扩充至 197 字(补 间/门/谈/体/屋/返/×/,/:等全部 UI 用字),用 Noto Sans CJK 重新生成图集并逐字校验;新增字符表覆盖全部 UI 文案、瞬态状态 2.5 秒过期、对话完成后回到轻量面板等回归测试;户外与室内统一左下角轻量 HUD,互动日志改为短暂反馈;样例脚本保留 E 交谈 提示与对话菜单停留帧。
Diffstat (limited to 'src/LivingVillage.Desktop.Tests')
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/DesktopTests.fs | 17 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/PrototypeTests.fs | 89 |
2 files changed, 100 insertions, 6 deletions
diff --git a/src/LivingVillage.Desktop.Tests/DesktopTests.fs b/src/LivingVillage.Desktop.Tests/DesktopTests.fs index 667ab69..526d1af 100644 --- a/src/LivingVillage.Desktop.Tests/DesktopTests.fs +++ b/src/LivingVillage.Desktop.Tests/DesktopTests.fs @@ -53,6 +53,23 @@ type DesktopTests () = Assert.IsTrue(observationView.Observations |> List.exists (fun observation -> observation.Id = NpcId 0)) [<TestMethod>] + member _.DialogueAndOverlayViewsRejectWorldCommandsUntilClosed () = + let world = Sim.initialWorldN 42UL 4 |> Harness.atNpc 0 + let openedWorld, dialogueView = M5Interaction.apply Interact world M5Interaction.initial + let _, afterObserve = M5Interaction.apply Observe openedWorld dialogueView + let _, afterNeeds = M5Interaction.apply ToggleNeeds openedWorld dialogueView + let _, afterChronicle = M5Interaction.apply ShowChronicle openedWorld dialogueView + let _, needsView = M5Interaction.apply ToggleNeeds world M5Interaction.initial + let _, interactionAttempt = M5Interaction.apply Interact world needsView + let _, closedView = M5Interaction.apply ClosePanel world dialogueView + + Assert.AreEqual<M5View>(dialogueView, afterObserve) + Assert.AreEqual<M5View>(dialogueView, afterNeeds) + Assert.AreEqual<M5View>(dialogueView, afterChronicle) + Assert.AreEqual<M5Panel>(NeedsPanel, interactionAttempt.Panel) + Assert.AreEqual<M5Panel>(WorldPanel, closedView.Panel) + + [<TestMethod>] member _.HeadlessSmokeProvesTheThreeHopCausalChain () = let result = Program.M5Smoke.run () diff --git a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs index c5b127f..a403223 100644 --- a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs +++ b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs @@ -148,7 +148,7 @@ type PrototypeTests () = Avatar = { world.Avatar with Pos = world.Npcs.[0].Pos } } let openedWorld, opened = M5Interaction.apply Interact nearby M5Interaction.initial let title = M5Interaction.titleText opened - let needsWorld, needsView = M5Interaction.apply ToggleNeeds openedWorld opened + let needsWorld, needsView = M5Interaction.apply ToggleNeeds openedWorld M5Interaction.initial let needsText = String.concat "|" (M5Interaction.panelLines needsWorld needsView) let _, chronicleView = M5Interaction.apply ShowChronicle nearby M5Interaction.initial let chronicleText = String.concat "|" (M5Interaction.panelLines nearby chronicleView) @@ -387,26 +387,103 @@ type PrototypeTests () = [<TestMethod>] member _.WorldPanelDoesNotRenderA永久KeyList () = - let lines = M5Interaction.panelLines (Sim.initialWorldN 42UL 0) M5Interaction.initial + let world = Sim.initialWorldN 42UL 0 + let lines = M5Interaction.panelLines world M5Interaction.initial + Assert.AreEqual<string list>([], lines) Assert.IsFalse(lines |> List.exists (fun line -> line = "Q 观察" || line = "TAB 需求" || line = "C 年鉴" || line = "L 关系")) [<TestMethod>] + member _.WorldPanelStatusIsTransientAndExpiresAfterTwoAndAHalfSeconds () = + let world = Sim.initialWorldN 42UL 0 + let _, interacted = M5Interaction.apply ClosePanel world M5Interaction.initial + let stamped = { interacted with StatusTick = Some world.Tick } + Assert.AreEqual<string list>([ "E 进屋"; "面板已关闭" ], M5Interaction.panelLines world stamped) + + let expired = { stamped with StatusTick = Some (world.Tick - M5Interaction.statusDisplayDurationTicks) } + Assert.AreEqual<string list>([ "E 进屋" ], M5Interaction.panelLines world expired) + + [<TestMethod>] + member _.DialogueCompletionReturnsToTheLightweightWorldPanel () = + let world = Sim.initialWorldN 42UL 4 |> fun value -> { value with Avatar = { value.Avatar with Pos = value.Npcs.[0].Pos } } + let openedWorld, opened = M5Interaction.apply Interact world M5Interaction.initial + Assert.AreEqual<M5Panel>(DialoguePanel, opened.Panel) + Assert.IsTrue(opened.Menu.IsSome) + + let nextWorld, view = M5Interaction.apply Intent1 openedWorld opened + Assert.AreEqual<M5Panel>(WorldPanel, view.Panel) + Assert.AreEqual<DialogueMenu option>(None, view.Menu) + Assert.AreEqual<int64 option>(Some nextWorld.Tick, view.StatusTick) + Assert.IsTrue(M5Interaction.statusText view |> fun text -> text.Contains("对话完成")) + Assert.IsTrue(M5Interaction.transientStatusLine nextWorld.Tick view |> Option.map (fun line -> line.Contains("对话完成")) |> Option.defaultValue false) + + [<TestMethod>] + member _.InteractionRangeAndTargetSelectionStayDeterministicAtTheBoundary () = + let avatar : Vec2 = + { X = float32 (32 * Sim.tilePixels + Sim.tilePixels / 2) + Y = float32 (24 * Sim.tilePixels + Sim.tilePixels / 2) } + let baseWorld = Sim.initialWorldN 42UL 4 + + let withNpcPositions (positions: (int * Vec2) list) = + let npcs = Array.copy baseWorld.Npcs + for index in 0 .. npcs.Length - 1 do + npcs.[index] <- { npcs.[index] with Pos = ({ X = 2000.0f; Y = 1400.0f } : Vec2) } + for index, position in positions do + npcs.[index] <- { npcs.[index] with Pos = position } + { baseWorld with + Avatar = { baseWorld.Avatar with Pos = avatar } + Npcs = npcs } + + let atBoundary = withNpcPositions [ 0, { X = avatar.X + Sim.chatRangePx; Y = avatar.Y } ] + let beyondBoundary = withNpcPositions [ 0, { X = avatar.X + Sim.chatRangePx + 0.1f; Y = avatar.Y } ] + let competing = + withNpcPositions + [ 0, { X = avatar.X + 72.0f; Y = avatar.Y } + 1, { X = avatar.X + 48.0f; Y = avatar.Y } ] + let switched = + { competing with + Avatar = { competing.Avatar with Pos = { X = avatar.X + 64.0f; Y = avatar.Y } } } + + Assert.AreEqual<InteractionAction option>(Some(TalkTo(NpcId 0)), InteractionResolver.resolve atBoundary Outside |> Option.map (fun target -> target.Action)) + Assert.AreEqual<InteractionAction option>(None, InteractionResolver.resolve beyondBoundary Outside |> Option.map (fun target -> target.Action)) + Assert.AreEqual<InteractionAction option>(Some(TalkTo(NpcId 1)), InteractionResolver.resolve competing Outside |> Option.map (fun target -> target.Action)) + Assert.AreEqual<InteractionAction option>(Some(TalkTo(NpcId 0)), InteractionResolver.resolve switched Outside |> Option.map (fun target -> target.Action)) + + [<TestMethod>] member _.CjkGlyphAtlasHasAStableCharacterTableAndGeometry () = - Assert.AreEqual<int>(188, characters.Length) + Assert.AreEqual<int>(197, characters.Length) CollectionAssert.AreEqual(characters, characters |> Array.sort) Assert.AreEqual<int>(characters.Length, characters |> Array.distinct |> Array.length) Assert.AreEqual<int>(16, columns) Assert.AreEqual<int>(24, cellPixels) - Assert.AreEqual<int>(12, rowCount) + Assert.AreEqual<int>(13, rowCount) Assert.AreEqual<int>(384, pixelWidth) - Assert.AreEqual<int>(288, pixelHeight) + Assert.AreEqual<int>(312, pixelHeight) Assert.AreEqual<string>("Assets/cjk-glyph-atlas.png", assetRelativePath) Assert.AreEqual<string>("Assets/cjk-glyphs.txt", manifestRelativePath) [<TestMethod>] + member _.RequiredUiLabelsAreFullyCoveredByTheGlyphTable () = + let labels = ChineseText.requiredUiLabels + + Assert.IsTrue(labels |> List.forall PixelText.isRenderable) + Assert.IsTrue(labels |> List.contains "时间") + Assert.IsTrue(labels |> List.contains "E 出屋") + Assert.IsTrue(labels |> List.contains "E 进屋") + Assert.IsTrue(labels |> List.contains "E 交谈") + Assert.IsTrue(PixelText.isRenderable "时间 白天") + Assert.IsTrue(PixelText.isRenderable "2倍速") + Assert.IsTrue(PixelText.isRenderable "E 出门") + Assert.IsTrue(PixelText.isRenderable "E 交谈") + Assert.IsFalse(PixelText.isRenderable "龘") + + [<TestMethod>] member _.CjkGlyphAtlasMatchesThePackagedManifestAndRejectsUnknownCharacters () = - Assert.IsTrue(ChineseText.requiredUiLabels |> List.forall (fun label -> label |> Seq.forall (fun character -> character = ' ' || not (Char.IsLetter character) || contains character))) + Assert.IsTrue(ChineseText.requiredUiLabels |> List.forall PixelText.isRenderable) Assert.IsTrue(contains '江') + Assert.IsTrue(contains '间') + Assert.IsTrue(contains '门') + Assert.IsTrue(contains '谈') + Assert.IsTrue(contains '体') Assert.IsTrue(contains '龘' |> not) Assert.AreEqual<int>(0, sourceRectangle '龘' |> Option.toList |> List.length) let manifestPath = runtimeManifestPath () |
