summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop.Tests
diff options
context:
space:
mode:
Diffstat (limited to 'src/LivingVillage.Desktop.Tests')
-rw-r--r--src/LivingVillage.Desktop.Tests/PrototypeTests.fs127
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 个字符、字符唯一,