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) }