summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop/HudLayout.fs
blob: 05fce335060703126770b2d5c302dd3ba6b152ab (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
namespace LivingVillage.Desktop

open Microsoft.Xna.Framework
open LivingVillage.Kernel
open LivingVillage.Kernel.Sim

/// 顶部窄信息条 HUD 的纯布局/配色模型:只做确定性计算,实际绘制在 Game.fs。
/// 常驻状态(时段/时间/倍速/体力/钱币)压进一条窄带,绝不用整屏纯色或大黑块。
module HudLayout =

    let barHeight = 36
    let accentHeight = 3
    let paddingX = 16
    let segmentGap = 20
    let textScale = 2

    /// 常驻段的像素图标(7x7 点阵,按 iconScale 放大),全部用 pixel 贴图绘制、无外部素材。
    type HudIcon =
        | PeriodIcon
        | ClockIcon
        | SpeedIcon
        | EnergyIcon
        | MoneyIcon

    type Segment =
        { Label: string
          Value: string
          Icon: HudIcon }

    let segmentText (segment: Segment) : string = sprintf "%s %s" segment.Label segment.Value

    /// 常驻状态片段,数值全部取自世界真实状态,绝不编造。
    let segments (world: World) (control: SimulationControl) : Segment list =
        let needs = world.Avatar.Mind.Needs
        [ { Label = "时段"; Value = M6Presentation.timePeriodLabel world.Tick; Icon = PeriodIcon }
          { Label = "时间"; Value = M6Presentation.clockTimeLabel world.Tick; Icon = ClockIcon }
          { Label = "倍速"; Value = M6Presentation.speedLabel control; Icon = SpeedIcon }
          { Label = "体力"; Value = sprintf "%.0f" needs.Energy; Icon = EnergyIcon }
          { Label = "钱币"; Value = sprintf "%.0f" needs.Money; Icon = MoneyIcon } ]

    // ---- 图标几何与点阵(纯数据/纯函数,Game 与回归测试共用)----

    let iconGrid = 7
    let iconScale = textScale
    let iconWidth = iconGrid * iconScale
    let iconHeight = iconGrid * iconScale
    let iconGap = 6

    /// 7x7 点阵,'1' 为实心。全部原创点阵,仅用现有像素贴图绘制。
    let iconRows (icon: HudIcon) : string list =
        match icon with
        | PeriodIcon ->
            [ "0001000"
              "1010101"
              "0011100"
              "0111110"
              "0011100"
              "1010101"
              "0001000" ]
        | ClockIcon ->
            [ "0011100"
              "0100010"
              "1001001"
              "1001000"
              "1001000"
              "0100010"
              "0011100" ]
        | SpeedIcon ->
            [ "0100010"
              "0110011"
              "0011011"
              "0001110"
              "0011011"
              "0110011"
              "0100010" ]
        | EnergyIcon ->
            [ "0000000"
              "0101010"
              "1111111"
              "1111111"
              "0111110"
              "0011100"
              "0001000" ]
        | MoneyIcon ->
            [ "0011100"
              "0111110"
              "1110111"
              "1100011"
              "1110111"
              "0111110"
              "0011100" ]

    /// 文本起始 x(图标占 iconWidth + iconGap)。
    let textOrigin (segmentX: int) : int = segmentX + iconWidth + iconGap

    /// 段文本(含标签)的像素宽度。
    let segmentWidth (segment: Segment) : int =
        FloatingPrompt.measureWidth (segmentText segment) textScale

    /// 段总宽(图标 + 间隙 + 文本)。
    let segmentSpan (segment: Segment) : int =
        iconWidth + iconGap + segmentWidth segment

    /// 左对齐排布,返回 (起点 x, 总宽, 分段);段内 x 起为图标,textOrigin 起为文本。
    /// 纯函数,便于布局回归断言。
    let layout (segments: Segment list) : (int * int * Segment) list =
        let _, placed =
            segments
            |> List.fold
                (fun (x, acc) segment ->
                    let width = segmentSpan segment
                    x + width + segmentGap, (x, width, segment) :: acc)
                (paddingX, [])
        List.rev placed

    /// P26:常驻片段文本的 1px 描边偏移(上/下/左/右),保证白天/夜景下都可读。
    let textOutlineOffsets : (int * int) list = [ (-1, 0); (1, 0); (0, -1); (0, 1) ]

    /// P26:常驻片段的半透明底色药丸,贴合文字高度并在左右各留 6px;纯函数便于回归。
    let segmentBackdrop (x: int) (width: int) : Microsoft.Xna.Framework.Rectangle =
        let height = 8 * textScale + 10
        let y = (barHeight - height) / 2
        Microsoft.Xna.Framework.Rectangle(x - 6, y, width + 12, height)

    type Palette =
        { BarTop: Color
          BarBottom: Color
          Accent: Color
          Label: Color
          Value: Color
          SegmentBackdrop: Color
          Outline: Color }

    /// 白天暖色 / 夜晚(冬夜)冷色微调;均为半透明渐变窄条,无大黑块。
    /// 每个常驻片段再叠一层半透明底色 + 1px 深色描边,双时段都能读清。
    let palette (mode: M6DayNight) : Palette =
        match mode with
        | Day ->
            { BarTop = Color(40, 56, 62, 200)
              BarBottom = Color(22, 34, 40, 200)
              Accent = Color(212, 168, 96)
              Label = Color(206, 220, 208)
              Value = Color(242, 248, 234)
              SegmentBackdrop = Color(8, 14, 20, 150)
              Outline = Color(6, 10, 14, 255) }
        | Night ->
            { BarTop = Color(22, 30, 48, 205)
              BarBottom = Color(13, 19, 33, 205)
              Accent = Color(120, 168, 196)
              Label = Color(176, 196, 214)
              Value = Color(228, 238, 246)
              SegmentBackdrop = Color(4, 8, 16, 160)
              Outline = Color(3, 6, 12, 255) }