diff options
Diffstat (limited to 'src/LivingVillage.Desktop/Game.fs')
| -rw-r--r-- | src/LivingVillage.Desktop/Game.fs | 210 |
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() = |
