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