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

    type Segment =
        { Label: string
          Value: string }

    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 }
          { Label = "时间"; Value = M6Presentation.clockTimeLabel world.Tick }
          { Label = "倍速"; Value = M6Presentation.speedLabel control }
          { Label = "体力"; Value = sprintf "%.0f" needs.Energy }
          { Label = "钱币"; Value = sprintf "%.0f" needs.Money } ]

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

    /// 左对齐排布,返回 (起点 x, 宽度, 分段);纯函数,便于布局回归断言。
    let layout (segments: Segment list) : (int * int * Segment) list =
        let _, placed =
            segments
            |> List.fold
                (fun (x, acc) segment ->
                    let width = segmentWidth 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) }