summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-21 13:11:52 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-21 13:11:52 +0800
commit76a6850bb88546fc21f5f78a53fbda04e77c8b52 (patch)
tree45e492b623ef820d452201421b85a0b642d349bd
parent000a0b00384806ce85d54dfcd2646cb340a26f01 (diff)
downloadliving-village-76a6850bb88546fc21f5f78a53fbda04e77c8b52.tar.gz
feat(desktop): 确定性昼夜光照过渡与夜灯暖光
新增 lightingBlend 纯函数(黎明明 4:00-6:00、暮色 20:00-22:00 线性过渡,边界与 isNightTick 对齐),天空底色/世界 tint 按混合插值渲染,夜晚灯笼暖光晕强度=LanternGlow(纯叠加矩形、无每帧分配风险);回归测试含锚点/单调斜坡/确定性/光晕开关。
-rw-r--r--src/LivingVillage.Desktop.Tests/PrototypeTests.fs35
-rw-r--r--src/LivingVillage.Desktop/Game.fs15
-rw-r--r--src/LivingVillage.Desktop/M6Presentation.fs45
3 files changed, 85 insertions, 10 deletions
diff --git a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
index 7e4ce5f..d77abd6 100644
--- a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
+++ b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
@@ -670,6 +670,41 @@ type PrototypeTests () =
Assert.AreEqual<string option>(None, M5Interaction.refreshPrompt world needsView |> fun value -> value.Prompt)
[<TestMethod>]
+ member _.DayPhaseLightingIsPureDeterministicAndTransitionsSmoothly () =
+ let ticksPerHour = Sim.ticksPerDay / 24L
+ let atHour hour = M6Presentation.lightingAtTick (int64 hour * ticksPerHour)
+
+ // Anchors: full day at noon, full night at midnight, no drift.
+ Assert.AreEqual<float32>(0.0f, (atHour 12).Blend)
+ 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)
+
+ // Determinism without randomness.
+ let once = atHour 13
+ let twice = atHour 13
+ Assert.AreEqual<M6RenderProfile>(once, twice)
+
+ // Lantern glow: warm halo only rises with night blend.
+ Assert.AreEqual<float32>(0.0f, (atHour 13).LanternGlow)
+ Assert.AreEqual<float32>(1.0f, (atHour 0).LanternGlow)
+
+ [<TestMethod>]
member _.WorldPanelDoesNotRenderA永久KeyList () =
let world = Sim.initialWorldN 42UL 0
let lines = M5Interaction.panelLines world M5Interaction.initial
diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs
index 1d50f3d..9464c8b 100644
--- a/src/LivingVillage.Desktop/Game.fs
+++ b/src/LivingVillage.Desktop/Game.fs
@@ -676,6 +676,21 @@ type LivingVillageGame() as this =
world.Tick
profile.WorldTint
+ // Warm lantern glows strengthen as night falls (pure function of tick phase).
+ if profile.LanternGlow > 0.3f then
+ let glow = profile.LanternGlow
+ let haloAlpha = int (90.0f * glow)
+ for prop in renderPlan.Props do
+ match prop.Kind with
+ | RiverLantern ->
+ let position = VillageArt.worldTile renderPlan prop.Position
+ let screenX = position.X * Sim.tilePixels - int camera.X + Sim.tilePixels / 2
+ let screenY = position.Y * Sim.tilePixels - int camera.Y - Sim.tilePixels / 2
+ spriteBatch.Draw(pixel, Rectangle(screenX - 22, screenY - 22, 60, 60), Color(252, 216, 148, int (float32 haloAlpha * 0.35f)))
+ spriteBatch.Draw(pixel, Rectangle(screenX - 14, screenY - 14, 44, 44), Color(252, 216, 148, haloAlpha))
+ spriteBatch.Draw(pixel, Rectangle(screenX - 8, screenY - 8, 32, 32), Color(255, 236, 180, haloAlpha))
+ | _ -> ()
+
let avatarSpec = VillageArt.avatarSpriteSpec avatarFacing world.Tick
VillageArt.drawCharacter spriteBatch artTextures camera world.Avatar.Pos avatarSpec profile.WorldTint
diff --git a/src/LivingVillage.Desktop/M6Presentation.fs b/src/LivingVillage.Desktop/M6Presentation.fs
index 8e13183..ea8fd2f 100644
--- a/src/LivingVillage.Desktop/M6Presentation.fs
+++ b/src/LivingVillage.Desktop/M6Presentation.fs
@@ -11,7 +11,9 @@ type M6DayNight =
type M6RenderProfile =
{ Mode: M6DayNight
Background: Color
- WorldTint: Color }
+ WorldTint: Color
+ Blend: float32
+ LanternGlow: float32 }
module M6Presentation =
@@ -20,15 +22,38 @@ module M6Presentation =
| Day -> "白天"
| Night -> "夜晚"
- let profileAtTick (tick: int64) : M6RenderProfile =
- if Sim.isNightTick tick then
- { Mode = Night
- Background = Color(18, 25, 55)
- WorldTint = Color(118, 136, 184) }
- else
- { Mode = Day
- Background = Color(100, 160, 210)
- WorldTint = Color(255, 248, 224) }
+ /// 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).
+ let lightingBlend (tick: int64) : float32 =
+ let hourTicks = Sim.ticksPerDay / 24L
+ let hour = float32 (tick / 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)
+
+ 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)
+
+ let worldTintAtBlend (blend: float32) : Color =
+ Color(lerp 255 118 blend, lerp 248 136 blend, lerp 224 184 blend)
+
+ /// Pure day-phase lighting: deterministic tint/background plus lantern warm-glow boost.
+ let lightingAtTick (tick: int64) : M6RenderProfile =
+ let blend = lightingBlend tick
+ { Mode = if blend < 0.5f then Day else Night
+ Background = backgroundAtBlend blend
+ Blend = blend
+ WorldTint = worldTintAtBlend blend
+ LanternGlow = blend }
+
+ let profileAtTick (tick: int64) : M6RenderProfile = lightingAtTick tick
let clockLabel (control: SimulationControl) : string =
if control.Paused then