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 "夜晚"