diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-21 13:11:52 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-21 13:11:52 +0800 |
| commit | 76a6850bb88546fc21f5f78a53fbda04e77c8b52 (patch) | |
| tree | 45e492b623ef820d452201421b85a0b642d349bd | |
| parent | 000a0b00384806ce85d54dfcd2646cb340a26f01 (diff) | |
| download | living-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.fs | 35 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/Game.fs | 15 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/M6Presentation.fs | 45 |
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 |
