1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
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, "菜单+面板同时打开时更不穿透")
|