summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop.Tests
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-21 21:54:01 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-21 21:54:01 +0800
commit87a00a036219f0f1b9c3eaccd7fcd9d782fe6011 (patch)
treef39ea3a04739705e84bce6558b1230da55495f98 /src/LivingVillage.Desktop.Tests
parentb5ffb35fc0824a297ff88297390310acb6c45335 (diff)
downloadliving-village-87a00a036219f0f1b9c3eaccd7fcd9d782fe6011.tar.gz
test(desktop): 预乘/光晕/水面/站位回归与夜间白块校验脚本
- premultiply 定点:核心 (255,226,168,34)->(34,30,22,34),(252,196,128,34)->(34,26,17,34), 通道恒 <= alpha、<250 无白 - lanternGlowLayers 径向/预乘/无白、水面 per-tile 三相、角色脚底对齐断言 - scripts/check-no-white-blocks.py:夜晚帧不得有 >=64x64、每通道>=250、alpha>=250 的块 (P13 旧帧 42/69 命中,P14 修复后 0 命中)
Diffstat (limited to 'src/LivingVillage.Desktop.Tests')
-rw-r--r--src/LivingVillage.Desktop.Tests/PrototypeTests.fs80
1 files changed, 80 insertions, 0 deletions
diff --git a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
index 46fb878..d5602e8 100644
--- a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
+++ b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
@@ -534,6 +534,86 @@ type PrototypeTests () =
Assert.AreEqual<int>(VillageArt.smokeFrameTick 99L, VillageArt.smokeFrameTick 99L)
[<TestMethod>]
+ member _.PremultiplyScalesEachChannelByItsOwnAlpha () =
+ // The warm lantern core: rgb(255,226,168) at alpha 34 must become 34/30/22 so the
+ // premultiplied SpriteBatch blend adds a dim warm value instead of full rgb.
+ let core = VillageArt.premultiply 255 226 168 34
+ Assert.AreEqual<byte>(34uy, core.R)
+ Assert.AreEqual<byte>(30uy, core.G)
+ Assert.AreEqual<byte>(22uy, core.B)
+ Assert.AreEqual<byte>(34uy, core.A)
+
+ // General fixed-point behaviour (round half away from zero).
+ let warm = VillageArt.premultiply 252 196 128 34
+ Assert.AreEqual<byte>(34uy, warm.R)
+ Assert.AreEqual<byte>(26uy, warm.G)
+ Assert.AreEqual<byte>(17uy, warm.B)
+ Assert.AreEqual<byte>(34uy, warm.A)
+
+ // Alpha 255 is the identity; alpha 0 zeroes the colour.
+ Assert.AreEqual<Color>(Color(252, 196, 128, 255), VillageArt.premultiply 252 196 128 255)
+ Assert.AreEqual<Color>(Color(0, 0, 0, 0), VillageArt.premultiply 252 196 128 0)
+
+ // A premultiplied channel can never exceed its own alpha, which is what makes an
+ // opaque white core impossible for any alpha < 250.
+ for alpha in [ 0; 1; 34; 70; 128; 200; 249 ] do
+ let c = VillageArt.premultiply 255 255 255 alpha
+ Assert.IsTrue(int c.R <= int c.A && int c.G <= int c.A && int c.B <= int c.A)
+ Assert.IsTrue(int (max c.R (max c.G c.B)) < 250)
+
+ [<TestMethod>]
+ member _.WaterRipplePhaseIsDistributedPerTile () =
+ // Neighbouring tiles must not share a phase in the same frame.
+ let a = VillageArt.waterFrameTickAt 128L 10 20
+ let b = VillageArt.waterFrameTickAt 128L 11 20
+ let c = VillageArt.waterFrameTickAt 128L 10 21
+ Assert.AreNotEqual<int>(a, b)
+ Assert.AreNotEqual<int>(a, c)
+
+ // Pure and in range 0..2 across the map and across ticks.
+ for tick in [ 0L; 31L; 32L; 40L; 64L; 127L; 200L ] do
+ for x in 0 .. 7 do
+ for y in 0 .. 7 do
+ let phase = VillageArt.waterFrameTickAt tick x y
+ Assert.IsTrue(phase >= 0 && phase <= 2)
+ Assert.AreEqual<int>(phase, VillageArt.waterFrameTickAt tick x y)
+
+ // A horizontal strip of river tiles shows at least two distinct phases at once.
+ let strip = [ 0 .. 11 ] |> List.map (fun x -> VillageArt.waterFrameTickAt 128L x 12)
+ Assert.IsTrue(Set.ofList strip |> Set.count >= 2)
+
+ [<TestMethod>]
+ member _.LanternGlowLayersArePremultipliedAndRadial () =
+ let centerX, centerY, radius, rings, peakAlpha = 100, 100, 44, 6, 140
+ let layers = VillageArt.lanternGlowLayers centerX centerY radius rings 255 214 150 peakAlpha
+ Assert.IsTrue(layers.Length > 0, "the glow must emit at least one layer")
+ Assert.AreEqual<(Rectangle * Color) list>(layers, VillageArt.lanternGlowLayers centerX centerY radius rings 255 214 150 peakAlpha)
+
+ for (rect, color) in layers do
+ Assert.AreEqual<int>(1, rect.Height)
+ Assert.IsTrue(rect.Width >= 1)
+ Assert.IsTrue(int color.R <= int color.A && int color.G <= int color.A && int color.B <= int color.A)
+ Assert.IsTrue(int (max color.R (max color.G color.B)) < 250, "no ring may be near-white")
+ Assert.IsTrue(rect.X >= centerX - radius && rect.X + rect.Width <= centerX + radius + 1)
+ Assert.IsTrue(rect.Y >= centerY - radius && rect.Y <= centerY + radius)
+
+ let covers px py =
+ layers |> List.exists (fun (rect, _) -> rect.X <= px && px < rect.X + rect.Width && rect.Y <= py && py < rect.Y + rect.Height)
+ Assert.IsTrue(covers centerX centerY, "the core must be covered")
+ Assert.IsFalse(covers (centerX + radius) (centerY + radius), "a square corner must stay dark: the glow is circular")
+ Assert.IsFalse(covers (centerX - radius - 5) centerY)
+
+ [<TestMethod>]
+ member _.CharacterFeetSitOnTheTileGroundLine () =
+ Assert.AreEqual<int>(16, VillageArt.characterFeetOffset)
+ let destination = VillageArt.characterDestination { X = 100.0f; Y = 200.0f } (Vector2(0.0f, 0.0f))
+ // Feet (sprite bottom) land half a tile below the tile centre; the sprite stays x-centred.
+ Assert.AreEqual<int>(216, destination.Y + destination.Height)
+ Assert.AreEqual<int>(100, destination.X + destination.Width / 2)
+ Assert.AreEqual<int>(32, destination.Width)
+ Assert.AreEqual<int>(48, destination.Height)
+
+ [<TestMethod>]
member _.RiverBandSpansTheSceneWithBankPathsAndCrossing () =
let plan = VillageArt.sampleRenderPlan ()
let footprintOf (value: VillageArt.JiangnanRenderPlan) = VillageArt.planFootprint value