summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop/HudLayout.fs
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-28 19:03:22 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-28 19:03:22 +0800
commit588560deda6b701a62653dff19079dc0a21fa944 (patch)
tree3d734aaf9ad546ec52a42af0a2c2635d2d26d670 /src/LivingVillage.Desktop/HudLayout.fs
parent5c67e85688ff1e6150b03501adae1a5d5e67bbdc (diff)
downloadliving-village-588560deda6b701a62653dff19079dc0a21fa944.tar.gz
p43: HUD 星露谷化切片 — 右上日历钟卡/体力条形图/底部10格hotbar(1-0选中+高亮+输入隔离)
Diffstat (limited to 'src/LivingVillage.Desktop/HudLayout.fs')
-rw-r--r--src/LivingVillage.Desktop/HudLayout.fs128
1 files changed, 128 insertions, 0 deletions
diff --git a/src/LivingVillage.Desktop/HudLayout.fs b/src/LivingVillage.Desktop/HudLayout.fs
index 05fce33..b19df4b 100644
--- a/src/LivingVillage.Desktop/HudLayout.fs
+++ b/src/LivingVillage.Desktop/HudLayout.fs
@@ -151,3 +151,131 @@ module HudLayout =
Value = Color(228, 238, 246)
SegmentBackdrop = Color(4, 8, 16, 160)
Outline = Color(3, 6, 12, 255) }
+
+ // ---- P43:右上日历钟卡(纯模型;季节/天数/时段/时钟全部由真实 tick 导出)----
+
+ type CalendarReading =
+ { Season: string
+ DayNumber: int64
+ Period: string
+ Clock: string }
+
+ /// 一季 28 模拟日,四季循环。纯演示层日历,只读 Tick,不写任何模拟状态。
+ let daysPerSeason = 28L
+ let seasonNames = [| "春"; "夏"; "秋"; "冬" |]
+
+ let dayIndexOf (tick: int64) : int64 = tick / Sim.ticksPerDay
+ let dayNumberOf (tick: int64) : int64 = dayIndexOf tick + 1L
+
+ let seasonOf (tick: int64) : string =
+ let index = ((dayIndexOf tick / daysPerSeason) % 4L + 4L) % 4L
+ seasonNames.[int index]
+
+ let calendarReading (tick: int64) : CalendarReading =
+ { Season = seasonOf tick
+ DayNumber = dayNumberOf tick
+ Period = M6Presentation.timePeriodLabel tick
+ Clock = M6Presentation.clockTimeLabel tick }
+
+ let calendarLine1 (reading: CalendarReading) : string =
+ sprintf "%s · 第%d天" reading.Season reading.DayNumber
+
+ let calendarLine2 (reading: CalendarReading) : string =
+ sprintf "%s %s" reading.Period reading.Clock
+
+ let calendarMarginRight = 12
+ let calendarMarginTop = 8
+ let calendarPadX = 10
+ let calendarPadY = 8
+ let calendarLineGap = 4
+ let calendarTextHeight = 8 * textScale
+ let calendarCardHeight = 2 * calendarTextHeight + calendarLineGap + 2 * calendarPadY
+ let calendarIcon = ClockIcon
+
+ /// 右上角卡片矩形:宽度按两行文字实测宽度 + 图标 + 内边距,绝不越界、绝不压住顶部窄条。
+ let calendarCardRect (viewportWidth: int) (reading: CalendarReading) : Rectangle =
+ let textWidth =
+ max
+ (FloatingPrompt.measureWidth (calendarLine1 reading) textScale)
+ (FloatingPrompt.measureWidth (calendarLine2 reading) textScale)
+ let width = iconWidth + iconGap + textWidth + 2 * calendarPadX
+ Rectangle(
+ viewportWidth - width - calendarMarginRight,
+ barHeight + accentHeight + calendarMarginTop,
+ width,
+ calendarCardHeight)
+
+ // ---- P43:体力图形化条形(填充比例取真实 Needs.Energy,0..100)----
+
+ let energyBarHeight = 10
+
+ let energyBarFill (energy: float32) : float32 =
+ if energy < 0.0f then 0.0f
+ elif energy > 100.0f then 1.0f
+ else energy / 100.0f
+
+ let energyBarTrack (x: int) (y: int) (width: int) : Rectangle =
+ Rectangle(x, y, width, energyBarHeight)
+
+ let energyBarFillRect (x: int) (y: int) (width: int) (energy: float32) : Rectangle =
+ Rectangle(x, y, int (float32 width * energyBarFill energy), energyBarHeight)
+
+ // ---- P43:底部 10 格 hotbar(原创点阵图标 + 键 1-0 选中 + 选中高亮)----
+
+ let hotbarSlotCount = 10
+ let hotbarSlotSize = 40
+ let hotbarSlotGap = 6
+ let hotbarIconScale = 3
+ let hotbarPad = 8
+ let hotbarBottomMargin = 12
+ let hotbarWidth = hotbarSlotCount * hotbarSlotSize + (hotbarSlotCount - 1) * hotbarSlotGap
+
+ let hotbarBarRect (viewportWidth: int) (viewportHeight: int) : Rectangle =
+ Rectangle(
+ (viewportWidth - hotbarWidth) / 2 - hotbarPad,
+ viewportHeight - hotbarSlotSize - hotbarBottomMargin - hotbarPad,
+ hotbarWidth + 2 * hotbarPad,
+ hotbarSlotSize + 2 * hotbarPad)
+
+ let hotbarSlotRect (viewportWidth: int) (viewportHeight: int) (index: int) : Rectangle =
+ let bar = hotbarBarRect viewportWidth viewportHeight
+ Rectangle(
+ bar.X + hotbarPad + index * (hotbarSlotSize + hotbarSlotGap),
+ bar.Y + hotbarPad,
+ hotbarSlotSize,
+ hotbarSlotSize)
+
+ let hotbarIconRect (slot: Rectangle) : Rectangle =
+ let size = iconGrid * hotbarIconScale
+ Rectangle(slot.X + (slot.Width - size) / 2, slot.Y + (slot.Height - size) / 2, size, size)
+
+ /// 10 个原创 7x7 点阵占位图标(0..9),仅用现有像素贴图绘制,无外部素材。
+ let hotbarIconPatterns : string list list =
+ [ [ "0011100"; "0100010"; "1000001"; "1000001"; "1000001"; "0100010"; "0011100" ]
+ [ "0001000"; "0011000"; "0001000"; "0001000"; "0001000"; "0001000"; "0011100" ]
+ [ "0000000"; "0100010"; "1110111"; "1111111"; "0111110"; "0011100"; "0001000" ]
+ [ "0001000"; "0001000"; "0011100"; "0111110"; "1111111"; "1111111"; "0111110" ]
+ [ "0000000"; "0001000"; "0011100"; "1111110"; "0011100"; "0001000"; "0000000" ]
+ [ "0010100"; "0010100"; "0111110"; "0011100"; "0011100"; "0101010"; "0100010" ]
+ [ "0011100"; "0111110"; "1101011"; "1101011"; "1101011"; "0111110"; "0011100" ]
+ [ "0010001"; "0010010"; "0010100"; "0011000"; "0110000"; "0100000"; "1000000" ]
+ [ "0000100"; "0001100"; "0011110"; "0111100"; "0111000"; "0110000"; "0100000" ]
+ [ "0001000"; "0011100"; "0111110"; "1111111"; "0011100"; "0100010"; "1111111" ] ]
+
+ let hotbarIconRows (index: int) : string list =
+ hotbarIconPatterns.[((index % hotbarSlotCount) + hotbarSlotCount) % hotbarSlotCount]
+
+ /// 键位 1..9 -> 槽 0..8,键 0 -> 槽 9;其余键不选格。
+ let selectSlotForDigit (digit: int) : int option =
+ if digit >= 1 && digit <= 9 then Some (digit - 1)
+ elif digit = 0 then Some 9
+ else None
+
+ /// 在 0..9 内循环移动选中格(供方向导航/测试),越界自动回绕。
+ let cycleHotbar (current: int) (delta: int) : int =
+ let n = hotbarSlotCount
+ ((current + delta) % n + n) % n
+
+ /// hotbar 输入仅在「游戏内(非菜单)」且世界输入允许(无面板/对话)时生效。
+ let hotbarInputAllowed (menuPlaying: bool) (worldInputAllowed: bool) : bool =
+ menuPlaying && worldInputAllowed