summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop.Tests
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-28 20:26:42 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-28 20:26:42 +0800
commit08925d4863d8aaf93a974b8fa21a91e931a0928e (patch)
treef18055c232fc1a6022eeab552402185c6948ec02 /src/LivingVillage.Desktop.Tests
parent2ab4b8a22aaec3482d62433cfa4b73a657f2a165 (diff)
downloadliving-village-08925d4863d8aaf93a974b8fa21a91e931a0928e.tar.gz
p47: 场景细节升级 — 河道倒影(waterFrameTickAt微抖)+岸边芦苇加密度+鸭/叶漂浮元素+近景屋顶统一光源阴影
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/P47SceneDetailTests.fs117
2 files changed, 118 insertions, 0 deletions
diff --git a/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj b/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj
index b158d04..045c1dc 100644
--- a/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj
+++ b/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj
@@ -26,6 +26,7 @@
<Compile Include="P44DialogTests.fs" />
<Compile Include="P45CharacterAnimTests.fs" />
<Compile Include="P46MenuAmbienceTests.fs" />
+ <Compile Include="P47SceneDetailTests.fs" />
<Compile Include="SampleTests.fs" />
</ItemGroup>
diff --git a/src/LivingVillage.Desktop.Tests/P47SceneDetailTests.fs b/src/LivingVillage.Desktop.Tests/P47SceneDetailTests.fs
new file mode 100644
index 0000000..4ff0a66
--- /dev/null
+++ b/src/LivingVillage.Desktop.Tests/P47SceneDetailTests.fs
@@ -0,0 +1,117 @@
+namespace LivingVillage.Desktop.Tests
+
+open Microsoft.VisualStudio.TestTools.UnitTesting
+open LivingVillage.Desktop
+
+/// P47 scene detail:
+/// * water reflections reuse waterFrameTickAt phase and only land on water;
+/// * bank reeds / floating props are deterministic map+tick pure functions;
+/// * the near foreground roof layer uses one unified shadow direction.
+[<TestClass>]
+type P47SceneDetailTests () =
+
+ let map = MapGen.generate (MapGen.defaultParams 42UL)
+ let assertInt (expected: int) (actual: int) (message: string) =
+ Assert.IsTrue((expected = actual), sprintf "%s: expected %d actual %d" message expected actual)
+
+ [<TestMethod>]
+ member _.ReflectionHelpersAreDeterministicAndBounded () =
+ assertInt 3 (fst SceneDetail.shadowOffsetPx) "shadow dx"
+ assertInt 4 (snd SceneDetail.shadowOffsetPx) "shadow dy"
+ // alpha strictly decreases with distance and never goes negative.
+ let alphas = [ for d in 1 .. 6 -> SceneDetail.reflectionAlphaAt d ]
+ Assert.IsTrue(alphas |> List.forall (fun a -> a >= 0), "reflection alpha must be >= 0")
+ Assert.IsTrue(alphas.[0] > alphas.[1] && alphas.[1] > alphas.[2], "alpha must decrease with distance")
+ // wobble is exactly the 3-phase water ramp mapped to -1/0/+1 and is deterministic.
+ for tick in [ 0L; 32L; 64L; 96L ] do
+ for x in [ 0; 3; 9 ] do
+ for y in [ 0; 2 ] do
+ let expected =
+ match VillageArt.waterFrameTickAt tick x y with
+ | 0 -> -1
+ | 1 -> 0
+ | _ -> 1
+ assertInt expected (SceneDetail.reflectionWobbleAt tick x y) "wobble must reuse waterFrameTickAt"
+ Assert.IsTrue(abs (SceneDetail.reflectionWobbleAt tick x y) <= 1, "wobble bounded to +/-1px")
+
+ [<TestMethod>]
+ member _.ReflectionsOnlyLandOnWaterBelowALightSource () =
+ let sources = SceneDetail.lightSourceTiles map
+ Assert.IsTrue(not (List.isEmpty sources), "the generator map must have bank light sources")
+ let streaks = SceneDetail.reflectionStreaks map
+ Assert.IsTrue(not (List.isEmpty streaks), "there must be at least one reflection streak")
+ for (x, y, alpha) in streaks do
+ Assert.IsTrue(SceneDetail.isWater map x y, sprintf "reflection (%d,%d) must sit on water" x y)
+ Assert.IsTrue(alpha > 0, "reflection alpha must be positive")
+ // every streak sits directly below its source within reflectionRange.
+ let sourceSet = sources |> Set.ofList
+ for (x, y, _) in streaks do
+ Assert.IsTrue([ 1 .. SceneDetail.reflectionRange ] |> List.exists (fun d -> Set.contains (x, y - d) sourceSet),
+ sprintf "reflection (%d,%d) must be under a source" x y)
+
+ [<TestMethod>]
+ member _.BankReedsAndFloatersAreDenseAndDeterministic () =
+ let reeds = SceneDetail.bankReedTiles map
+ Assert.IsTrue(not (List.isEmpty reeds), "bank reeds must be non-empty")
+ Assert.IsTrue(reeds |> List.distinct |> List.length = reeds.Length, "reeds must be distinct")
+ for (x, y) in reeds do
+ Assert.IsTrue(not (SceneDetail.isWater map x y), "reeds sit on land")
+ let nearWater =
+ SceneDetail.isWater map (x - 1) y
+ || SceneDetail.isWater map (x + 1) y
+ || SceneDetail.isWater map x (y - 1)
+ || SceneDetail.isWater map x (y + 1)
+ Assert.IsTrue(nearWater, "reeds hug the water edge")
+ Assert.IsTrue(x % 4 <> 0, "extra reeds interleave the existing spacing-4 set")
+ let floats = SceneDetail.floatingProps map
+ Assert.IsTrue(not (List.isEmpty floats), "floating props must be non-empty")
+ Assert.IsTrue(floats |> List.distinct |> List.length = floats.Length, "floaters must be distinct")
+ for (x, y, _) in floats do
+ Assert.IsTrue(SceneDetail.isWater map x y && SceneDetail.isWater map (x + 1) y,
+ "floaters sit on open water")
+ // both float kinds appear across the map.
+ let kinds = floats |> List.map (fun (_, _, k) -> k) |> List.distinct
+ Assert.IsTrue(kinds.Length >= 1, "float kinds present")
+
+ [<TestMethod>]
+ member _.FloatDriftAndFrameAreBoundedAndMoveWithTick () =
+ for tick in [ 0L; 100L; 5000L ] do
+ for seed in [ 0; 7; 31 ] do
+ let dx, dy = SceneDetail.floatDrift tick seed
+ Assert.IsTrue(dx >= -2 && dx <= 2, sprintf "float dx %d out of [-2,2]" dx)
+ Assert.IsTrue(dy >= -1 && dy <= 1, sprintf "float dy %d out of [-1,1]" dy)
+ assertInt (SceneDetail.floatFrame tick seed) (SceneDetail.floatFrame tick seed) "frame deterministic"
+ Assert.IsTrue(SceneDetail.floatFrame tick seed >= 0 && SceneDetail.floatFrame tick seed <= 1, "frame in 0/1")
+ let driftOverTime = [ for t in 0L .. 40L -> SceneDetail.floatDrift t 3 ]
+ Assert.IsTrue(driftOverTime |> List.distinct |> List.length > 1, "drift must move with tick")
+
+ [<TestMethod>]
+ member _.ForegroundRooftopsAreStaggeredAndCoverTheWidth () =
+ let width, height = 1280, 720
+ let rows = SceneDetail.foregroundRooftops width height 0L
+ Assert.IsTrue(rows.Length >= width / 32, "foreground spans the viewport width")
+ for (_, y, w) in rows do
+ let stagger = (height - 64) - y
+ Assert.IsTrue(stagger >= 0 && stagger <= 24 && stagger % 8 = 0,
+ sprintf "stagger %d must be one of 0/8/16/24" stagger)
+ assertInt 32 w "foreground roof cell width"
+ // deterministic for the same tick, and slowly drifts across ticks.
+ Assert.IsTrue(SceneDetail.foregroundRooftops width height 0L = SceneDetail.foregroundRooftops width height 0L)
+ Assert.IsTrue(SceneDetail.foregroundRooftops width height 0L <> SceneDetail.foregroundRooftops width height 240L)
+
+ [<TestMethod>]
+ member _.FloaterAtlasContract () =
+ assertInt 96 FloaterArt.atlasWidth "floater atlas width"
+ assertInt 16 FloaterArt.atlasHeight "floater atlas height"
+ assertInt 24 FloaterArt.cellWidth "floater cell width"
+ assertInt 16 FloaterArt.cellHeight "floater cell height"
+ for kind in [ FloaterArt.duckIndex; FloaterArt.leafIndex ] do
+ for frame in 0 .. FloaterArt.frameCount - 1 do
+ let rect = FloaterArt.sourceRectangle kind frame
+ Assert.IsTrue(rect.X >= 0 && rect.X + rect.Width <= FloaterArt.atlasWidth, "source rect inside atlas")
+ assertInt FloaterArt.cellHeight rect.Height "source cell height"
+ // four distinct cells, duck and leaf never share a frame cell.
+ let cells =
+ [ for kind in [ FloaterArt.duckIndex; FloaterArt.leafIndex ] do
+ for frame in 0 .. FloaterArt.frameCount - 1 -> FloaterArt.sourceRectangle kind frame ]
+ Assert.IsTrue(cells |> List.distinct |> List.length = 4, "four distinct floater cells")