summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop/MenuAmbience.fs
blob: 74fefeb556b3e787044196f6a46a5a1c054395c2 (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
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

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

    /// 灯笼呼吸 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