diff options
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/PrototypeTests.fs | 44 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/M6Presentation.fs | 69 |
2 files changed, 80 insertions, 33 deletions
diff --git a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs index b8f23a0..5a153b1 100644 --- a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs +++ b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs @@ -858,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 diff --git a/src/LivingVillage.Desktop/M6Presentation.fs b/src/LivingVillage.Desktop/M6Presentation.fs index 65300f0..11399c0 100644 --- a/src/LivingVillage.Desktop/M6Presentation.fs +++ b/src/LivingVillage.Desktop/M6Presentation.fs @@ -13,7 +13,8 @@ type M6RenderProfile = Background: Color WorldTint: Color Blend: float32 - LanternGlow: float32 } + LanternGlow: float32 + SkyWarmth: float32 } module M6Presentation = @@ -22,36 +23,68 @@ module M6Presentation = | Day -> "白天" | Night -> "夜晚" - /// Continuous light blend: 0 = full day, 1 = full night. Ramps are deterministic - /// pure functions of the simulation tick (dawn 4:00-6:00, dusk 20:00-22:00), so the - /// boundaries agree with Sim.isNightTick (night >= 22:00 or < 6:00). + // Transition windows in float hours. Dawn lingers past 6:00 and dusk begins before + // 20:00 so the blue hour is longer and the sky warms before it darkens; the anchors + // still agree with Sim.isNightTick (night >= 22:00 or < 6:00) once fully dark. + let dawnStartHour = 4.0f + let dawnEndHour = 6.5f + let duskStartHour = 19.5f + let duskEndHour = 22.0f + + /// Continuous light blend: 0 = full day, 1 = full night. Deterministic pure function + /// of the simulation tick, parameterised by the dawn/dusk windows above. let lightingBlend (tick: int64) : float32 = let hourTicks = Sim.ticksPerDay / 24L - let hour = float32 (tick / hourTicks) + let hour = float32 tick / float32 hourTicks let clamp01 v = if v < 0.0f then 0.0f elif v > 1.0f then 1.0f else v - if hour >= 6.0f && hour < 20.0f then 0.0f - elif hour >= 22.0f then 1.0f - elif hour < 4.0f then 1.0f - elif hour < 6.0f then clamp01 (1.0f - (hour - 4.0f) / 2.0f) - else clamp01 ((hour - 20.0f) / 2.0f) + if hour >= dawnEndHour && hour < duskStartHour then 0.0f + elif hour >= duskEndHour || hour < dawnStartHour then 1.0f + elif hour < dawnEndHour then clamp01 (1.0f - (hour - dawnStartHour) / (dawnEndHour - dawnStartHour)) + else clamp01 ((hour - duskStartHour) / (duskEndHour - duskStartHour)) + + /// Golden-hour warmth (0..1): a triangular bump peaking mid-dawn and mid-dusk, zero at + /// noon and midnight. Drives the warm sky/world tint so P17 transition frames read golden. + let transitionWarmth (tick: int64) : float32 = + let hour = float32 tick / float32 (Sim.ticksPerDay / 24L) + let bump (startHour: float32) (endHour: float32) : float32 = + if hour <= startHour || hour >= endHour then + 0.0f + else + let mid = (startHour + endHour) * 0.5f + let half = (endHour - startHour) * 0.5f + 1.0f - abs (hour - mid) / half + max (bump dawnStartHour dawnEndHour) (bump duskStartHour duskEndHour) let private lerp (a: int) (b: int) (blend: float32) : int = a + int (float32 (b - a) * blend) - let backgroundAtBlend (blend: float32) : Color = - Color(lerp 100 18 blend, lerp 160 25 blend, lerp 210 55 blend) + /// Blended sky/world colour plus the golden-hour warmth shift (warm channels up, blue down). + let backgroundAtWarmth (blend: float32) (warmth: float32) : Color = + Color( + lerp 100 18 blend + int (52.0f * warmth), + lerp 160 25 blend + int (14.0f * warmth), + lerp 210 55 blend - int (34.0f * warmth)) + + let worldTintAtWarmth (blend: float32) (warmth: float32) : Color = + Color( + lerp 255 118 blend + int (10.0f * warmth), + lerp 248 136 blend - int (6.0f * warmth), + lerp 224 184 blend - int (26.0f * warmth)) - let worldTintAtBlend (blend: float32) : Color = - Color(lerp 255 118 blend, lerp 248 136 blend, lerp 224 184 blend) + /// Warmth-free base colours (kept for callers that only have a blend value). + let backgroundAtBlend (blend: float32) : Color = backgroundAtWarmth blend 0.0f + let worldTintAtBlend (blend: float32) : Color = worldTintAtWarmth blend 0.0f - /// Pure day-phase lighting: deterministic tint/background plus lantern warm-glow boost. + /// Pure day-phase lighting: deterministic tint/background/warmth plus lantern glow. let lightingAtTick (tick: int64) : M6RenderProfile = let blend = lightingBlend tick + let warmth = transitionWarmth tick { Mode = if blend < 0.5f then Day else Night - Background = backgroundAtBlend blend + Background = backgroundAtWarmth blend warmth Blend = blend - WorldTint = worldTintAtBlend blend - LanternGlow = blend } + WorldTint = worldTintAtWarmth blend warmth + LanternGlow = blend + SkyWarmth = warmth } let profileAtTick (tick: int64) : M6RenderProfile = lightingAtTick tick |
