summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop.Tests
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-28 18:48:05 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-28 18:48:05 +0800
commitd3a2604e30b1deb91afebf550a79cde71bbca6d6 (patch)
tree7cf10505e76f5a6749faadb96ff4d678a3c8fb26 /src/LivingVillage.Desktop.Tests
parent323552b7e49550d378d3871fadcb711c16c71fed (diff)
downloadliving-village-d3a2604e30b1deb91afebf550a79cde71bbca6d6.tar.gz
p42: 启动/菜单水面涟漪整改 — 振幅0.32+波峰列, 帧差14.3, 防降振回归 (leader fix: 提交+推送)
Diffstat (limited to 'src/LivingVillage.Desktop.Tests')
-rw-r--r--src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj1
-rw-r--r--src/LivingVillage.Desktop.Tests/P42LaunchFxTests.fs79
2 files changed, 80 insertions, 0 deletions
diff --git a/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj b/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj
index 46bede8..03d63bd 100644
--- a/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj
+++ b/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj
@@ -21,6 +21,7 @@
<Compile Include="P38HudIconTests.fs" />
<Compile Include="P39ArtTests.fs" />
<Compile Include="P40ArtTests.fs" />
+ <Compile Include="P42LaunchFxTests.fs" />
<Compile Include="SampleTests.fs" />
</ItemGroup>
diff --git a/src/LivingVillage.Desktop.Tests/P42LaunchFxTests.fs b/src/LivingVillage.Desktop.Tests/P42LaunchFxTests.fs
new file mode 100644
index 0000000..9c1fe6d
--- /dev/null
+++ b/src/LivingVillage.Desktop.Tests/P42LaunchFxTests.fs
@@ -0,0 +1,79 @@
+namespace LivingVillage.Desktop.Tests
+
+open Microsoft.VisualStudio.TestTools.UnitTesting
+open LivingVillage.Desktop
+
+/// P42 launch/menu environment FX:
+/// * water ripples whose brightness phase is a pure function of (frame, column, row);
+/// * eave/bank lantern breathing glow through the shared P14 premultiplied-alpha path;
+/// * a deterministic shot table for the LV_P42_SHOT evidence hook.
+/// Every function is frame-driven, so re-rendering the same frame is byte-identical.
+[<TestClass>]
+type P42LaunchFxTests () =
+
+ [<TestMethod>]
+ member _.RippleAndBreathAreDeterministicForTheSameFrame () =
+ for frame in [ 0L; 1L; 17L; 30L; 150L; 4097L ] do
+ for column in [ 0; 1; 7; 39 ] do
+ for rowIndex in [ 0; 1 ] do
+ Assert.AreEqual<float32>(
+ TitleScreen.waterRippleBrightness frame column rowIndex,
+ TitleScreen.waterRippleBrightness frame column rowIndex)
+ Assert.AreEqual<int>(
+ TitleScreen.waterRippleLift frame column rowIndex,
+ TitleScreen.waterRippleLift frame column rowIndex)
+ for index in [ 0; 1; 3 ] do
+ Assert.AreEqual<int>(
+ TitleScreen.lanternBreathAlpha frame index,
+ TitleScreen.lanternBreathAlpha frame index)
+
+ [<TestMethod>]
+ member _.RipplePhaseMovesWithTheFrame () =
+ // The same column changes brightness as the frame advances (the ripple travels),
+ // and adjacent columns differ at a fixed frame (the ripple is per-tile).
+ let values = [ 0L .. 24L ] |> List.map (fun f -> TitleScreen.waterRippleBrightness f 5 0)
+ Assert.IsTrue(values |> List.distinct |> List.length > 1, "ripple must move with the frame")
+ let atFrame = [ 0 .. 12 ] |> List.map (fun c -> TitleScreen.waterRippleBrightness 40L c 0)
+ Assert.IsTrue(atFrame |> List.distinct |> List.length > 1, "ripple must vary across columns")
+ // Over a full period it swings both above and below the base brightness.
+ let overPeriod = [ for f in 0L .. 80L -> TitleScreen.waterRippleBrightness f 5 0 ]
+ Assert.IsTrue(overPeriod |> List.exists (fun v -> v > 1.0f) && overPeriod |> List.exists (fun v -> v < 1.0f),
+ "ripple must swing above and below the base brightness")
+
+ [<TestMethod>]
+ member _.BreathAmplitudeIsBoundedAndVaries () =
+ let samples =
+ [ for frame in 0L .. 120L do
+ for index in 0 .. 5 do
+ yield TitleScreen.lanternBreathAlpha frame index ]
+ Assert.IsTrue(samples |> List.forall (fun a -> a >= 40 && a <= 96),
+ "breath alpha must stay in [40, 96] so it never goes dark or saturates")
+ Assert.IsTrue(samples |> List.distinct |> List.length > 1, "breath must vary with frame/index")
+ // Out-of-step lanterns: two adjacent lights differ at the same frame.
+ Assert.AreNotEqual<int>(TitleScreen.lanternBreathAlpha 50L 0, TitleScreen.lanternBreathAlpha 50L 1)
+
+ [<TestMethod>]
+ member _.P42ShotTableIsOrderedAndNamesAreDistinct () =
+ Assert.IsTrue(P42ShotScript.splashEarlyFrame < P42ShotScript.splashLateFrame)
+ Assert.IsTrue(LaunchScreen.revealProgress P42ShotScript.splashEarlyFrame > 0.0f)
+ Assert.IsTrue(LaunchScreen.revealProgress P42ShotScript.splashLateFrame < 1.0f,
+ "both splash FX frames must still show the water band")
+ Assert.IsTrue(P42ShotScript.menuFrame >= 120L, "menu shot must wait for the entrance to settle")
+ let names = [ P42ShotScript.splashEarlyName; P42ShotScript.splashLateName; P42ShotScript.menuName ]
+ Assert.AreEqual<int>(names.Length, names |> List.distinct |> List.length)
+
+ [<TestMethod>]
+ member _.RippleAmplitudeStaysAboveVisibilityBudget () =
+ // Guards against a regression that quietly lowers the ripple back below visibility.
+ Assert.IsTrue(TitleScreen.waterRippleAmplitude >= 0.25f,
+ "ripple amplitude must stay >= 0.25 to read at 1280x720 (1x)")
+ let samples =
+ [ for frame in 0L .. 40L do
+ for column in 0 .. 39 do
+ yield TitleScreen.waterRippleBrightness frame column 0 ]
+ let peakToPeak = (List.max samples) - (List.min samples)
+ Assert.IsTrue(peakToPeak >= 0.5f, sprintf "ripple peak-to-peak %.3f must be >= 0.50" peakToPeak)
+ // Adjacent 28px tile columns must differ enough to be visible at 1x.
+ let brightness = TitleScreen.waterRippleBrightness 150L
+ let adjacent = [ for column in 0 .. 38 -> abs (brightness column 0 - brightness (column + 1) 0) ]
+ Assert.IsTrue(List.max adjacent >= 0.2f, sprintf "max adjacent-column ripple step %.3f must be >= 0.20" (List.max adjacent))