summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop/M6Presentation.fs
blob: 752034a04d49c3218d6b44c2f46f2b9143c68ba2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
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

    /// 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 一致(纯函数)。
    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 "夜晚"