diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-22 01:30:56 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-22 01:30:56 +0800 |
| commit | 6dfc6652048810e42058662bb11febde190f8a52 (patch) | |
| tree | 14d491da96ab04573d0e31872d49048c70abe763 /src/LivingVillage.Desktop.Tests | |
| parent | caac9fdad0a9d25319147758add43f091d42e7c6 (diff) | |
| parent | a67e9262caca67f1078f77ed2f274a46e284c6c8 (diff) | |
| download | living-village-6dfc6652048810e42058662bb11febde190f8a52.tar.gz | |
merge: P17 起时钩子修复与黄昏精修(独立复验全绿 Desktop 108/Kernel 89)
Diffstat (limited to 'src/LivingVillage.Desktop.Tests')
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/PrototypeTests.fs | 127 |
1 files changed, 105 insertions, 22 deletions
diff --git a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs index 88f6016..b250824 100644 --- a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs +++ b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs @@ -335,7 +335,7 @@ type PrototypeTests () = SouthFacing WestFacing EastFacing ] - let sprites = directions |> List.map (fun direction -> npcSpriteSpec (NpcId 7) direction true 4L) + let sprites = directions |> List.map (fun direction -> npcSpriteSpec (NpcId 0) direction true 4L) Assert.AreEqual<NpcVisualVariant>(sameNpc, repeatedNpc) Assert.AreNotEqual<NpcVisualVariant>(sameNpc, otherNpc) @@ -473,8 +473,9 @@ type PrototypeTests () = Assert.IsTrue(footprint.Contains({ X = door.X + dx; Y = door.Y - 2 } : TilePosition), sprintf "missing ridge tile dx=%d" dx) Assert.IsTrue(footprint.Contains({ X = door.X + dx; Y = door.Y - 1 } : TilePosition), sprintf "missing eave tile dx=%d" dx) Assert.IsTrue(footprint.Contains({ X = door.X + dx; Y = door.Y } : TilePosition), sprintf "missing wall tile dx=%d" dx) - // 12 house + 3 bridge + 3 bamboo + 2 garden + 2x2 willow + 2 shrub + 2 flowers + 2 reeds + 1 bench + 2 lanterns. - Assert.AreEqual<int>(33, footprint.Count) + // 12 house + 3 bridge + 3 bamboo + 2 garden + 2x2 willow + 2 shrub + 2 flowers + 2 reeds + // + 1 bench + 2 lanterns + 2 stone lanterns + 1 water vat + 1 market stall (P17). + Assert.AreEqual<int>(37, footprint.Count) [<TestMethod>] member _.VegetationAndRiversidePropsDecorateTheScene () = @@ -489,13 +490,20 @@ type PrototypeTests () = Assert.AreEqual<int>(2, count VillageArt.ReedCluster) Assert.AreEqual<int>(1, count VillageArt.StoneBench) Assert.AreEqual<int>(2, count VillageArt.RiverLantern) + Assert.AreEqual<int>(2, count VillageArt.StoneLantern) + Assert.AreEqual<int>(1, count VillageArt.WaterVat) + Assert.AreEqual<int>(1, count VillageArt.MarketStall) - // Reeds must root in water; lanterns stand on the bank path. + // Reeds must root in water; lanterns and P17 street details stand on the paved bank path. for prop in plan.Props do let tile = VillageArt.worldTile plan prop.Position match prop.Kind with | VillageArt.ReedCluster -> Assert.IsTrue(grounds.Contains(prop.Position), "reeds must sit on water tiles") - | VillageArt.RiverLantern -> Assert.IsTrue(plan.Grounds |> List.contains (prop.Position, VillageArt.StonePath), "lanterns must stand on the bank path") + | VillageArt.RiverLantern + | VillageArt.StoneLantern + | VillageArt.WaterVat + | VillageArt.MarketStall -> + Assert.IsTrue(plan.Grounds |> List.contains (prop.Position, VillageArt.StonePath), "street details must stand on the bank path") | _ -> () [<TestMethod>] @@ -526,6 +534,34 @@ type PrototypeTests () = Assert.AreEqual<CharacterFrame>(VillageArt.characterFrame true 12L, VillageArt.characterFrame true 12L) [<TestMethod>] + member _.NpcTradesAndWalkRhythmsAreDistinctAndDeterministic () = + // Three trades repeat by id and stay stable, with no randomness. + Assert.AreEqual<VillageArt.NpcTrade>(VillageArt.Farmer, VillageArt.npcTrade (NpcId 0)) + Assert.AreEqual<VillageArt.NpcTrade>(VillageArt.Peddler, VillageArt.npcTrade (NpcId 1)) + Assert.AreEqual<VillageArt.NpcTrade>(VillageArt.Scholar, VillageArt.npcTrade (NpcId 2)) + Assert.AreEqual<VillageArt.NpcTrade>(VillageArt.npcTrade (NpcId 9), VillageArt.npcTrade (NpcId 0)) + + // Distinct cadences: peddler shuffles fastest, scholar strolls slowest. + let farmer = VillageArt.rhythmFor VillageArt.Farmer + let peddler = VillageArt.rhythmFor VillageArt.Peddler + let scholar = VillageArt.rhythmFor VillageArt.Scholar + Assert.IsTrue(peddler.WalkWindow < farmer.WalkWindow && farmer.WalkWindow < scholar.WalkWindow) + Assert.IsTrue(peddler.IdleWindow < farmer.IdleWindow && farmer.IdleWindow < scholar.IdleWindow) + + // Same tick, three trade-specific frames (at 12 ticks: Farmer IV, Peddler II, Scholar I). + let frameAt trade tick = VillageArt.animationFrameFor (VillageArt.rhythmFor trade) tick + let frames = + [ VillageArt.Farmer; VillageArt.Peddler; VillageArt.Scholar ] + |> List.map (fun trade -> frameAt trade 12L) + Assert.AreEqual<AnimationFrame list>([ FrameFour; FrameTwo; FrameOne ], frames) + Assert.AreEqual<VillageArt.WalkRhythm>(VillageArt.rhythmFor VillageArt.Scholar, VillageArt.rhythmFor VillageArt.Scholar) + + // npcSpriteSpec now follows the NPC's own trade cadence. + Assert.AreEqual<CharacterFrame>(WalkFrame FrameFour, (VillageArt.npcSpriteSpec (NpcId 0) VillageArt.SouthFacing true 12L).Frame) + Assert.AreEqual<CharacterFrame>(WalkFrame FrameTwo, (VillageArt.npcSpriteSpec (NpcId 1) VillageArt.SouthFacing true 12L).Frame) + Assert.AreEqual<CharacterFrame>(WalkFrame FrameOne, (VillageArt.npcSpriteSpec (NpcId 2) VillageArt.SouthFacing true 12L).Frame) + + [<TestMethod>] member _.ChimneySmokeFramesAreDeterministicAndThreePhase () = Assert.AreEqual<int>(0, VillageArt.smokeFrameTick 0L) Assert.AreEqual<int>(0, VillageArt.smokeFrameTick 39L) @@ -650,8 +686,8 @@ type PrototypeTests () = member _.RuntimeMovementAndNpcTargetsProduceStableSpriteDirections () = let idle = VillageArt.facingAfterMovement { X = 4.0f; Y = 4.0f } { X = 4.0f; Y = 4.0f } VillageArt.EastFacing let moved = VillageArt.facingAfterMovement { X = 4.0f; Y = 4.0f } { X = 2.0f; Y = 4.0f } VillageArt.EastFacing - let targetSpec = VillageArt.npcSpriteSpecAtTarget (NpcId 7) { X = 10.0f; Y = 10.0f } { X = 10.0f; Y = 8.0f } 4L - let arrivedSpec = VillageArt.npcSpriteSpecAtTarget (NpcId 7) { X = 10.0f; Y = 10.0f } { X = 10.0f; Y = 10.0f } 4L + let targetSpec = VillageArt.npcSpriteSpecAtTarget (NpcId 0) { X = 10.0f; Y = 10.0f } { X = 10.0f; Y = 8.0f } 4L + let arrivedSpec = VillageArt.npcSpriteSpecAtTarget (NpcId 0) { X = 10.0f; Y = 10.0f } { X = 10.0f; Y = 10.0f } 4L Assert.AreEqual<VillageArt.FacingDirection>(VillageArt.EastFacing, idle) Assert.AreEqual<VillageArt.FacingDirection>(VillageArt.WestFacing, moved) @@ -822,21 +858,35 @@ type PrototypeTests () = Assert.AreEqual<float32>(1.0f, (atHour 0).Blend) Assert.AreEqual<float32>(1.0f, (atHour 2).Blend) - // Dawn ramp: 4:00 -> 6:00 goes night -> day, monotonic. - let dawnStart = (atHour 4).Blend - let dawnMid = (M6Presentation.lightingAtTick (int64 5 * ticksPerHour)).Blend - let dawnEnd = (atHour 6).Blend - Assert.AreEqual<float32>(1.0f, dawnStart) - Assert.AreEqual<float32>(0.5f, dawnMid) - Assert.AreEqual<float32>(0.0f, dawnEnd) - - // Dusk ramp 20:00 -> 22:00 goes day -> night monotonically. - let duskStart = (atHour 20).Blend - let duskMid = (M6Presentation.lightingAtTick (int64 21 * ticksPerHour)).Blend - let duskEnd = (atHour 22).Blend - Assert.AreEqual<float32>(0.0f, duskStart) - Assert.AreEqual<float32>(0.5f, duskMid) - Assert.AreEqual<float32>(1.0f, duskEnd) + // P17 window: dawn 4:00 -> 6:30 goes night -> day, monotonic (fractional hours). + let dawnMidTick = int64 5 * ticksPerHour + ticksPerHour / 4L // 5:15 + let dawnEndTick = int64 6 * ticksPerHour + ticksPerHour / 2L // 6:30 + Assert.AreEqual<float32>(1.0f, (atHour 4).Blend) + Assert.AreEqual<float32>(0.5f, (M6Presentation.lightingAtTick dawnMidTick).Blend) + Assert.AreEqual<float32>(0.0f, (M6Presentation.lightingAtTick dawnEndTick).Blend) + + // Dusk ramp 19:30 -> 22:00 goes day -> night monotonically. + let duskStartTick = int64 19 * ticksPerHour + ticksPerHour / 2L // 19:30 + let duskMidTick = int64 20 * ticksPerHour + ticksPerHour * 3L / 4L // 20:45 + Assert.AreEqual<float32>(0.0f, (M6Presentation.lightingAtTick duskStartTick).Blend) + Assert.AreEqual<float32>(0.5f, (M6Presentation.lightingAtTick duskMidTick).Blend) + Assert.AreEqual<float32>(1.0f, (atHour 22).Blend) + + // Blue hour: the transition lingers past 6:00 and starts before 20:00. + Assert.IsTrue((atHour 6).Blend > 0.0f && (atHour 6).Blend < 0.5f) + Assert.IsTrue((atHour 20).Blend > 0.0f && (atHour 20).Blend < 0.5f) + + // Golden-hour warmth peaks mid-dawn/mid-dusk and vanishes at noon/midnight. + Assert.AreEqual<float32>(0.0f, (atHour 12).SkyWarmth) + Assert.AreEqual<float32>(0.0f, (atHour 0).SkyWarmth) + Assert.AreEqual<float32>(1.0f, (M6Presentation.lightingAtTick dawnMidTick).SkyWarmth) + Assert.AreEqual<float32>(1.0f, (M6Presentation.lightingAtTick duskMidTick).SkyWarmth) + Assert.IsTrue((atHour 5).SkyWarmth > 0.5f) + Assert.IsTrue((atHour 21).SkyWarmth > 0.5f) + // The warm shift pushes the sky redder and less blue than the plain blend. + let warmDawn = atHour 5 + Assert.IsTrue(warmDawn.Background.R > (M6Presentation.backgroundAtBlend warmDawn.Blend).R) + Assert.IsTrue(warmDawn.Background.B < (M6Presentation.backgroundAtBlend warmDawn.Blend).B) // Determinism without randomness. let once = atHour 13 @@ -847,6 +897,24 @@ type PrototypeTests () = Assert.AreEqual<float32>(0.0f, (atHour 13).LanternGlow) Assert.AreEqual<float32>(1.0f, (atHour 0).LanternGlow) + // Evidence hook: fractional start hours map exactly onto sim ticks. + Assert.AreEqual<int64>(int64 20 * ticksPerHour + ticksPerHour * 3L / 4L, M6Presentation.startTickForHour 20.75) + Assert.AreEqual<int64>(6L * ticksPerHour, M6Presentation.startTickForHour 6.0) + + [<TestMethod>] + member _.StartTickResolutionHonoursEvidenceHourThenDaylightThenMidnight () = + let ticksPerHour = Sim.ticksPerDay / 24L + + // P17-FIX: the evidence hour must win on the sample/autoplay path too, otherwise + // StartNewGame re-created the world at 00:00 and dusk/day shots were actually night. + Assert.AreEqual<int64>(M6Presentation.startTickForHour 20.75, M6Presentation.resolveStartTick (Some 20.75) false) + Assert.AreEqual<int64>(int64 20 * ticksPerHour + ticksPerHour * 3L / 4L, M6Presentation.resolveStartTick (Some 20.75) false) + // Explicit hour overrides the daylight bootstrap. + Assert.AreEqual<int64>(12L * ticksPerHour, M6Presentation.resolveStartTick (Some 12.0) true) + // No override: daylight -> 6:00, otherwise midnight. + Assert.AreEqual<int64>(6L * ticksPerHour, M6Presentation.resolveStartTick None true) + Assert.AreEqual<int64>(0L, M6Presentation.resolveStartTick None false) + [<TestMethod>] member _.TitleEntranceProgressIsDeterministicAndCompleteAfterTwoSeconds () = // 120 frames at the fixed 60Hz step = 2s entrance, pure function of frame count. @@ -1019,6 +1087,21 @@ 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) + let worldPath = Path.Combine(AppContext.BaseDirectory, "Assets/jiangnan-world.png") + let header = File.ReadAllBytes worldPath + let be32 offset = + (int header.[offset] <<< 24) + ||| (int header.[offset + 1] <<< 16) + ||| (int header.[offset + 2] <<< 8) + ||| 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((atlasHeight = VillageArt.worldAtlasHeight), "world atlas height must stay 32px") + [<TestMethod>] member _.CjkGlyphManifestIsUniqueAndMatchesAtlasGrid () = // 防复发:图集用字增长时,清单必须非空、每行恰 1 个字符、字符唯一, |
