summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop/MenuAmbience.fs
blob: 6d258c57265c2bb0cd12e93efcfed9325050b5ef (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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
namespace LivingVillage.Desktop

/// P46 菜单/启动环境动效(纯函数层):
///   ① 水面逐格涟漪 —— 复用 VillageArt.waterFrameTickAt 的相位索引,再叠加 32 tick 内的
///      分数相位,使相邻水格相位错开、且 32 tick 边界之间也连续波动;
///   ② 灯笼呼吸光晕 —— alpha 在 [40,96] 间随帧数正弦呼吸,供 VillageArt.lanternGlowLayers 复用;
///   ③ 启动画面屋脊上方双层视差云影 —— 纯水平位移、有界、随帧缓慢漂移。
/// 全部为帧计数纯函数:同参数逐字节可复现,无时钟/随机源。
module MenuAmbience =

    open System

    // ---------------- ① 水面逐格涟漪 ----------------

    /// 涟漪幅度,沿用 P42 的可见性预算(>=0.25 才能在 1280x720 下读得出来)。
    let waterRippleAmplitude = 0.32f

    let private twoPiOverThree = 2.0f * float32 Math.PI / 3.0f

    /// 逐格涟漪相位(弧度)。基相位直接调用 VillageArt.waterFrameTickAt(tick/32 + tileX +
    /// 2*tileY 对 3 取模),再加 tick%32/32 的分数相位:在 32 tick 的整数边界上退化为
    /// waterFrameTickAt 的 3 帧相位,边界之间则连续推进,所以既有逐列差异又有逐帧运动。
    let waterPhase (frame: int64) (column: int) (rowIndex: int) : float32 =
        let index = VillageArt.waterFrameTickAt frame column rowIndex
        let fraction = float32 (frame % 32L) / 32.0f
        (float32 index + fraction) * twoPiOverThree

    /// 该水格的正弦波值,范围 [-1, 1]。
    let waterWave (frame: int64) (column: int) (rowIndex: int) : float32 =
        sin (waterPhase frame column rowIndex)

    /// 该水格亮度倍率:1.0 ± waterRippleAmplitude。
    let waterColumnBrightness (frame: int64) (column: int) (rowIndex: int) : float32 =
        1.0f + waterRippleAmplitude * waterWave frame column rowIndex

    /// 该水格亮面整体抬升像素(-2..2),用于高光行位移而不留空洞。
    let waterColumnLift (frame: int64) (column: int) (rowIndex: int) : int =
        int (Math.Round(float (waterWave frame column rowIndex * 2.0f), MidpointRounding.AwayFromZero))

    /// 是否处于波峰(正弦 > 0.7),渲染器会给这一列加一条高光。
    let waterColumnCrest (frame: int64) (column: int) (rowIndex: int) : bool =
        waterWave frame column rowIndex > 0.7f

    // ---------------- ①.5 第二层高光波光(P57) ----------------
    // P46 验收遗留:主水带波光层次偏弱。在**主水带之上**再叠一层细高光条:
    //   * 更快/更细:周期 80 tick(主水带连续相位 96 tick),故推进速度略不同;
    //   * 逐列相位错开:相邻列固定差 highlightColumnStep(=2.4 rad,非零且异于主水带的 120°);
    //   * 更亮:纯白 (255,255,255),亮于主水带波峰色 (225,240,245);
    //   * 更窄:条高 2px(主波峰 3px)、段宽 20px(满格 32px),且亮列阈值更高(0.86 > 0.7)。
    // 全为帧计数纯函数:无随机、无时钟;`light` 参数用于昼夜 tint 衰减(日场 1.0,夜场 <1)。

    let highlightPeriodTicks = 80.0f
    let highlightColumnStep = 2.4f
    let highlightRowStep = 0.9f
    let highlightCrestThreshold = 0.86f
    let highlightThickness = 2
    let highlightBarWidth = 20
    let highlightRowDelta = 1
    let highlightRgb : int * int * int = (255, 255, 255)
    let highlightMaxAlpha = 220
    let highlightDayLight = 1.0f
    let highlightNightLight = 0.42f

    /// 高光层相位(弧度)。帧推进用 highlightPeriodTicks(与主水带的 96 tick 不同),
    /// 逐列差 highlightColumnStep、逐行差 highlightRowStep;帧号取模保持有界、无浮点漂移。
    let highlightPhase (frame: int64) (column: int) (rowIndex: int) : float32 =
        let t = float32 (frame % 100000L)
        (t / highlightPeriodTicks) * (2.0f * float32 Math.PI)
        + float32 column * highlightColumnStep
        + float32 rowIndex * highlightRowStep

    /// 高光层正弦波值,范围 [-1, 1]。
    let highlightWave (frame: int64) (column: int) (rowIndex: int) : float32 =
        sin (highlightPhase frame column rowIndex)

    /// 该列是否出现细高光条(阈值比主水带高 → 更稀疏)。
    let highlightVisible (frame: int64) (column: int) (rowIndex: int) : bool =
        highlightWave frame column rowIndex > highlightCrestThreshold

    /// 高光条 alpha:波峰处最亮,随 `light`(昼夜 tint)线性衰减,钳 [0,255]。
    let highlightAlpha (light: float32) (frame: int64) (column: int) (rowIndex: int) : int =
        let wave = highlightWave frame column rowIndex
        let raw = float32 highlightMaxAlpha * light * (0.6f + 0.4f * wave)
        let rounded = int (Math.Round(float raw, MidpointRounding.AwayFromZero))
        max 0 (min 255 rounded)

    // ---------------- ② 灯笼呼吸光晕 ----------------

    /// 灯笼呼吸 alpha,界于 [40, 96];相位随帧与灯序号推进,邻灯不同步。
    let lanternBreathAlpha (frame: int64) (index: int) : int =
        let phase = float32 (frame % 4096L) / 22.0f + float32 index * 1.3f
        68 + int (Math.Round(float (sin phase * 28.0f), MidpointRounding.AwayFromZero))

    // ---------------- ③ 屋脊上方双层视差云影 ----------------

    let cloudLayerCount = 2

    /// 两层云影的速度(px/帧),内层更慢,形成视差。
    let cloudLayerSpeed (layer: int) : float32 =
        if layer % cloudLayerCount = 0 then 0.5f else 1.0f

    /// 云影中心 y。两层都落在屋脊(TitleScreen.RidgeY = 44)上方。
    let cloudLayerY (layer: int) : int =
        if layer % cloudLayerCount = 0 then 16 else 32

    /// 云影 alpha(预乘绘制,低 alpha 柔光,绝不形成实心块)。外层更快、更实,便于量测视差。
    let cloudLayerAlpha (layer: int) : int =
        if layer % cloudLayerCount = 0 then 12 else 30

    let cloudLayerRadiusX (layer: int) : int =
        if layer % cloudLayerCount = 0 then 52 else 74

    let cloudLayerRadiusY (layer: int) : int =
        if layer % cloudLayerCount = 0 then 9 else 11

    let private cloudBases =
        [| [| 150; 520; 900; 1180 |]
           [| 300; 700; 1050 |] |]

    /// 云影水平位移(px),按图层速度随帧单调推进并按 (width+200) 环绕,保持有界。
    let cloudOffsetPx (layer: int) (frame: int64) (width: int) : int =
        let speed = cloudLayerSpeed layer
        let travelled =
            int (Math.Round(float (float32 (frame % 100000L) * speed), MidpointRounding.AwayFromZero))
        let span = max 1 (width + 200)
        ((travelled % span) + span) % span

    /// 该图层若干云团中心 (x, y),x 已含位移并环绕,y 恒定在屋脊上方。
    let cloudPuffCenters (layer: int) (frame: int64) (width: int) : (int * int) list =
        let offset = cloudOffsetPx layer frame width
        let span = max 1 (width + 200)
        let y = cloudLayerY layer
        cloudBases.[layer % cloudLayerCount]
        |> Array.toList
        |> List.map (fun baseX -> (((baseX + offset) % span) - 100), y)

    /// 该图层的云团椭圆 (cx, cy, rx, ry),供渲染层用 VillageArt.ellipseRuns 落成柔边。
    let cloudPuffs (layer: int) (frame: int64) (width: int) : (int * int * int * int) list =
        let rx = cloudLayerRadiusX layer
        let ry = cloudLayerRadiusY layer
        cloudPuffCenters layer frame width |> List.map (fun (cx, cy) -> cx, cy, rx, ry)

/// P46 取证:环境动效证据帧表(60Hz 帧号纯数据,Game 与回归测试共用)。
/// * splash 涟漪中帧:水面仍可见(reveal<1);
/// * 云影两帧:都由视差位移纯函数决定,帧差即云影位移;
/// * 菜单灯笼两相位:入场完成后(>=120)的呼吸波峰/波谷,两帧 luma 差即呼吸幅度。
module P46ShotScript =

    let splashRippleName = "p46-splash-ripple"
    let cloudEarlyName = "p46-cloud-a"
    let cloudLateName = "p46-cloud-b"
    let menuPeakName = "p46-menu-lantern-a"
    let menuTroughName = "p46-menu-lantern-b"

    let splashRippleFrame = 16L
    let cloudEarlyFrame = 100L
    let cloudLateFrame = 140L
    // menuEntranceFrame 在 Update 末尾 +1,故设置值取目标相位帧的前一帧。
    let menuPeakFrame = 172L
    let menuTroughFrame = 241L

/// P57 取证:第二层高光波光证据帧表(60Hz 帧号纯数据,Game 与回归测试共用)。
/// 对同一帧各拍「高光层关 / 开」两帧,像素差即高光层贡献;splash 与 menu 各一组。
module P57ShotScript =

    let splashBaseName = "p57-splash-base"
    let splashHighlightName = "p57-splash-highlight"
    let menuBaseName = "p57-menu-base"
    let menuHighlightName = "p57-menu-highlight"

    /// splash 水面仍可见(reveal<1)。
    let splashFrame = 16L
    /// menu 入场完成后的稳定帧(>=120)。
    let menuFrame = 180L