summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop.Tests
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-22 10:14:31 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-22 10:14:31 +0800
commitb377e750a6a3f35c5d64a29f14ac3fbd87d4a752 (patch)
tree8baac5fca38fdcecda4a00eed7a58b1031ab9dca /src/LivingVillage.Desktop.Tests
parentd36e86ac637ccc477479f88bea511c49b0a6a67c (diff)
parent5f12e63f2d2203267c30666f1559e06ec637b91c (diff)
downloadliving-village-b377e750a6a3f35c5d64a29f14ac3fbd87d4a752.tar.gz
merge: P26 美术整改(民居第二变体/河岸装饰/夜月光/HUD 双景可读)— Hermes 复验通过
Diffstat (limited to 'src/LivingVillage.Desktop.Tests')
-rw-r--r--src/LivingVillage.Desktop.Tests/DesktopTests.fs24
-rw-r--r--src/LivingVillage.Desktop.Tests/MapGenTests.fs47
-rw-r--r--src/LivingVillage.Desktop.Tests/P16UiTests.fs21
-rw-r--r--src/LivingVillage.Desktop.Tests/PrototypeTests.fs48
4 files changed, 136 insertions, 4 deletions
diff --git a/src/LivingVillage.Desktop.Tests/DesktopTests.fs b/src/LivingVillage.Desktop.Tests/DesktopTests.fs
index b78f4e0..7e4a849 100644
--- a/src/LivingVillage.Desktop.Tests/DesktopTests.fs
+++ b/src/LivingVillage.Desktop.Tests/DesktopTests.fs
@@ -160,6 +160,30 @@ type DesktopTests () =
Assert.IsTrue(noon.Background.B > noon.Background.R)
[<TestMethod>]
+ member _.MoonlightVeilIsCoolAndOnlyAtNight () =
+ let noonTick = 12L * Sim.ticksPerDay / 24L
+ let midnightTick = 0L
+ let deepNightTick = 23L * Sim.ticksPerDay / 24L
+ Assert.IsTrue(M6Presentation.moonlightVeilAlpha noonTick = 0, "daytime must have no moonlight")
+
+ let moon = M6Presentation.moonlightVeilAlpha midnightTick
+ Assert.IsTrue(moon > 0, "deep night must get a cool moonlight wash")
+ Assert.AreEqual<int>(moon, M6Presentation.moonlightVeilAlpha deepNightTick)
+ Assert.IsTrue(moon <= 72, "moonlight veil stays a low alpha so the scene is not washed out")
+
+ let r, g, b = M6Presentation.moonlightRgb
+ Assert.IsTrue(b > g && g > r, "moonlight must be cool: blue-dominant channels")
+
+ // Dusk is owned by the warm golden veil, so moonlight is suppressed there.
+ let duskTick = M6Presentation.startTickForHour 20.75
+ Assert.IsTrue(M6Presentation.moonlightVeilAlpha duskTick < moon, "dusk is warmed, not moonlit")
+
+ // Premultiplied premultiply of this colour never saturates to white.
+ let veiled = VillageArt.premultiply r g b moon
+ Assert.IsTrue(int veiled.B <= int veiled.A)
+ Assert.IsTrue(int (max veiled.R (max veiled.G veiled.B)) < 250)
+
+ [<TestMethod>]
member _.DaylightAutoplayBootstrapStartsAtDawnAndPreservesDefaultStart () =
let dawnTick = 6L * Sim.ticksPerDay / 24L
let daylightWorld = WorldBootstrap.initialWorldForDaylight true 42UL Sim.npcCount
diff --git a/src/LivingVillage.Desktop.Tests/MapGenTests.fs b/src/LivingVillage.Desktop.Tests/MapGenTests.fs
index f426a8a..6486dd7 100644
--- a/src/LivingVillage.Desktop.Tests/MapGenTests.fs
+++ b/src/LivingVillage.Desktop.Tests/MapGenTests.fs
@@ -298,6 +298,53 @@ type MapGenTests () =
Assert.AreEqual<int64>(6042052L, checksum (MapGen.generateWithSize 64 48 (uint64 4242)))
Assert.AreEqual<int64>(1284656962L, checksum (MapGen.generateWithSize 256 192 (uint64 4242)))
+ // ---- P26:河岸装饰(护岸石垒/芦苇/垂柳)仅大图、落在岸格且不挡通行 ----
+
+ [<TestMethod>]
+ member _.RiverDecorationsOnlyEnabledOnLargeMaps () =
+ Assert.IsFalse((MapGen.paramsForSize 64 48 (uint64 1)).RiverDecorations)
+ Assert.IsFalse((MapGen.paramsForSize 256 192 (uint64 1)).RiverDecorations)
+ Assert.IsTrue((MapGen.paramsForSize 512 384 (uint64 1)).RiverDecorations)
+ Assert.AreEqual<int>(0, (MapGen.generateWithSize 64 48 (uint64 4242)).Decorations.Length)
+ Assert.AreEqual<int>(0, (MapGen.generateWithSize 256 192 (uint64 4242)).Decorations.Length)
+
+ [<TestMethod>]
+ member _.RiverbankDecorationsSitOnBanksAndNeverBlockPathsOrBridges () =
+ let map = MapGen.generateWithSize 512 384 (uint64 4242)
+ Assert.IsTrue(map.Decorations.Length > 0, "512x384 must place riverbank decorations")
+ let pathSet = System.Collections.Generic.HashSet<int>(map.Paths |> List.map (fun (x, y) -> y * map.Width + x))
+ let bridgeSet = System.Collections.Generic.HashSet<int>(map.Bridges |> List.map (fun (x, y) -> y * map.Width + x))
+ let water x y = map.Tiles.[y * map.Width + x] = int MapGen.GroundTile.Water
+ let inCore x y = x >= map.Core.MinX && x <= map.Core.MaxX && y >= map.Core.MinY && y <= map.Core.MaxY
+ for decoration in map.Decorations do
+ let footprint =
+ match decoration.Kind with
+ | MapGen.Willow -> [ decoration.Y; decoration.Y + 1 ]
+ | _ -> [ decoration.Y ]
+ for y in footprint do
+ let i = y * map.Width + decoration.X
+ Assert.AreNotEqual<int>(int MapGen.GroundTile.Water, map.Tiles.[i], "decoration must sit on land")
+ Assert.IsFalse(pathSet.Contains i, "decoration must not block a path tile")
+ Assert.IsFalse(bridgeSet.Contains i, "decoration must not block a bridge tile")
+ Assert.IsFalse(inCore decoration.X y, "decoration must stay outside the core")
+ // Bank adjacency: at least one footprint tile must touch the river orthogonally.
+ let touchesWater =
+ footprint
+ |> List.exists (fun y ->
+ [ (decoration.X + 1, y); (decoration.X - 1, y); (decoration.X, y + 1); (decoration.X, y - 1) ]
+ |> List.exists (fun (nx, ny) ->
+ nx >= 0 && nx < map.Width && ny >= 0 && ny < map.Height && water nx ny))
+ Assert.IsTrue(touchesWater, "decoration must line the riverbank, not float inland")
+
+ [<TestMethod>]
+ member _.LargeMapRiverDecorationsAreDeterministic () =
+ let a = MapGen.generateWithSize 512 384 (uint64 4242)
+ let b = MapGen.generateWithSize 512 384 (uint64 4242)
+ Assert.IsTrue(a.Decorations = b.Decorations)
+ Assert.AreEqual<string>(MapGen.serialize a, MapGen.serialize b)
+ let c = MapGen.generateWithSize 512 384 (uint64 7)
+ Assert.AreNotEqual<string>(MapGen.serialize a, MapGen.serialize c)
+
[<TestMethod>]
member _.VisibleRangeClampsAtLegacyMapEdges () =
let x0, y0, x1, y1 = MapGen.visibleTileRange 64 48 0 0 2048 1536
diff --git a/src/LivingVillage.Desktop.Tests/P16UiTests.fs b/src/LivingVillage.Desktop.Tests/P16UiTests.fs
index 2e474ac..b68bbd3 100644
--- a/src/LivingVillage.Desktop.Tests/P16UiTests.fs
+++ b/src/LivingVillage.Desktop.Tests/P16UiTests.fs
@@ -128,6 +128,27 @@ type P16HudTests () =
Assert.IsTrue(ChineseText.requiredUiLabels |> List.contains "体力")
Assert.IsTrue(ChineseText.requiredUiLabels |> List.contains "钱币")
+ [<TestMethod>]
+ member _.HudPersistentSegmentsGetBackdropAndOutlineInBothPeriods () =
+ // P26:白天/夜景两套调色板都要有半透明底色 + 不透明深色描边,保证常驻元素可读。
+ for mode in [ Day; Night ] do
+ let palette = HudLayout.palette mode
+ Assert.IsTrue(palette.SegmentBackdrop.A > 0uy, "segment backdrop must be translucent, not opaque")
+ Assert.IsTrue(palette.SegmentBackdrop.A < 255uy, "segment backdrop must keep the scene visible")
+ Assert.AreEqual<byte>(255uy, palette.Outline.A)
+ Assert.IsTrue(palette.Outline.R < 40uy && palette.Outline.G < 40uy && palette.Outline.B < 40uy)
+ let outlineSum = int palette.Outline.R + int palette.Outline.G + int palette.Outline.B
+ let valueSum = int palette.Value.R + int palette.Value.G + int palette.Value.B
+ Assert.IsTrue(outlineSum < valueSum, "outline must contrast with the bright value text")
+ Assert.AreEqual<int>(4, HudLayout.textOutlineOffsets.Length)
+ for (dx, dy) in HudLayout.textOutlineOffsets do
+ Assert.IsTrue(dx <> 0 || dy <> 0, "outline offset must be non-zero")
+ let placed = HudLayout.layout (HudLayout.segments world SimulationControl.initial)
+ for (x, width, _) in placed do
+ let rect = HudLayout.segmentBackdrop x width
+ Assert.IsTrue(rect.X <= x && rect.X + rect.Width >= x + width, "backdrop must cover the segment text")
+ Assert.IsTrue(rect.Y >= 0 && rect.Y + rect.Height <= HudLayout.barHeight, "backdrop must stay inside the bar")
+
// ---- P16-FIX 菜单截图帧表 ----
[<TestClass>]
diff --git a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
index e58139c..bc3425a 100644
--- a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
+++ b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
@@ -641,6 +641,28 @@ type PrototypeTests () =
Assert.IsFalse(covers (centerX - radius - 5) centerY)
[<TestMethod>]
+ member _.LanternGlowIsRadialWithoutSquareCorners () =
+ // P26 复核:光晕为同心圆叠加,逐像素累积 alpha;边缘/四角不得出现方斑。
+ let centerX, centerY, radius, rings, peakAlpha = 80, 80, 40, 6, 140
+ let layers = VillageArt.lanternGlowLayers centerX centerY radius rings 255 214 150 peakAlpha
+ let size = radius * 2 + 3
+ let origin = centerX - radius - 1
+ let alpha = Array2D.zeroCreate<int> size size
+ for (rect, color) in layers do
+ for y in rect.Y .. rect.Y + rect.Height - 1 do
+ for x in rect.X .. rect.X + rect.Width - 1 do
+ alpha.[y - origin, x - origin] <- alpha.[y - origin, x - origin] + int color.A
+ let center = alpha.[radius + 1, radius + 1]
+ Assert.IsTrue(center > 0, "the core must be lit")
+ let corner = [ (0, 0); (size - 1, 0); (0, size - 1); (size - 1, size - 1) ]
+ for (cx, cy) in corner do
+ Assert.IsTrue(alpha.[cy, cx] = 0, "glow corner must stay empty: no square patch")
+ // Radial falloff along the centre row: alpha never rises as distance from the core grows.
+ let row = [ for dx in 0 .. radius -> alpha.[radius + 1, radius + 1 + dx] ]
+ Assert.IsTrue(row |> List.pairwise |> List.forall (fun (a, b) -> a >= b), "glow must fade radially, not form a flat block")
+ Assert.IsTrue(row.[0] = center && row.[radius] < center, "core is brightest, rim is dimmer")
+
+ [<TestMethod>]
member _.CharacterFeetSitOnTheTileGroundLine () =
Assert.AreEqual<int>(16, VillageArt.characterFeetOffset)
let destination = VillageArt.characterDestination { X = 100.0f; Y = 200.0f } (Vector2(0.0f, 0.0f))
@@ -1104,6 +1126,23 @@ type PrototypeTests () =
// 953775FA 硬对齐由 --performance-baseline 于 finalWorld 支线证据。
[<TestMethod>]
+ member _.HouseFacadeVariantsAlternateAlongTheVillage () =
+ // P26:512 大图上两种民居立面都要出现,且按位置确定性交替(纯函数)。
+ let map = MapGen.generateWithSize 512 384 (uint64 4242)
+ let variants = map.Buildings |> List.map VillageArt.houseFacadeVariant
+ Assert.IsTrue(Set.ofList variants |> Set.contains 0, "variant A must appear")
+ Assert.IsTrue(Set.ofList variants |> Set.contains 1, "variant B must appear")
+ let repeated = MapGen.generateWithSize 512 384 (uint64 4242)
+ Assert.AreEqual<(int list)>(variants, repeated.Buildings |> List.map VillageArt.houseFacadeVariant)
+ // Neighbouring buildings along the same river bank must flip variant, i.e. not one solid run.
+ let flipped =
+ variants
+ |> List.pairwise
+ |> List.filter (fun (a, b) -> a <> b)
+ |> List.length
+ Assert.IsTrue(flipped > 0, "facades must alternate, not stay one variant")
+
+ [<TestMethod>]
member _.JiangnanArtAssetsArePackagedAsRealPngFiles () =
let requiredAssets =
[ "Assets/jiangnan-world.png"
@@ -1116,9 +1155,10 @@ type PrototypeTests () =
Assert.AreEqual<string list>([], missing, sprintf "Missing Jiangnan PNG assets: %s" (String.concat ", " missing))
- // The baked world atlas must match the 28-slot layout the renderer indexes into
- // (P17 added 石灯笼/水缸/摊贩摊位 as slots 25..27).
- Assert.AreEqual<int>(28, VillageArt.worldTileCount)
+ // The baked world atlas must match the 32-slot layout the renderer indexes into
+ // (P17 added 石灯笼/水缸/摊贩摊位 as slots 25..27; P26 added the variant-B house
+ // parts 屋檐挑出/木格栅窗/院门 as slots 28..30 and 护岸石垒 as slot 31).
+ Assert.AreEqual<int>(32, VillageArt.worldTileCount)
let worldPath = Path.Combine(AppContext.BaseDirectory, "Assets/jiangnan-world.png")
let header = File.ReadAllBytes worldPath
let be32 offset =
@@ -1128,7 +1168,7 @@ type PrototypeTests () =
||| int header.[offset + 3]
let atlasWidth : int = be32 16
let atlasHeight : int = be32 20
- Assert.IsTrue((atlasWidth = VillageArt.worldAtlasWidth), "world atlas width must match 28 x 32px slots")
+ Assert.IsTrue((atlasWidth = VillageArt.worldAtlasWidth), "world atlas width must match 32 x 32px slots")
Assert.IsTrue((atlasHeight = VillageArt.worldAtlasHeight), "world atlas height must stay 32px")
[<TestMethod>]