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 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 / 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 duskStartHour duskEndHour) let private lerp (a: int) (b: int) (blend: float32) : int = a + int (float32 (b - a) * 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)) /// 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 LanternGlow = blend 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 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 一致(纯函数)。 let timePeriodLabel (tick: int64) : string = let hour = hourOf tick if hour < 5 then "夜晚" elif hour < 8 then "清晨" elif hour < 17 then "白天" elif hour < 20 then "黄昏" else "夜晚"