summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop.Tests
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-21 10:12:20 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-21 10:12:20 +0800
commit9df2da41289f4d0852f86e86eae81ca7f7e7b598 (patch)
treebca1a071e3369a9e2ebbf5e424da3dd45547f2be /src/LivingVillage.Desktop.Tests
parentc932969539b00a42937f0aa5ed1e10b4e94932a3 (diff)
downloadliving-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.fs17
-rw-r--r--src/LivingVillage.Desktop.Tests/PrototypeTests.fs89
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 ()