diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-28 19:03:22 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-28 19:03:22 +0800 |
| commit | 588560deda6b701a62653dff19079dc0a21fa944 (patch) | |
| tree | 3d734aaf9ad546ec52a42af0a2c2635d2d26d670 /src/LivingVillage.Desktop.Tests/P43HudTests.fs | |
| parent | 5c67e85688ff1e6150b03501adae1a5d5e67bbdc (diff) | |
| download | living-village-588560deda6b701a62653dff19079dc0a21fa944.tar.gz | |
p43: HUD 星露谷化切片 — 右上日历钟卡/体力条形图/底部10格hotbar(1-0选中+高亮+输入隔离)
Diffstat (limited to 'src/LivingVillage.Desktop.Tests/P43HudTests.fs')
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/P43HudTests.fs | 129 |
1 files changed, 129 insertions, 0 deletions
diff --git a/src/LivingVillage.Desktop.Tests/P43HudTests.fs b/src/LivingVillage.Desktop.Tests/P43HudTests.fs new file mode 100644 index 0000000..18f2e78 --- /dev/null +++ b/src/LivingVillage.Desktop.Tests/P43HudTests.fs @@ -0,0 +1,129 @@ +namespace LivingVillage.Desktop.Tests + +open System +open Microsoft.VisualStudio.TestTools.UnitTesting +open LivingVillage.Kernel +open LivingVillage.Kernel.Sim +open LivingVillage.Desktop + +/// P43 界面切片:右上日历钟卡、体力条形图、底部 10 格 hotbar 的纯模型与输入隔离回归。 +[<TestClass>] +type P43HudTests () = + + let world = Sim.initialWorldN 42UL 4 + let tickNoon = M6Presentation.resolveStartTick (Some 12.0) false + let tickNight = M6Presentation.resolveStartTick (Some 23.5) false + + let assertEqualMsg (expected: 'a) (actual: 'a) (message: string) = + Assert.IsTrue((expected = actual), sprintf "%s (expected %A, actual %A)" message expected actual) + + let closeTo (expected: float32) (actual: float32) (message: string) = + Assert.IsTrue(abs (expected - actual) < 1e-4f, sprintf "%s (expected %f, actual %f)" message expected actual) + + // ---- 1. 日历钟卡 ---- + + [<TestMethod>] + member _.CalendarReadingIsDerivedFromRealTick () = + let reading = HudLayout.calendarReading tickNoon + assertEqualMsg (M6Presentation.timePeriodLabel tickNoon) reading.Period "时段须取自 tick 纯函数" + assertEqualMsg (M6Presentation.clockTimeLabel tickNoon) reading.Clock "时钟须取自 tick 纯函数" + assertEqualMsg (tickNoon / Sim.ticksPerDay + 1L) reading.DayNumber "第 N 天 = tick/ticksPerDay + 1" + Assert.IsTrue(HudLayout.seasonNames |> Array.contains reading.Season, "季节须为四季之一") + let seasonOfDay (day: int64) = (HudLayout.calendarReading (day * Sim.ticksPerDay)).Season + let distinct = [ 0L .. 111L ] |> List.map seasonOfDay |> List.distinct + assertEqualMsg 4 distinct.Length "四个季节都应出现" + assertEqualMsg (seasonOfDay 0L) (seasonOfDay 27L) "同一季内 28 天不换季" + Assert.IsTrue(seasonOfDay 0L <> seasonOfDay 28L, "满 28 天必须换季") + + [<TestMethod>] + member _.CalendarCardFitsTopRightWithoutOverlap () = + for tick in [ tickNoon; tickNight ] do + let reading = HudLayout.calendarReading tick + let rect = HudLayout.calendarCardRect 1280 reading + Assert.IsTrue(rect.X >= 0 && rect.Y >= 0, "卡片不得越出视口左上") + Assert.IsTrue(rect.X + rect.Width <= 1280, "卡片不得越出视口右边") + Assert.IsTrue(rect.Y >= HudLayout.barHeight + HudLayout.accentHeight, "卡片不得压住顶部窄条") + let line1 = HudLayout.calendarLine1 reading + let line2 = HudLayout.calendarLine2 reading + let textW = + max + (FloatingPrompt.measureWidth line1 HudLayout.textScale) + (FloatingPrompt.measureWidth line2 HudLayout.textScale) + Assert.IsTrue(HudLayout.iconWidth + HudLayout.iconGap + textW <= rect.Width, "卡片宽度须容下图标与两行文字") + Assert.IsTrue(2 * (8 * HudLayout.textScale) + HudLayout.calendarLineGap <= rect.Height, "卡片高度须容下两行文字") + + // ---- 2. 体力图形化条形 ---- + + [<TestMethod>] + member _.EnergyBarIsProportionalAndClampedToRealState () = + closeTo 0.0f (HudLayout.energyBarFill 0.0f) "0 体力空条" + closeTo 1.0f (HudLayout.energyBarFill 100.0f) "满体力满条" + closeTo 0.5f (HudLayout.energyBarFill 50.0f) "50 体力半条" + closeTo 0.0f (HudLayout.energyBarFill -25.0f) "下限截断" + closeTo 1.0f (HudLayout.energyBarFill 250.0f) "上限截断" + let track = HudLayout.energyBarTrack 100 10 60 + let fill = HudLayout.energyBarFillRect 100 10 60 world.Avatar.Mind.Needs.Energy + Assert.IsTrue(fill.X >= track.X, "填充不得越过轨道左沿") + Assert.IsTrue(fill.X + fill.Width <= track.X + track.Width, "填充不得越过轨道右沿") + Assert.IsTrue(fill.Height <= track.Height, "填充不得高过轨道") + assertEqualMsg 0 (HudLayout.energyBarFillRect 100 10 60 0.0f).Width "空体力宽度为 0" + assertEqualMsg track.Width (HudLayout.energyBarFillRect 100 10 60 100.0f).Width "满体力填满轨道" + + // ---- 3. hotbar 布局与选中 ---- + + [<TestMethod>] + member _.HotbarSlotsAreTenDistinctNonOverlappingAndInsideViewport () = + assertEqualMsg 10 HudLayout.hotbarSlotCount "共 10 格" + let bar = HudLayout.hotbarBarRect 1280 720 + Assert.IsTrue(bar.X >= 0 && bar.Y >= 0, "hotbar 不得越出视口") + Assert.IsTrue(bar.X + bar.Width <= 1280 && bar.Y + bar.Height <= 720, "hotbar 不得越出视口") + let slots = [ 0 .. HudLayout.hotbarSlotCount - 1 ] |> List.map (HudLayout.hotbarSlotRect 1280 720) + for slot in slots do + Assert.IsTrue(slot.X >= bar.X && slot.Y >= bar.Y, "槽位须在底栏内") + Assert.IsTrue( + slot.X + slot.Width <= bar.X + bar.Width && slot.Y + slot.Height <= bar.Y + bar.Height, + "槽位须在底栏内") + assertEqualMsg HudLayout.hotbarSlotSize slot.Width "槽宽" + assertEqualMsg HudLayout.hotbarSlotSize slot.Height "槽高" + slots + |> List.pairwise + |> List.iter (fun (a, b) -> Assert.IsTrue(b.X >= a.X + a.Width, "槽位不得重叠")) + + [<TestMethod>] + member _.HotbarIconsAreTenOriginalWellFormedAndDistinct () = + let icons = [ 0 .. HudLayout.hotbarSlotCount - 1 ] |> List.map HudLayout.hotbarIconRows + for rows in icons do + assertEqualMsg HudLayout.iconGrid rows.Length "图标行数" + for row in rows do + assertEqualMsg HudLayout.iconGrid row.Length "图标行宽" + Assert.IsTrue(rows |> List.exists (fun row -> row.Contains "1"), "图标须有实心像素") + assertEqualMsg HudLayout.hotbarSlotCount (icons |> List.distinct |> List.length) "图标互异" + Assert.IsTrue(HudLayout.iconGrid * HudLayout.hotbarIconScale <= HudLayout.hotbarSlotSize, "图标须放进槽位") + + [<TestMethod>] + member _.HotbarDigitKeysOneThroughZeroSelectSlots () : unit = + for digit in 1 .. 9 do + assertEqualMsg (Some (digit - 1)) (HudLayout.selectSlotForDigit digit) (sprintf "键 %d" digit) + assertEqualMsg (Some 9) (HudLayout.selectSlotForDigit 0) "键 0 -> 第 10 格" + assertEqualMsg None (HudLayout.selectSlotForDigit 11) "非 1-0 键不选格" + + [<TestMethod>] + member _.HotbarSelectionCyclesWithinBounds () = + assertEqualMsg 0 (HudLayout.cycleHotbar 0 0) "原地" + assertEqualMsg 9 (HudLayout.cycleHotbar 0 -1) "首格向前到末格" + assertEqualMsg 0 (HudLayout.cycleHotbar 9 1) "末格向后到首格" + assertEqualMsg 3 (HudLayout.cycleHotbar 8 5) "跨回绕" + assertEqualMsg (HudLayout.hotbarSlotCount - 1) (HudLayout.cycleHotbar 0 (HudLayout.hotbarSlotCount - 1)) "最大步" + for start in 0 .. HudLayout.hotbarSlotCount - 1 do + for delta in -25 .. 25 do + let next = HudLayout.cycleHotbar start delta + Assert.IsTrue(next >= 0 && next < HudLayout.hotbarSlotCount, sprintf "循环越界 %d+%d" start delta) + + // ---- 4. 菜单打开时 hotbar 输入隔离 ---- + + [<TestMethod>] + member _.HotbarInputIsIsolatedWhileMenuOrPanelIsOpen () = + Assert.IsTrue(HudLayout.hotbarInputAllowed true true, "游戏内世界输入可用时 hotbar 可响应") + Assert.IsFalse(HudLayout.hotbarInputAllowed false true, "Esc 菜单打开(非 Playing)时 hotbar 输入不穿透") + Assert.IsFalse(HudLayout.hotbarInputAllowed true false, "面板/对话打开时 hotbar 输入不穿透") + Assert.IsFalse(HudLayout.hotbarInputAllowed false false, "菜单+面板同时打开时更不穿透") |
