summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop.Tests/P46MenuAmbienceTests.fs
blob: d01b07f899d3504168a41709523b611aef3b2669 (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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
namespace LivingVillage.Desktop.Tests

open Microsoft.VisualStudio.TestTools.UnitTesting
open LivingVillage.Desktop

/// P46 menu/splash environment FX:
///  * per-tile water ripple whose phase reuses VillageArt.waterFrameTickAt;
///  * lantern breathing glow alpha;
///  * two-layer parallax cloud shadow above the ridge (pure horizontal drift);
///  * a deterministic shot table + layout invariants (nothing moved, nothing clipped).
/// Every function is frame-driven, so re-rendering the same frame is byte-identical.
[<TestClass>]
type P46MenuAmbienceTests () =

    let twoPiOverThree = 2.0f * float32 System.Math.PI / 3.0f
    let assertInt (expected: int) (actual: int) (message: string) =
        Assert.IsTrue((expected = actual), sprintf "%s: expected %d actual %d" message expected actual)
    let assertClose (expected: float32) (actual: float32) (message: string) =
        Assert.IsTrue((abs (expected - actual) < 1e-5f), sprintf "%s: expected %f actual %f" message expected actual)

    [<TestMethod>]
    member _.WaterPhaseReusesWaterFrameTickAtOnTheCycleBoundary () =
        // At every 32-tick boundary the fractional part is zero, so the phase must equal
        // the waterFrameTickAt index scaled to 120-degree steps: that is the reuse contract.
        for frame in [ 0L; 32L; 64L; 31L * 32L ] do
            for column in [ 0; 1; 7; 39 ] do
                for rowIndex in [ 0; 1 ] do
                    let expected =
                        float32 (VillageArt.waterFrameTickAt frame column rowIndex) * twoPiOverThree
                    assertClose expected (MenuAmbience.waterPhase frame column rowIndex)
                        (sprintf "frame %d col %d row %d must reuse waterFrameTickAt" frame column rowIndex)

    [<TestMethod>]
    member _.AllEffectsAreDeterministicForTheSameFrame () =
        for frame in [ 0L; 1L; 16L; 33L; 173L; 4097L ] do
            for column in [ 0; 5; 39 ] do
                for rowIndex in [ 0; 1 ] do
                    assertClose
                        (MenuAmbience.waterColumnBrightness frame column rowIndex)
                        (MenuAmbience.waterColumnBrightness frame column rowIndex)
                        "water brightness must be deterministic"
                    assertInt
                        (MenuAmbience.waterColumnLift frame column rowIndex)
                        (MenuAmbience.waterColumnLift frame column rowIndex)
                        "water lift must be deterministic"
            for index in [ 0; 1; 3 ] do
                assertInt
                    (MenuAmbience.lanternBreathAlpha frame index)
                    (MenuAmbience.lanternBreathAlpha frame index)
                    "breath must be deterministic"
        for layer in [ 0; 1 ] do
            assertInt
                (MenuAmbience.cloudOffsetPx layer 140L 1280)
                (MenuAmbience.cloudOffsetPx layer 140L 1280)
                "cloud offset must be deterministic"
            assertInt
                (List.length (MenuAmbience.cloudPuffs layer 140L 1280))
                (List.length (MenuAmbience.cloudPuffs layer 140L 1280))
                "cloud puffs must be deterministic"

    [<TestMethod>]
    member _.WaterRippleVariesAcrossColumnsAndFrames () =
        Assert.IsTrue(MenuAmbience.waterRippleAmplitude >= 0.25f, "ripple amplitude must stay >= 0.25")
        // Adjacent tile columns sit 120 degrees apart, so they differ visibly.
        let adjacent =
            [ for column in 0 .. 38 ->
                  abs (MenuAmbience.waterColumnBrightness 150L column 0
                       - MenuAmbience.waterColumnBrightness 150L (column + 1) 0) ]
        Assert.IsTrue(List.max adjacent >= 0.2f, sprintf "adjacent column step %.3f must be >= 0.20" (List.max adjacent))
        // Across one row the column brightness has a real spread.
        let columns = [ for column in 0 .. 39 -> float (MenuAmbience.waterColumnBrightness 150L column 0) ]
        let mean = List.average columns
        let std = sqrt (List.average [ for v in columns -> (v - mean) ** 2.0 ])
        Assert.IsTrue(std >= 0.15, sprintf "across-column std %.3f must be >= 0.15" std)
        // And the same column moves as the frame advances within a 32-tick window.
        let overFrames = [ for frame in 0L .. 31L -> MenuAmbience.waterColumnBrightness frame 5 0 ]
        Assert.IsTrue(overFrames |> List.distinct |> List.length > 3, "ripple must move with the frame")

    [<TestMethod>]
    member _.LanternBreathIsBoundedAndSwingsTwoPhases () =
        let samples =
            [ for frame in 0L .. 260L do
                  for index in 0 .. 5 -> MenuAmbience.lanternBreathAlpha frame index ]
        Assert.IsTrue(samples |> List.forall (fun a -> a >= 40 && a <= 96),
                      "breath alpha must stay in [40, 96]")
        Assert.IsTrue(samples |> List.distinct |> List.length > 1, "breath must vary")
        Assert.AreNotEqual<int>(MenuAmbience.lanternBreathAlpha 50L 0, MenuAmbience.lanternBreathAlpha 50L 1)
        // The two evidence phases must actually be a peak and a trough (readable luma delta).
        let peak = MenuAmbience.lanternBreathAlpha (P46ShotScript.menuPeakFrame + 1L) 0
        let trough = MenuAmbience.lanternBreathAlpha (P46ShotScript.menuTroughFrame + 1L) 0
        Assert.IsTrue(peak - trough >= 50, sprintf "breath phases %d vs %d must differ by >= 50" peak trough)

    [<TestMethod>]
    member _.CloudParallaxIsBoundedAndLayered () =
        let width = 1280
        let span = width + 200
        assertInt 2 MenuAmbience.cloudLayerCount "two parallax layers"
        for layer in 0 .. MenuAmbience.cloudLayerCount - 1 do
            for frame in [ 0L; 40L; 100L; 140L; 10000L ] do
                let offset = MenuAmbience.cloudOffsetPx layer frame width
                Assert.IsTrue(offset >= 0 && offset < span, sprintf "layer %d offset %d out of [0,%d)" layer offset span)
                // Every cloud body sits entirely above the ridge line: no ground/roof overlap.
                for (_, cy, _, ry) in MenuAmbience.cloudPuffs layer frame width do
                    Assert.IsTrue(cy - ry >= 0 && cy + ry < 44,
                                  sprintf "layer %d cloud y %d..%d must stay above ridge 44" layer (cy - ry) (cy + ry))
        // The inner layer drifts slower than the outer one (parallax), inside one span.
        assertInt (2 * MenuAmbience.cloudOffsetPx 0 100L width) (MenuAmbience.cloudOffsetPx 1 100L width) "parallax 2x"
        // Drift moves with the frame.
        assertInt 20 (MenuAmbience.cloudOffsetPx 0 140L width - MenuAmbience.cloudOffsetPx 0 100L width) "cloud drift px"

    [<TestMethod>]
    member _.P46ShotTableIsOrderedAndNamesAreDistinct () =
        Assert.IsTrue(P46ShotScript.splashRippleFrame < P46ShotScript.cloudEarlyFrame)
        Assert.IsTrue(P46ShotScript.cloudEarlyFrame < P46ShotScript.cloudLateFrame)
        Assert.IsTrue(P46ShotScript.menuPeakFrame < P46ShotScript.menuTroughFrame)
        Assert.IsTrue(LaunchScreen.revealProgress P46ShotScript.splashRippleFrame < 1.0f,
                      "the ripple frame must still show the water band")
        Assert.IsTrue(P46ShotScript.cloudLateFrame <= LaunchScreen.durationFrames,
                      "cloud frames must fall inside the splash")
        Assert.IsTrue(P46ShotScript.menuPeakFrame >= 120L && P46ShotScript.menuTroughFrame >= 120L,
                      "menu lantern phases must wait for the entrance to settle")
        let names =
            [ P46ShotScript.splashRippleName
              P46ShotScript.cloudEarlyName
              P46ShotScript.cloudLateName
              P46ShotScript.menuPeakName
              P46ShotScript.menuTroughName ]
        assertInt names.Length (names |> List.distinct |> List.length) "shot names distinct"

    [<TestMethod>]
    member _.LayoutKeyFieldsAreUnchanged () =
        // Pins the splash/menu layout so an environment-FX change can never silently move it.
        let layout = TitleScreen.layout 1280 720
        assertInt 44 layout.RidgeY "RidgeY"
        assertInt 76 layout.EaveY "EaveY"
        Assert.AreEqual<int list>([ 624; 656 ], layout.WaterRows)
        assertInt 512 layout.TitleX "TitleX"
        assertInt 120 layout.TitleY "TitleY"
        assertInt 4 layout.TitleScale "TitleScale"
        assertInt 410 layout.PanelX "PanelX"
        assertInt 296 layout.PanelY "PanelY"
        assertInt 460 layout.PanelWidth "PanelWidth"
        assertInt 320 layout.PanelHeight "PanelHeight"
        assertInt 450 layout.MenuX "MenuX"
        assertInt 434 layout.CursorX "CursorX"
        assertInt 466 layout.TextX "TextX"
        assertInt 240 layout.ControlsPanelY "ControlsPanelY"
        assertInt 566 layout.ControlsActionX "ControlsActionX"

    [<TestMethod>]
    member _.LayoutStaysInsideTheViewportAcrossSizes () =
        for (width, height) in [ 320, 240; 640, 480; 800, 600; 1280, 720; 1920, 1080 ] do
            let layout = TitleScreen.layout width height
            let titleWidth = 4 * (4 * 16)
            Assert.IsTrue(layout.TitleX >= 0 && layout.TitleX + titleWidth <= width,
                          sprintf "%dx%d title out of bounds" width height)
            Assert.IsTrue(layout.PanelX >= 0 && layout.PanelY >= 0
                          && layout.PanelX + layout.PanelWidth <= width
                          && layout.PanelY + layout.PanelHeight <= height,
                          sprintf "%dx%d panel out of bounds" width height)
            for rowIndex in 0 .. 4 do
                let cursor, label = TitleScreen.selectionRects layout rowIndex
                Assert.IsTrue(TitleScreen.rectsDisjoint cursor label,
                              sprintf "%dx%d row %d cursor/label overlap" width height rowIndex)
                Assert.IsTrue(label.X >= 0 && label.X + label.Width <= layout.PanelX + layout.PanelWidth,
                              sprintf "%dx%d row %d label out of panel" width height rowIndex)
            for rowIndex in 0 .. 7 do
                let key, action = TitleScreen.controlRowRects layout rowIndex
                Assert.IsTrue(TitleScreen.rectsDisjoint key action,
                              sprintf "%dx%d control row %d overlap" width height rowIndex)
                if height >= 600 then
                    Assert.IsTrue(key.Y + key.Height <= height && action.Y + action.Height <= height,
                                  sprintf "%dx%d control row %d below viewport" width height rowIndex)
            for row in layout.WaterRows do
                Assert.IsTrue(row >= 0 && row + 32 <= height,
                              sprintf "%dx%d water row %d out of bounds" width height row)