summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop.Tests/P43HudTests.fs
blob: 18f2e782a4f90a59a778b4ac755c0f2cd1658a49 (plain)
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, "菜单+面板同时打开时更不穿透")