From 588560deda6b701a62653dff19079dc0a21fa944 Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Mon, 28 Sep 2026 19:03:22 +0800 Subject: p43: HUD 星露谷化切片 — 右上日历钟卡/体力条形图/底部10格hotbar(1-0选中+高亮+输入隔离) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/LivingVillage.Desktop/Game.fs | 168 +++++++++++++++++++++++++++++++++++++- 1 file changed, 166 insertions(+), 2 deletions(-) (limited to 'src/LivingVillage.Desktop/Game.fs') diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs index 121a090..ea03437 100644 --- a/src/LivingVillage.Desktop/Game.fs +++ b/src/LivingVillage.Desktop/Game.fs @@ -306,6 +306,11 @@ type LivingVillageGame() as this = let p42ShotMode = Environment.GetEnvironmentVariable("LV_P42_SHOT") = "1" let mutable p42SplashEarlyDone = false let mutable p42SplashLateDone = false + // P43 证据钩子:LV_P43_SHOT=1 时在真实世界里拍昼/夜两张 HUD 整帧(日历钟卡 + 体力条 + 10 格 hotbar)。 + let p43ShotMode = Environment.GetEnvironmentVariable("LV_P43_SHOT") = "1" + let mutable p43Step = 0 + let mutable p43PendingName = "" + let mutable p43Pending = false let mutable autoplayFrames = 0 let mutable flowStep = 0 let mutable flowHold = 0 @@ -314,6 +319,8 @@ type LivingVillageGame() as this = let mutable hudClock = 0L let mutable hudStatus = "" let mutable hudStatusAt = 0L + // P43:hotbar 选中格(0..9),仅由游戏内 1-0 键或证据脚本写入,非伪造状态。 + let mutable hotbarSelected = 0 let mutable pauseHelpControl: SimulationControl option = None let recordMode = Environment.GetEnvironmentVariable("LV_AUTOPLAY_RECORD") = "1" let recordDirectory = @@ -770,6 +777,47 @@ type LivingVillageGame() as this = p40PendingName world.Tick world.Avatar.Pos.X world.Avatar.Pos.Y camera.X camera.Y p40Step <- p40Step + 1 + /// P43 证据:真实世界里拍昼 / 夜两张 HUD 整帧(右上日历钟卡 + 体力条形 + 底部 10 格 hotbar)。 + member private this.PrepareP43Shot () = + let setHour (hour: float) = + let tick = M6Presentation.resolveStartTick (Some hour) false + world <- { world with Tick = tick; Time = float tick * Sim.dtSeconds } + if not p43Pending then + match p43Step with + | 0 -> + setHour 12.0 + hotbarSelected <- (HudLayout.selectSlotForDigit 1 |> Option.defaultValue 0) + this.CenterCamera() + p43PendingName <- "p43-hud-day" + p43Pending <- true + | 1 -> + setHour 23.5 + hotbarSelected <- (HudLayout.selectSlotForDigit 5 |> Option.defaultValue 0) + this.CenterCamera() + p43PendingName <- "p43-hud-night" + p43Pending <- true + | _ -> + printfn "p43-shot=done" + this.Exit() + + /// P43 证据:在 `Draw` 末尾落盘,并打印实测布局矩形与真实状态(供 p43-analysis.txt)。 + member private this.CaptureP43Shot () = + if p43Pending then + p43Pending <- false + System.IO.Directory.CreateDirectory recordDirectory |> ignore + this.SaveBackBuffer(sprintf "%s/%s.png" recordDirectory p43PendingName) + let viewport = this.GraphicsDevice.Viewport + let reading = HudLayout.calendarReading world.Tick + let cal = HudLayout.calendarCardRect viewport.Width reading + let bar = HudLayout.hotbarBarRect viewport.Width viewport.Height + let slot = HudLayout.hotbarSlotRect viewport.Width viewport.Height 0 + printfn + "p43-shot=%s tick=%d calendar=%d,%d,%d,%d hotbar=%d,%d,%d,%d slot0=%d,%d,%d,%d selected=%d energy=%.0f day=%d season=%s period=%s clock=%s" + p43PendingName world.Tick cal.X cal.Y cal.Width cal.Height bar.X bar.Y bar.Width bar.Height + slot.X slot.Y slot.Width slot.Height hotbarSelected world.Avatar.Mind.Needs.Energy + reading.DayNumber reading.Season reading.Period reading.Clock + p43Step <- p43Step + 1 + /// P20 第三步取证:真走位巡游(Sim.step 驱动)沿桥/路/民居门移动,用于桥面行走帧与昼夜录制。 member private this.RunRiverscapeTour () = if not riverscapeTourWatchStarted then @@ -909,6 +957,17 @@ type LivingVillageGame() as this = printfn $"m5 panel={M5Interaction.panelName m5View} status={m5View.Status} tick={world.Tick}" | None -> () let panelOpen = m5View.Panel <> WorldPanel + // P43:游戏内按 1-0 直接选中 hotbar 槽位;仅当世界输入允许(无面板/对话)时生效, + // Esc 打开的菜单走 UpdateMenu 分支、根本不进这里,输入不穿透。 + if HudLayout.hotbarInputAllowed (menu.Page = Playing) (M5Interaction.worldInputAllowed m5View) then + let digitKeys = + [ 1, Keys.D1; 2, Keys.D2; 3, Keys.D3; 4, Keys.D4; 5, Keys.D5 + 6, Keys.D6; 7, Keys.D7; 8, Keys.D8; 9, Keys.D9; 0, Keys.D0 ] + for (digit, key) in digitKeys do + if pressed key then + match HudLayout.selectSlotForDigit digit with + | Some slot -> hotbarSelected <- slot + | None -> () if not relationView && not panelOpen then let input = if automated then @@ -1087,6 +1146,14 @@ type LivingVillageGame() as this = this.PrepareP40Shot() elif p37ShotMode then this.RunP37Shot() + elif p43ShotMode then + if menu.Page = MainMenu then + this.DispatchMenuInput Confirm + elif menu.Page = OccupationSelect then + if menu.Selected <> 0 then this.DispatchMenuInput Down + else this.DispatchMenuInput Confirm + elif menu.Page = Playing then + this.PrepareP43Shot() elif menu.Page = Playing then this.UpdatePlaying gameTime kb pressed pressedAny elif menuShotMode then @@ -1488,6 +1555,8 @@ type LivingVillageGame() as this = this.CaptureP39Shot() if p40ShotMode && menu.Page = Playing then this.CaptureP40Shot() + if p43ShotMode && menu.Page = Playing then + this.CaptureP43Shot() /// 启动画面:分层天色 + 屋脊/水面剪影 + 标题淡入上浮 + 跳过提示,全部为帧计数纯函数。 member private this.DrawSplash() = @@ -1811,6 +1880,8 @@ type LivingVillageGame() as this = member private this.DrawM5Overlay() = this.DrawHudBar() + this.DrawCalendarCard() + this.DrawHotbar() if m5View.Panel = WorldPanel then this.DrawTransientStatus() else @@ -1849,13 +1920,106 @@ type LivingVillageGame() as this = spriteBatch.Draw(pixel, HudLayout.segmentBackdrop x width, palette.SegmentBackdrop) let textX = HudLayout.textOrigin x let labelAdvanced = FloatingPrompt.measureWidth (segment.Label + " ") HudLayout.textScale + let isEnergy = segment.Icon = HudLayout.EnergyIcon for (dx, dy) in HudLayout.textOutlineOffsets do drawIcon (x + dx) (iconY + dy) palette.Outline segment.Icon PixelText.draw spriteBatch pixel cjkAtlas (textX + dx) (textY + dy) HudLayout.textScale palette.Outline segment.Label - PixelText.draw spriteBatch pixel cjkAtlas (textX + labelAdvanced + dx) (textY + dy) HudLayout.textScale palette.Outline segment.Value + if not isEnergy then + PixelText.draw spriteBatch pixel cjkAtlas (textX + labelAdvanced + dx) (textY + dy) HudLayout.textScale palette.Outline segment.Value drawIcon x iconY palette.Value segment.Icon PixelText.draw spriteBatch pixel cjkAtlas textX textY HudLayout.textScale palette.Label segment.Label - PixelText.draw spriteBatch pixel cjkAtlas (textX + labelAdvanced) textY HudLayout.textScale palette.Value segment.Value + if isEnergy then + // P43:体力改图形化条形,填充比例 = 真实 Needs.Energy / 100(0..100),非伪造。 + let valueX = textX + labelAdvanced + let valueWidth = max 12 (FloatingPrompt.measureWidth segment.Value HudLayout.textScale) + let barY = (barHeight - HudLayout.energyBarHeight) / 2 + let track = HudLayout.energyBarTrack valueX barY valueWidth + spriteBatch.Draw(pixel, track, palette.Outline) + spriteBatch.Draw( + pixel, + Rectangle(track.X + 1, track.Y + 1, max 1 (track.Width - 2), track.Height - 2), + palette.SegmentBackdrop) + let fill = + HudLayout.energyBarFillRect + (track.X + 1) + (track.Y + 1) + (max 1 (track.Width - 2)) + world.Avatar.Mind.Needs.Energy + if fill.Width > 0 then + let barColor = + match profile.Mode with + | Day -> Color(212, 168, 96) + | Night -> Color(120, 168, 196) + spriteBatch.Draw(pixel, fill, barColor) + else + PixelText.draw spriteBatch pixel cjkAtlas (textX + labelAdvanced) textY HudLayout.textScale palette.Value segment.Value + spriteBatch.End() + + /// 按 7x7 点阵行用像素贴图绘制图标(无外部素材);调用方须已 Begin。 + member private this.DrawPattern (rows: string list) (iconX: int) (iconY: int) (scale: int) (color: Color) = + rows + |> List.iteri (fun row bits -> + bits + |> Seq.iteri (fun column bit -> + if bit = '1' then + spriteBatch.Draw(pixel, Rectangle(iconX + column * scale, iconY + row * scale, scale, scale), color))) + + /// P43:右上角日历钟卡(季节/第 N 天/时段/时钟),半透明深色底 + 细边框,绝不越界。 + member private this.DrawCalendarCard() = + let viewport = this.GraphicsDevice.Viewport + let profile = M6Presentation.profileAtTick world.Tick + let palette = HudLayout.palette profile.Mode + let reading = HudLayout.calendarReading world.Tick + let rect = HudLayout.calendarCardRect viewport.Width reading + let line1 = HudLayout.calendarLine1 reading + let line2 = HudLayout.calendarLine2 reading + spriteBatch.Begin() + spriteBatch.Draw(pixel, rect, palette.SegmentBackdrop) + spriteBatch.Draw(pixel, Rectangle(rect.X, rect.Y, rect.Width, 2), palette.Accent) + spriteBatch.Draw(pixel, Rectangle(rect.X, rect.Bottom - 2, rect.Width, 2), palette.Outline) + spriteBatch.Draw(pixel, Rectangle(rect.X, rect.Y, 2, rect.Height), palette.Outline) + spriteBatch.Draw(pixel, Rectangle(rect.Right - 2, rect.Y, 2, rect.Height), palette.Outline) + let iconX = rect.X + HudLayout.calendarPadX + let iconY = rect.Y + (rect.Height - HudLayout.iconHeight) / 2 + this.DrawPattern (HudLayout.iconRows HudLayout.calendarIcon) iconX iconY HudLayout.iconScale palette.Value + let textX = iconX + HudLayout.iconWidth + HudLayout.iconGap + let line1Y = rect.Y + HudLayout.calendarPadY + let line2Y = line1Y + 8 * HudLayout.textScale + HudLayout.calendarLineGap + for (dx, dy) in HudLayout.textOutlineOffsets do + PixelText.draw spriteBatch pixel cjkAtlas (textX + dx) (line1Y + dy) HudLayout.textScale palette.Outline line1 + PixelText.draw spriteBatch pixel cjkAtlas (textX + dx) (line2Y + dy) HudLayout.textScale palette.Outline line2 + PixelText.draw spriteBatch pixel cjkAtlas textX line1Y HudLayout.textScale palette.Value line1 + PixelText.draw spriteBatch pixel cjkAtlas textX line2Y HudLayout.textScale palette.Accent line2 + spriteBatch.End() + + /// P43:底部居中 10 格 hotbar,原创点阵图标 + 选中高亮 + 键位数字;半透明底 + 细边框。 + member private this.DrawHotbar() = + let viewport = this.GraphicsDevice.Viewport + let profile = M6Presentation.profileAtTick world.Tick + let palette = HudLayout.palette profile.Mode + let bar = HudLayout.hotbarBarRect viewport.Width viewport.Height + spriteBatch.Begin() + spriteBatch.Draw(pixel, bar, palette.SegmentBackdrop) + spriteBatch.Draw(pixel, Rectangle(bar.X, bar.Y, bar.Width, 2), palette.Accent) + spriteBatch.Draw(pixel, Rectangle(bar.X, bar.Bottom - 2, bar.Width, 2), palette.Outline) + spriteBatch.Draw(pixel, Rectangle(bar.X, bar.Y, 2, bar.Height), palette.Outline) + spriteBatch.Draw(pixel, Rectangle(bar.Right - 2, bar.Y, 2, bar.Height), palette.Outline) + for index in 0 .. HudLayout.hotbarSlotCount - 1 do + let slot = HudLayout.hotbarSlotRect viewport.Width viewport.Height index + let selected = index = hotbarSelected + let slotBackdrop = + if selected then Color(48, 66, 74, 210) else palette.SegmentBackdrop + spriteBatch.Draw(pixel, slot, slotBackdrop) + let border = if selected then palette.Accent else palette.Outline + spriteBatch.Draw(pixel, Rectangle(slot.X, slot.Y, slot.Width, 2), border) + spriteBatch.Draw(pixel, Rectangle(slot.X, slot.Bottom - 2, slot.Width, 2), border) + spriteBatch.Draw(pixel, Rectangle(slot.X, slot.Y, 2, slot.Height), border) + spriteBatch.Draw(pixel, Rectangle(slot.Right - 2, slot.Y, 2, slot.Height), border) + let iconRect = HudLayout.hotbarIconRect slot + let iconColor = if selected then palette.Accent else palette.Value + this.DrawPattern (HudLayout.hotbarIconRows index) iconRect.X iconRect.Y HudLayout.hotbarIconScale iconColor + let label = if index = 9 then "0" else string (index + 1) + PixelText.draw spriteBatch pixel cjkAtlas (slot.X + 3) (slot.Y + 2) 1 (if selected then palette.Accent else palette.Label) label spriteBatch.End() /// 常驻情境状态:顶部条下方的小药丸,仅 WorldPanel 且瞬态未过期时出现。 -- cgit v1.2.3