summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop/Game.fs
diff options
context:
space:
mode:
Diffstat (limited to 'src/LivingVillage.Desktop/Game.fs')
-rw-r--r--src/LivingVillage.Desktop/Game.fs168
1 files changed, 166 insertions, 2 deletions
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 且瞬态未过期时出现。