diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-22 00:10:18 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-22 00:10:18 +0800 |
| commit | f79839a4bb57b977b7208e5f826a8b0989cbc33e (patch) | |
| tree | cf0c105b114102bfbb73b3c8d400e83614b861be /src/LivingVillage.Desktop | |
| parent | d85cce460fd7fbb8854fb11ba547be1baf576de7 (diff) | |
| download | living-village-f79839a4bb57b977b7208e5f826a8b0989cbc33e.tar.gz | |
feat(desktop): P16 HUD 顶部窄信息条(时段/时间/倍速/体力/钱币)+ 冬夜冷色 + 面板/提示渐变重绘
Diffstat (limited to 'src/LivingVillage.Desktop')
| -rw-r--r-- | src/LivingVillage.Desktop/ChineseText.fs | 6 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/Game.fs | 94 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/HudLayout.fs | 68 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/Interaction.fs | 9 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj | 1 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/M6Presentation.fs | 22 |
6 files changed, 158 insertions, 42 deletions
diff --git a/src/LivingVillage.Desktop/ChineseText.fs b/src/LivingVillage.Desktop/ChineseText.fs index 0d988fe..52e124d 100644 --- a/src/LivingVillage.Desktop/ChineseText.fs +++ b/src/LivingVillage.Desktop/ChineseText.fs @@ -124,6 +124,12 @@ module ChineseText = "E 进屋" "E 交谈" "时间" + "时段" + "倍速" + "体力" + "钱币" + "清晨" + "黄昏" "互动已记录" "1倍速" "2倍速" diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs index 8402a72..3bb43f8 100644 --- a/src/LivingVillage.Desktop/Game.fs +++ b/src/LivingVillage.Desktop/Game.fs @@ -864,52 +864,72 @@ type LivingVillageGame() as this = this.DrawM5Overlay() member private this.DrawM5Overlay() = + this.DrawHudBar() + if m5View.Panel = WorldPanel then + this.DrawTransientStatus() + else + this.DrawPanel() + + /// 顶部窄信息条:时段/时间/倍速/体力/钱币,半透明渐变 + 强调线,绝非大黑块。 + member private this.DrawHudBar() = let viewport = this.GraphicsDevice.Viewport - let textScale = 2 - let lineHeight = 18 let profile = M6Presentation.profileAtTick world.Tick - let timeLine = sprintf "时间 %s %s" (M6Presentation.dayNightLabel profile.Mode) (M6Presentation.clockLabel simulationControl) - let persistentLines = - timeLine :: (m5View.Prompt |> Option.toList) - let dialogueLines = - match m5View.Panel, m5View.Menu with - | DialoguePanel, Some menu -> - menu.Options - |> List.mapi (fun index intent -> sprintf "%d %s" (index + 1) (M5Interaction.intentLine intent)) - |> fun options -> [ sprintf "互动:村民 %d" (M5Interaction.menuNpcNumber menu) ] @ options - | _ -> [] + let palette = HudLayout.palette profile.Mode + let placed = HudLayout.layout (HudLayout.segments world simulationControl) + let barHeight = HudLayout.barHeight + let textY = (barHeight - 8 * HudLayout.textScale) / 2 + spriteBatch.Begin() + // 竖向渐变:上半略亮、下半压暗,避免整条纯色。 + spriteBatch.Draw(pixel, Rectangle(0, 0, viewport.Width, barHeight / 2), palette.BarTop) + spriteBatch.Draw(pixel, Rectangle(0, barHeight / 2, viewport.Width, barHeight - barHeight / 2), palette.BarBottom) + spriteBatch.Draw(pixel, Rectangle(0, barHeight, viewport.Width, HudLayout.accentHeight), palette.Accent) + for (x, _, segment) in placed do + PixelText.draw spriteBatch pixel cjkAtlas x textY HudLayout.textScale palette.Label segment.Label + let labelAdvanced = FloatingPrompt.measureWidth (segment.Label + " ") HudLayout.textScale + PixelText.draw spriteBatch pixel cjkAtlas (x + labelAdvanced) textY HudLayout.textScale palette.Value segment.Value + spriteBatch.End() + + /// 常驻情境状态:顶部条下方的小药丸,仅 WorldPanel 且瞬态未过期时出现。 + member private this.DrawTransientStatus() = + let viewport = this.GraphicsDevice.Viewport let statusView = { m5View with StatusTick = Some hudStatusAt } - let statusLine = M5Interaction.transientStatusLine hudClock statusView |> Option.toList - let allLines = persistentLines @ dialogueLines @ statusLine - let cornerLines = M5Interaction.cornerStatusLines world - let panelWidth = min 360 (max 1 (viewport.Width - 32)) - let maxLines = max 1 ((viewport.Height - 48) / lineHeight) - let lines = allLines |> List.truncate maxLines - let panelHeight = lines.Length * lineHeight + 24 + match M5Interaction.transientStatusLine hudClock statusView with + | None -> () + | Some line -> + let scale = 2 + let width = FloatingPrompt.measureWidth line scale + let x = 16 + let y = HudLayout.barHeight + HudLayout.accentHeight + 8 + spriteBatch.Begin() + spriteBatch.Draw(pixel, Rectangle(x - 8, y - 6, width + 16, 8 * scale + 12), Color(16, 26, 32, 200)) + spriteBatch.Draw(pixel, Rectangle(x - 8, y - 6, 2, 8 * scale + 12), Color(92, 180, 190)) + PixelText.draw spriteBatch pixel cjkAtlas x y scale (Color(232, 242, 232)) line + spriteBatch.End() + + /// 打开的面板(对话/需求/观察/年鉴/任务):底部左侧轻量渐变板,沿用既有 panelLines。 + member private this.DrawPanel() = + let viewport = this.GraphicsDevice.Viewport + let textScale = 2 + let lineHeight = 18 + let lines = M5Interaction.panelLines world m5View + let panelWidth = min 380 (max 1 (viewport.Width - 32)) + let maxLines = max 1 ((viewport.Height - HudLayout.barHeight - 48) / lineHeight) + let visibleLines = lines |> List.truncate maxLines + let panelHeight = visibleLines.Length * lineHeight + 26 let panelX = 16 - let panelY = max 16 (viewport.Height - panelHeight - 16) - let panel = Rectangle(panelX, panelY, panelWidth, panelHeight) + let panelY = max (HudLayout.barHeight + 12) (viewport.Height - panelHeight - 16) let innerX = panelX + 12 let innerWidth = max 1 (panelWidth - 24) let maxChars = max 1 (innerWidth / (6 * textScale)) spriteBatch.Begin() - spriteBatch.Draw(pixel, panel, Color(15, 19, 25, 160)) + // 竖向渐变板(半透明,非纯黑),顶部与左侧各一条强调线。 + spriteBatch.Draw(pixel, Rectangle(panelX, panelY, panelWidth, panelHeight / 2), Color(30, 44, 52, 190)) + spriteBatch.Draw(pixel, Rectangle(panelX, panelY + panelHeight / 2, panelWidth, panelHeight - panelHeight / 2), Color(16, 24, 30, 190)) spriteBatch.Draw(pixel, Rectangle(panelX, panelY, panelWidth, 3), Color(92, 180, 190)) - lines - |> List.iteri (fun index line -> - let visible = if line.Length > maxChars then line.Substring(0, maxChars) else line - let color = if index = 0 then Color(220, 235, 220) else Color(190, 205, 210) - PixelText.draw spriteBatch pixel cjkAtlas innerX (panelY + 12 + index * lineHeight) textScale color visible) - - // Minimal top-right status strip: day counter + real money/energy values. - let cornerWidth = 232 - let cornerHeight = List.length cornerLines * lineHeight + 20 - let cornerX = viewport.Width - cornerWidth - 16 - let cornerY = 16 - spriteBatch.Draw(pixel, Rectangle(cornerX, cornerY, cornerWidth, cornerHeight), Color(15, 19, 25, 150)) - spriteBatch.Draw(pixel, Rectangle(cornerX, cornerY, cornerWidth, 3), Color(92, 180, 190)) - cornerLines + spriteBatch.Draw(pixel, Rectangle(panelX, panelY, 2, panelHeight), Color(92, 180, 190, 160)) + visibleLines |> List.iteri (fun index line -> let visible = if line.Length > maxChars then line.Substring(0, maxChars) else line - PixelText.draw spriteBatch pixel cjkAtlas (cornerX + 10) (cornerY + 10 + index * lineHeight) textScale (Color(220, 235, 220)) visible) + let color = if index = 0 then Color(226, 238, 226) else Color(192, 208, 212) + PixelText.draw spriteBatch pixel cjkAtlas innerX (panelY + 13 + index * lineHeight) textScale color visible) spriteBatch.End() diff --git a/src/LivingVillage.Desktop/HudLayout.fs b/src/LivingVillage.Desktop/HudLayout.fs new file mode 100644 index 0000000..d943cec --- /dev/null +++ b/src/LivingVillage.Desktop/HudLayout.fs @@ -0,0 +1,68 @@ +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 + + type Palette = + { BarTop: Color + BarBottom: Color + Accent: Color + Label: Color + Value: Color } + + /// 白天暖色 / 夜晚(冬夜)冷色微调;均为半透明渐变窄条,无大黑块。 + 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) } + | 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) } diff --git a/src/LivingVillage.Desktop/Interaction.fs b/src/LivingVillage.Desktop/Interaction.fs index b29cf6b..dd372ef 100644 --- a/src/LivingVillage.Desktop/Interaction.fs +++ b/src/LivingVillage.Desktop/Interaction.fs @@ -161,11 +161,10 @@ module FloatingPrompt = Width: int Height: int } - // HUD exclusion geometry shared with the Game.fs renderer: bottom-left lightweight - // panel band and top-right corner status strip. + // HUD exclusion geometry shared with the Game.fs renderer: full-width top status bar + // and the bottom-left lightweight panel band. + let hudTopBarHeight = 39 let hudBottomBandHeight = 160 - let hudTopRightWidth = 248 - let hudTopRightHeight = 72 /// Width of text as PixelText.draw advances it (scale applied, CJK 8px, latin 6px, space 4px). let measureWidth (text: string) (scale: int) : int = @@ -194,7 +193,7 @@ module FloatingPrompt = let x = if width + margin * 2 > viewportWidth then margin else max margin (min (viewportWidth - margin - width) screenX) - let topSafe = if x + width >= viewportWidth - hudTopRightWidth then hudTopRightHeight + margin else margin + let topSafe = hudTopBarHeight + margin let bottomSafe = viewportHeight - hudBottomBandHeight - height - margin let y = max topSafe (min bottomSafe screenY) { X = x; Y = y; Width = width; Height = height } diff --git a/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj b/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj index 27e5da6..d57036b 100644 --- a/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj +++ b/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj @@ -17,6 +17,7 @@ <Compile Include="Interaction.fs" /> <Compile Include="SampleScript.fs" /> <Compile Include="M6Presentation.fs" /> + <Compile Include="HudLayout.fs" /> <Compile Include="TitleScreen.fs" /> <Compile Include="Game.fs" /> diff --git a/src/LivingVillage.Desktop/M6Presentation.fs b/src/LivingVillage.Desktop/M6Presentation.fs index ea8fd2f..65300f0 100644 --- a/src/LivingVillage.Desktop/M6Presentation.fs +++ b/src/LivingVillage.Desktop/M6Presentation.fs @@ -63,3 +63,25 @@ module M6Presentation = | OneX -> "1倍速" | TwoX -> "2倍速" | FiveX -> "5倍速" + + /// 倍速标签(语义别名,HUD 用)。 + let speedLabel (control: SimulationControl) : string = clockLabel control + + let private hourOf (tick: int64) : int = + int ((tick / (Sim.ticksPerDay / 24L)) % 24L) + + /// 24 小时制的模拟时钟(HH:MM),由 tick 纯函数导出。 + let clockTimeLabel (tick: int64) : string = + let hourTicks = Sim.ticksPerDay / 24L + let hour = hourOf tick + let minute = int ((tick % hourTicks) * 60L / hourTicks) + sprintf "%02d:%02d" hour minute + + /// 时段标签:清晨/白天/黄昏/夜晚,与实际 tick 一致(纯函数)。 + let timePeriodLabel (tick: int64) : string = + let hour = hourOf tick + if hour < 5 then "夜晚" + elif hour < 8 then "清晨" + elif hour < 17 then "白天" + elif hour < 20 then "黄昏" + else "夜晚" |
