namespace LivingVillage.Desktop open Microsoft.Xna.Framework open LivingVillage.Kernel open LivingVillage.Kernel.Sim type M6DayNight = | Day | Night type M6RenderProfile = { Mode: M6DayNight Background: Color WorldTint: Color Blend: float32 LanternGlow: float32 SkyWarmth: float32 } module M6Presentation = let dayNightLabel mode : string = match mode with | Day -> "白天" | Night -> "夜晚" // Transition windows in float hours. Dawn lingers past 6:00 so the blue hour is long. // Dusk keeps two independent ramps: the darkening ramp 20:00 -> 22:00 (still agrees with // Sim.isNightTick at 22:00), and a wider golden-hour warmth bump peaking at 20:30. Keeping // them separate is what makes the orange dusk visible: at 20:45 the scene is still only // ~0.37 dark (Day mode) while warmth is high. let dawnStartHour = 4.0f let dawnEndHour = 6.5f let duskStartHour = 20.0f let duskEndHour = 22.0f let duskWarmthStartHour = 19.25f let duskWarmthEndHour = 21.75f /// 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 / float32 hourTicks let clamp01 v = if v < 0.0f then 0.0f elif v > 1.0f then 1.0f else v 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 duskWarmthStartHour duskWarmthEndHour) /// Additive golden-hour veil alpha (0 at noon/midnight, peaks ~150 at dawn/dusk mid). /// Multiplicative WorldTint alone cannot warm green foliage, so the renderer layers this /// premultiplied orange wash over the finished world scene. let goldenVeilAlpha (tick: int64) : int = int (150.0f * transitionWarmth tick) /// Cool moonlight wash applied as a premultiplied veil over the finished night scene. /// Multiplicative night tint can only darken, so this additive cool layer is what makes deep /// night read moon-blue. It is zero whenever the sun lights the scene (blend 0) and is /// suppressed through the warm dawn/dusk windows (warmth), so it never competes with the /// golden veil. Purely desktop render state; the Kernel never sees it. let moonlightRgb : int * int * int = (120, 170, 235) let moonlightVeilAlpha (tick: int64) : int = let blend = lightingBlend tick let warmth = transitionWarmth tick int (72.0f * blend * (1.0f - warmth)) let private lerp (a: int) (b: int) (blend: float32) : int = a + int (float32 (b - a) * blend) /// Blended sky colour plus a strong golden-hour shift: at dusk this reads as a warm orange /// horizon (red up hard, blue down hard) rather than the old muted blue-grey twilight. let backgroundAtWarmth (blend: float32) (warmth: float32) : Color = Color( lerp 100 18 blend + int (120.0f * warmth), lerp 160 25 blend + int (20.0f * warmth), lerp 210 55 blend - int (90.0f * warmth)) /// World tint multiplies the whole scene. The day baseline is neutral white so noon keeps /// a true white balance (the old day base 255/248/224 clipped blue by ~12% and made daylight /// look yellow); night stays cool, and the golden shade only appears through `warmth`, which /// is a pure function that is zero at noon and outside the dawn/dusk windows. let worldTintAtWarmth (blend: float32) (warmth: float32) : Color = Color( lerp 255 118 blend + int (70.0f * warmth), lerp 255 136 blend - int (10.0f * warmth), lerp 255 184 blend - int (75.0f * warmth)) /// 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/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 = backgroundAtWarmth blend warmth Blend = blend WorldTint = worldTintAtWarmth blend warmth // Lanterns glow through the golden hour too, so dusk reads as warm lanterns at a // warm sunset rather than only a local night point-light. LanternGlow = max blend (warmth * 0.8f) SkyWarmth = warmth } let profileAtTick (tick: int64) : M6RenderProfile = lightingAtTick tick let clockLabel (control: SimulationControl) : string = if control.Paused then "暂停" else match control.Speed with | OneX -> "1倍速" | TwoX -> "2倍速" | FiveX -> "5倍速" /// 倍速标签(语义别名,HUD 用)。 let speedLabel (control: SimulationControl) : string = clockLabel control /// Deterministic start tick for a wall-clock hour (evidence harness): 20.75 -> 20:45. let startTickForHour (hour: float) : int64 = int64 (hour * float (Sim.ticksPerDay / 24L)) /// Start-tick decision shared by bootstrap and new-game: an explicit evidence hour wins, /// else daylight starts at 6:00, else midnight. Pure function, no side effects. let resolveStartTick (startHour: float option) (startAtDaylight: bool) : int64 = match startHour with | Some hour -> startTickForHour hour | None -> if startAtDaylight then 6L * Sim.ticksPerDay / 24L else 0L let private hourOf (tick: int64) : int = int ((tick / (Sim.ticksPerDay / 24L)) % 24L) /// 24 小时制的模拟时钟(HH:MM),由 tick 纯函数导出。 let clockTimeLabel (tick: int64) : string = let hourTicks = Sim.ticksPerDay / 24L let hour = hourOf tick let minute = int ((tick % hourTicks) * 60L / hourTicks) sprintf "%02d:%02d" hour minute /// 时段标签:清晨/白天/黄昏/夜晚,与实际 tick 一致(纯函数)。 /// 黄昏覆盖到 21:00,与 dusk 暖色窗口(峰值 20:30)对齐;22:00 起才是夜晚。 let timePeriodLabel (tick: int64) : string = let hour = hourOf tick if hour < 5 then "夜晚" elif hour < 8 then "清晨" elif hour < 17 then "白天" elif hour < 21 then "黄昏" else "夜晚" // ------------------------------------------------------------------ // P73:四时段光照档(清晨/正午/黄昏/夜晚)。 // 既有的 blend/warmth 是连续曲线;这里在其上叠加一层「时段档」—— // 每个时段一份乘性环境色调 + 加性覆盖色 + 灯光强度,保证 6/12/18/22 时 // 四张真实帧肉眼可辨,尤其填补 17:00–19:15 原本与正午无差的黄昏空档。 // 正午档为恒等(白 tint / alpha 0 / 灯光 0),因此不改变既有正午观感。 // ------------------------------------------------------------------ type M6Period = | Morning | Noon | Dusk | Night type M6PeriodLighting = { AmbientTint: Color Veil: Color VeilAlpha: int LanternGlow: float32 } /// 时段档参数表(纯数据)。调用方按 periodAtTick 取值。 let periodLighting (period: M6Period) : M6PeriodLighting = match period with | Morning -> { AmbientTint = Color(238, 246, 255) Veil = Color(255, 222, 160) VeilAlpha = 20 LanternGlow = 0.0f } | Noon -> { AmbientTint = Color(255, 255, 255) Veil = Color(255, 255, 255) VeilAlpha = 0 LanternGlow = 0.0f } | Dusk -> { AmbientTint = Color(255, 231, 200) Veil = Color(255, 140, 58) VeilAlpha = 52 LanternGlow = 0.6f } | Night -> { AmbientTint = Color(150, 176, 216) Veil = Color(70, 110, 190) VeilAlpha = 26 LanternGlow = 1.0f } /// 时段枚举名(证据日志用,纯函数;与 timePeriodLabel 边界一致)。 let lightingPeriodName = function | Morning -> "清晨" | Noon -> "正午" | Dusk -> "黄昏" | Night -> "夜晚" /// tick → 四时段档。边界与 timePeriodLabel 完全一致:<5 夜晚、<8 清晨、<17 正午、<21 黄昏、否则夜晚。 let periodAtTick (tick: int64) : M6Period = let hour = hourOf tick if hour < 5 then Night elif hour < 8 then Morning elif hour < 17 then Noon elif hour < 21 then Dusk else Night let periodLightingAtTick (tick: int64) : M6PeriodLighting = periodAtTick tick |> periodLighting /// 两乘性色调逐通道相乘(255 为恒等)。用于把时段环境色调并入世界 tint。 let multiplyTint (a: Color) (b: Color) : Color = Color(int a.R * int b.R / 255, int a.G * int b.G / 255, int a.B * int b.B / 255, 255) /// 实际灯光强度:连续曲线与时段档取较大者(黄昏档让灯笼从 18:00 起点亮)。 let effectiveLanternGlow (tick: int64) : float32 = max (lightingAtTick tick).LanternGlow (periodLightingAtTick tick).LanternGlow