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.fs210
1 files changed, 194 insertions, 16 deletions
diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs
index ea03437..00ae04b 100644
--- a/src/LivingVillage.Desktop/Game.fs
+++ b/src/LivingVillage.Desktop/Game.fs
@@ -311,6 +311,11 @@ type LivingVillageGame() as this =
let mutable p43Step = 0
let mutable p43PendingName = ""
let mutable p43Pending = false
+ // P44 证据钩子:LV_P44_SHOT=1 时在真实世界里开一场 NPC 对话弹层,拍默认页/翻页后/关闭恢复三帧。
+ let p44ShotMode = Environment.GetEnvironmentVariable("LV_P44_SHOT") = "1"
+ let mutable p44Step = 0
+ let mutable p44PendingName = ""
+ let mutable p44Pending = false
let mutable autoplayFrames = 0
let mutable flowStep = 0
let mutable flowHold = 0
@@ -321,6 +326,9 @@ type LivingVillageGame() as this =
let mutable hudStatusAt = 0L
// P43:hotbar 选中格(0..9),仅由游戏内 1-0 键或证据脚本写入,非伪造状态。
let mutable hotbarSelected = 0
+ // P44:对话/文字弹层模型与内容指纹(内容变化时重建,翻页状态保持不变)。
+ let mutable dialogOverlay: DialogOverlay.Model option = None
+ let mutable dialogOverlayKey = ""
let mutable pauseHelpControl: SimulationControl option = None
let recordMode = Environment.GetEnvironmentVariable("LV_AUTOPLAY_RECORD") = "1"
let recordDirectory =
@@ -818,6 +826,74 @@ type LivingVillageGame() as this =
reading.DayNumber reading.Season reading.Period reading.Clock
p43Step <- p43Step + 1
+ /// P44 证据:真实世界里把角色放到 NPC 旁、用真实 Interact 命令开对话弹层,
+ /// 依次拍「默认页 / 翻页后 / Esc 关闭后恢复」三帧。
+ member private this.PrepareP44Shot () =
+ let setHour (hour: float) =
+ let tick = M6Presentation.resolveStartTick (Some hour) false
+ world <- { world with Tick = tick; Time = float tick * Sim.dtSeconds }
+ if not p44Pending then
+ match p44Step with
+ | 0 ->
+ setHour 12.0
+ let npc = world.Npcs.[0]
+ world <- { world with Avatar = { world.Avatar with Pos = npc.Pos } }
+ let nextWorld, nextView = M5Interaction.apply M5Command.Interact world m5View
+ world <- nextWorld
+ m5View <- nextView
+ this.CenterCamera()
+ p44PendingName <- "p44-dialog-a"
+ p44Pending <- true
+ | 1 ->
+ match dialogOverlay with
+ | Some model ->
+ match DialogOverlay.nextPage model with
+ | Some next -> dialogOverlay <- Some next
+ | None -> ()
+ | None -> ()
+ p44PendingName <- "p44-dialog-b"
+ p44Pending <- true
+ | 2 ->
+ let nextWorld, nextView = M5Interaction.apply M5Command.ClosePanel world m5View
+ world <- nextWorld
+ m5View <- nextView
+ p44PendingName <- "p44-dialog-c"
+ p44Pending <- true
+ | _ ->
+ printfn "p44-shot=done"
+ this.Exit()
+
+ /// P44 证据:在 `Draw` 末尾落盘,并打印弹层实测 rect / 行数 / 页码;关闭帧打印 overlay=gone。
+ member private this.CaptureP44Shot () =
+ if p44Pending then
+ p44Pending <- false
+ System.IO.Directory.CreateDirectory recordDirectory |> ignore
+ this.SaveBackBuffer(sprintf "%s/%s.png" recordDirectory p44PendingName)
+ let viewport = this.GraphicsDevice.Viewport
+ match dialogOverlay with
+ | Some model ->
+ let layout = DialogOverlay.rect viewport.Width viewport.Height model
+ let current = DialogOverlay.view model
+ printfn
+ "p44-shot=%s panel=%s rect=%d,%d,%d,%d page=%d/%d lines=%d avatar=%b"
+ p44PendingName
+ (M5Interaction.panelName m5View)
+ layout.X
+ layout.Y
+ layout.Width
+ layout.Height
+ current.PageNumber
+ current.PageCount
+ current.Lines.Length
+ model.ShowAvatar
+ | None ->
+ printfn
+ "p44-shot=%s panel=%s overlay=gone prompt=%s"
+ p44PendingName
+ (M5Interaction.panelName m5View)
+ (m5View.Prompt |> Option.defaultValue "<none>")
+ p44Step <- p44Step + 1
+
/// P20 第三步取证:真走位巡游(Sim.step 驱动)沿桥/路/民居门移动,用于桥面行走帧与昼夜录制。
member private this.RunRiverscapeTour () =
if not riverscapeTourWatchStarted then
@@ -936,19 +1012,35 @@ type LivingVillageGame() as this =
if relationView then relationSnapshot <- Some(Sim.relationMatrix world)
let state = if relationView then "on" else "off"
printfn $"relation view={state} tick={world.Tick}"
+ // P44:弹层开启时用 空格/回车/E 翻页(下一页);最后一页再翻为 no-op,不崩。
+ let overlayOpen = dialogOverlay.IsSome
+ if overlayOpen && (pressed Keys.Space || pressed Keys.Enter || pressed Keys.E) then
+ match DialogOverlay.nextPage dialogOverlay.Value with
+ | Some next -> dialogOverlay <- Some next
+ | None -> ()
let m5Command =
- if M5Interaction.worldInputAllowed m5View && pressed Keys.E then Some Interact
- elif m5View.Panel = DialoguePanel && pressedAny [ Keys.D1; Keys.NumPad1 ] then Some Intent1
- elif m5View.Panel = DialoguePanel && pressedAny [ Keys.D2; Keys.NumPad2 ] then Some Intent2
- elif m5View.Panel = DialoguePanel && pressedAny [ Keys.D3; Keys.NumPad3 ] then Some Intent3
- elif m5View.Panel = DialoguePanel && pressedAny [ Keys.D4; Keys.NumPad4 ] then Some Intent4
- elif m5View.Panel = DialoguePanel && pressedAny [ Keys.D5; Keys.NumPad5 ] then Some Intent5
- elif m5View.Panel = DialoguePanel && pressedAny [ Keys.D6; Keys.NumPad6 ] then Some Intent6
- elif pressed Keys.Q then Some Observe
- elif pressed Keys.Tab then Some ToggleNeeds
- elif pressed Keys.C then Some ShowChronicle
- elif pressed Keys.T then Some ShowTaskPanel
- else None
+ if overlayOpen then
+ // P44:弹层开启只允许对话选项 1-6;E/Q/Tab/C/T 等世界交互键全部屏蔽,不穿透。
+ if m5View.Panel = DialoguePanel && pressedAny [ Keys.D1; Keys.NumPad1 ] then Some Intent1
+ elif m5View.Panel = DialoguePanel && pressedAny [ Keys.D2; Keys.NumPad2 ] then Some Intent2
+ elif m5View.Panel = DialoguePanel && pressedAny [ Keys.D3; Keys.NumPad3 ] then Some Intent3
+ elif m5View.Panel = DialoguePanel && pressedAny [ Keys.D4; Keys.NumPad4 ] then Some Intent4
+ elif m5View.Panel = DialoguePanel && pressedAny [ Keys.D5; Keys.NumPad5 ] then Some Intent5
+ elif m5View.Panel = DialoguePanel && pressedAny [ Keys.D6; Keys.NumPad6 ] then Some Intent6
+ else None
+ else
+ if M5Interaction.worldInputAllowed m5View && pressed Keys.E then Some Interact
+ elif m5View.Panel = DialoguePanel && pressedAny [ Keys.D1; Keys.NumPad1 ] then Some Intent1
+ elif m5View.Panel = DialoguePanel && pressedAny [ Keys.D2; Keys.NumPad2 ] then Some Intent2
+ elif m5View.Panel = DialoguePanel && pressedAny [ Keys.D3; Keys.NumPad3 ] then Some Intent3
+ elif m5View.Panel = DialoguePanel && pressedAny [ Keys.D4; Keys.NumPad4 ] then Some Intent4
+ elif m5View.Panel = DialoguePanel && pressedAny [ Keys.D5; Keys.NumPad5 ] then Some Intent5
+ elif m5View.Panel = DialoguePanel && pressedAny [ Keys.D6; Keys.NumPad6 ] then Some Intent6
+ elif pressed Keys.Q then Some Observe
+ elif pressed Keys.Tab then Some ToggleNeeds
+ elif pressed Keys.C then Some ShowChronicle
+ elif pressed Keys.T then Some ShowTaskPanel
+ else None
match m5Command with
| Some command ->
let nextWorld, nextView = M5Interaction.apply command world m5View
@@ -1154,6 +1246,14 @@ type LivingVillageGame() as this =
else this.DispatchMenuInput Confirm
elif menu.Page = Playing then
this.PrepareP43Shot()
+ elif p44ShotMode 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.PrepareP44Shot()
elif menu.Page = Playing then
this.UpdatePlaying gameTime kb pressed pressedAny
elif menuShotMode then
@@ -1169,6 +1269,8 @@ type LivingVillageGame() as this =
pauseHelpControl <- None
printfn $"help=pause-close simulation={M6Presentation.clockLabel simulationControl}"
| _ -> ()
+ if menu.Page = Playing then
+ this.SyncDialogOverlay()
hudClock <- hudClock + 1L
if m5View.Status <> hudStatus then
hudStatus <- m5View.Status
@@ -1557,6 +1659,8 @@ type LivingVillageGame() as this =
this.CaptureP40Shot()
if p43ShotMode && menu.Page = Playing then
this.CaptureP43Shot()
+ if p44ShotMode && menu.Page = Playing then
+ this.CaptureP44Shot()
/// 启动画面:分层天色 + 屋脊/水面剪影 + 标题淡入上浮 + 跳过提示,全部为帧计数纯函数。
member private this.DrawSplash() =
@@ -1878,14 +1982,88 @@ type LivingVillageGame() as this =
| _ -> ()
this.DrawM5Overlay()
+ /// P44:按当前面板同步对话弹层模型;内容不变时保留翻页状态,离开 DialoguePanel 即关闭。
+ member private this.SyncDialogOverlay () =
+ if m5View.Panel = DialoguePanel then
+ let lines = M5Interaction.panelLines world m5View
+ let key = String.concat "\n" lines
+ match dialogOverlay with
+ | Some _ when dialogOverlayKey = key -> ()
+ | _ ->
+ dialogOverlayKey <- key
+ let title = match lines with t :: _ -> t | [] -> "对话"
+ let body = match lines with _ :: rest -> rest | [] -> []
+ dialogOverlay <- Some(DialogOverlay.create title body (this.GraphicsDevice.Viewport.Width) true)
+ else
+ dialogOverlay <- None
+ dialogOverlayKey <- ""
+
+ /// P44:绘制完整背景框 + 原点点阵头像 + 自动换行文字 + 分页标记。
+ member private this.DrawDialogOverlay (model: DialogOverlay.Model) =
+ let viewport = this.GraphicsDevice.Viewport
+ let profile = M6Presentation.profileAtTick world.Tick
+ let palette = HudLayout.palette profile.Mode
+ let layout = DialogOverlay.rect viewport.Width viewport.Height model
+ let current = DialogOverlay.view model
+ let lx = layout.X
+ let ly = layout.Y
+ spriteBatch.Begin()
+ // 完整背景框:上下两段半透明深色渐变 + 四边细边框 + 顶部强调线。
+ spriteBatch.Draw(pixel, Rectangle(lx, ly, layout.Width, layout.Height / 2), Color(30, 44, 52, 224))
+ spriteBatch.Draw(
+ pixel,
+ Rectangle(lx, ly + layout.Height / 2, layout.Width, layout.Height - layout.Height / 2),
+ Color(14, 22, 30, 232))
+ spriteBatch.Draw(pixel, Rectangle(lx, ly, layout.Width, 3), palette.Accent)
+ spriteBatch.Draw(pixel, Rectangle(lx, ly + layout.Height - 2, layout.Width, 2), palette.Outline)
+ spriteBatch.Draw(pixel, Rectangle(lx, ly, 2, layout.Height), palette.Outline)
+ spriteBatch.Draw(pixel, Rectangle(lx + layout.Width - 2, ly, 2, layout.Height), palette.Outline)
+ // 头像:原创 8x16 点阵 @ scale 4 = 32x64,加内框。
+ if model.ShowAvatar then
+ let avatar = DialogOverlay.avatarRect layout
+ spriteBatch.Draw(pixel, Rectangle(avatar.X - 4, avatar.Y - 4, avatar.Width + 8, avatar.Height + 8), palette.SegmentBackdrop)
+ spriteBatch.Draw(pixel, Rectangle(avatar.X - 4, avatar.Y - 4, avatar.Width + 8, 2), palette.Accent)
+ spriteBatch.Draw(pixel, Rectangle(avatar.X - 4, avatar.Y + avatar.Height + 2, avatar.Width + 8, 2), palette.Outline)
+ this.DrawPattern
+ DialogOverlay.avatarPattern
+ avatar.X
+ avatar.Y
+ DialogOverlay.avatarScale
+ palette.Value
+ let textX = DialogOverlay.textOriginX layout model.ShowAvatar
+ let titleY = ly + DialogOverlay.paddingY
+ for (dx, dy) in HudLayout.textOutlineOffsets do
+ PixelText.draw spriteBatch pixel cjkAtlas (textX + dx) (titleY + dy) DialogOverlay.textScale palette.Outline model.Title
+ PixelText.draw spriteBatch pixel cjkAtlas textX titleY DialogOverlay.textScale palette.Accent model.Title
+ let bodyY = DialogOverlay.textOriginY layout
+ current.Lines
+ |> List.iteri (fun index line ->
+ let y = bodyY + index * DialogOverlay.lineHeight
+ for (dx, dy) in HudLayout.textOutlineOffsets do
+ PixelText.draw spriteBatch pixel cjkAtlas (textX + dx) (y + dy) DialogOverlay.textScale palette.Outline line
+ PixelText.draw spriteBatch pixel cjkAtlas textX y DialogOverlay.textScale palette.Value line)
+ // 分页标记:页码 + 翻页/关闭提示;有下一页时显示箭头。
+ let marker =
+ sprintf "第 %d/%d 页 空格 翻页 Esc 关闭" current.PageNumber current.PageCount
+ let markerWidth = FloatingPrompt.measureWidth marker DialogOverlay.textScale
+ let markerX = lx + layout.Width - DialogOverlay.paddingX - markerWidth
+ let markerY = ly + layout.Height - DialogOverlay.pageMarkerHeight
+ PixelText.draw spriteBatch pixel cjkAtlas markerX markerY DialogOverlay.textScale palette.Label marker
+ if current.HasNext then
+ PixelText.draw spriteBatch pixel cjkAtlas (markerX - 18) markerY DialogOverlay.textScale palette.Accent ">"
+ spriteBatch.End()
+
member private this.DrawM5Overlay() =
this.DrawHudBar()
this.DrawCalendarCard()
this.DrawHotbar()
- if m5View.Panel = WorldPanel then
- this.DrawTransientStatus()
- else
- this.DrawPanel()
+ match dialogOverlay with
+ | Some model -> this.DrawDialogOverlay model
+ | None ->
+ if m5View.Panel = WorldPanel then
+ this.DrawTransientStatus()
+ else
+ this.DrawPanel()
/// 顶部窄信息条:时段/时间/倍速/体力/钱币,半透明渐变 + 强调线,绝非大黑块。
member private this.DrawHudBar() =