summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop/TitleScreen.fs
blob: 2e760efc29065e39d1a41a2b803c276fa8a26ca5 (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
namespace LivingVillage.Desktop

module TitleScreen =

    type ColorRgb =
        { R: int
          G: int
          B: int }

    type TitleLayout =
        { SkyBands: ColorRgb list
          RoofTileSlots: int list
          RidgeY: int
          EaveY: int
          WaterRows: int list
          Lanterns: (int * int) list
          TitleText: string
          TitleX: int
          TitleY: int
          TitleScale: int
          MenuX: int
          MenuTopY: int
          MenuLineHeight: int
          PanelX: int
          PanelY: int
          PanelWidth: int
          PanelHeight: int
          HeadingY: int
          FirstItemY: int
          CursorX: int
          CursorWidth: int
          TextX: int
          ControlsPanelY: int
          ControlsRowHeight: int
          ControlsKeyX: int
          ControlsKeyWidth: int
          ControlsActionX: int }

    type TextRect =
        { X: int
          Y: int
          Width: int
          Height: int }

    let rectsDisjoint (a: TextRect) (b: TextRect) : bool =
        a.X + a.Width <= b.X
        || b.X + b.Width <= a.X
        || a.Y + a.Height <= b.Y
        || b.Y + b.Height <= a.Y

    /// Key-name and action rectangles for one controls-page row. The key column has a
    /// fixed width shared by every row, so all action texts start on the same x.
    let controlRowRects (layout: TitleLayout) (rowIndex: int) : TextRect * TextRect =
        let y = layout.ControlsPanelY + 60 + rowIndex * layout.ControlsRowHeight
        { X = layout.ControlsKeyX; Y = y; Width = layout.ControlsKeyWidth; Height = 16 },
        { X = layout.ControlsActionX; Y = y; Width = layout.PanelX + layout.PanelWidth - layout.ControlsActionX - 24; Height = 16 }

    /// Cursor and label rectangles for one menu row. The cursor is drawn in its own
    /// column; the label always starts to the right of it, so they can never overprint.
    let selectionRects (layout: TitleLayout) (rowIndex: int) : TextRect * TextRect =
        let y = layout.FirstItemY + rowIndex * layout.MenuLineHeight
        { X = layout.CursorX; Y = y; Width = layout.CursorWidth; Height = 16 },
        { X = layout.TextX; Y = y; Width = layout.PanelX + layout.PanelWidth - layout.TextX - 24; Height = 16 }

    // Palette mirrors the VillageArt atlas: roof (45,49,57)/(30,33,38), water (54,116,145),
    // lantern paper (212,92,74). Purely decorative reuse of existing art colors.
    let private skyBands : ColorRgb list =
        [ { R = 16; G = 26; B = 42 }
          { R = 20; G = 36; B = 52 }
          { R = 24; G = 46; B = 62 }
          { R = 28; G = 56; B = 68 }
          { R = 32; G = 64; B = 72 } ]

    let private roofRidgeSlot = 8
    let private roofEaveSlot = 9
    let private waterTileSlot = 1
    let private lanternColor = { R = 212; G = 92; B = 74 }
    let private lanternHalo = { R = 252; G = 216; B = 148 }

    let lanternColors : ColorRgb * ColorRgb = lanternColor, lanternHalo
    let skyBandCount = skyBands.Length
    let waterSlot = waterTileSlot
    let roofSlots = roofRidgeSlot, roofEaveSlot

    /// Deterministic 2s entrance at the fixed 60Hz step: 1.0 = elements fully parked
    /// off-screen at start, 0.0 = settled. Pure function of the frame counter.
    let entranceProgress (frameCount: int64) : float32 =
        if frameCount >= 120L then 0.0f
        else 1.0f - float32 frameCount / 120.0f

    // ---- P42 environment FX: all pure functions of the frame counter, so re-rendering
    // the same frame is byte-identical. Shared by the splash and the start menu. ----

    /// Sine ripple amplitude of the water band. Kept as a named constant so a test can
    /// assert it never regresses below the full-frame visibility budget (>= 0.25).
    let waterRippleAmplitude = 0.32f

    /// Water ripple phase: moves with the frame and is dense across tile columns
    /// (~5 tiles per wave), so adjacent 28px columns differ enough to read at 1x.
    let waterRipplePhase (frame: int64) (column: int) (rowIndex: int) : float32 =
        float32 (frame % 4096L) / 5.0f + float32 column * 1.25f + float32 rowIndex * 0.6f

    /// Water ripple brightness per tile column: base 1.0 +/- waterRippleAmplitude sine.
    let waterRippleBrightness (frame: int64) (column: int) (rowIndex: int) : float32 =
        1.0f + waterRippleAmplitude * sin (waterRipplePhase frame column rowIndex)

    /// Water crest lift in whole pixels (-2..2) for a tile column; drives the moving
    /// ripple highlight without ever leaving a hole in the water band.
    let waterRippleLift (frame: int64) (column: int) (rowIndex: int) : int =
        int (System.Math.Round(float (sin (waterRipplePhase frame column rowIndex) * 2.0f), System.MidpointRounding.AwayFromZero))

    /// True on columns sitting on a wave crest; the renderer caps these with a bright
    /// highlight and the frame analysis counts them.
    let waterRippleCrest (frame: int64) (column: int) (rowIndex: int) : bool =
        sin (waterRipplePhase frame column rowIndex) > 0.55f

    /// Eave-lantern breathing glow alpha, bounded to [40, 96]; phase moves with the
    /// frame and the lantern index so the bank lanterns breathe out of step.
    let lanternBreathAlpha (frame: int64) (index: int) : int =
        let phase = float32 (frame % 4096L) / 22.0f + float32 index * 1.3f
        68 + int (System.Math.Round(float (sin phase * 28.0f), System.MidpointRounding.AwayFromZero))

    let layout (viewportWidth: int) (viewportHeight: int) : TitleLayout =
        let width = max 320 viewportWidth
        let height = max 240 viewportHeight
        let tilePixels = 32
        let columns = width / tilePixels
        let roofTileSlots =
            [ 0 .. columns - 1 ]
            |> List.map (fun column ->
                if column % 4 = 0 || column % 7 = 3 then roofRidgeSlot else roofEaveSlot)
        let waterRows =
            [ height - 96
              height - 64 ]
        let lanternCount = max 3 (columns / 10)
        let lanterns =
            [ 1 .. lanternCount ]
            |> List.map (fun index ->
                let x = index * width / (lanternCount + 1)
                let y = (List.head waterRows) - 44 + (if index % 2 = 0 then 12 else 0)
                x, y)
        let panelWidth = min 460 (width - 64)
        { SkyBands = skyBands
          RoofTileSlots = roofTileSlots
          RidgeY = 44
          EaveY = 76
          WaterRows = waterRows
          Lanterns = lanterns
          TitleText = "江南水乡"
          TitleX = (width - 4 * (4 * 16)) / 2
          TitleY = 120
          TitleScale = 4
          MenuX = (width - panelWidth) / 2 + 40
          MenuTopY = height / 2 - 40
          MenuLineHeight = 30
          PanelX = (width - panelWidth) / 2
          PanelY = height / 2 - 64
          PanelWidth = panelWidth
          PanelHeight = min 360 (height - height / 2 - 40)
          HeadingY = height / 2 - 64 + 20
          FirstItemY = height / 2 - 64 + 60
          CursorX = (width - panelWidth) / 2 + 24
          CursorWidth = 20
          TextX = (width - panelWidth) / 2 + 24 + 20 + 12
          ControlsPanelY = max 16 (height - 480)
          ControlsRowHeight = 25
          ControlsKeyX = (width - panelWidth) / 2 + 24
          ControlsKeyWidth = 120
          ControlsActionX = (width - panelWidth) / 2 + 24 + 120 + 12 }