summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-28 19:29:27 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-28 19:29:27 +0800
commite28b3c102f2a0b387a4df84f053306d71e5e41ba (patch)
tree0b3b13b10bba4283dde3aba60cc0ec42495d6b82 /src/LivingVillage.Desktop
parentd671335d687496f393dbad63950707cabbf24e5b (diff)
downloadliving-village-e28b3c102f2a0b387a4df84f053306d71e5e41ba.tar.gz
p44: 对话框/文字弹层 — 完整背景框+中文断行换行+分页翻页+Esc关闭+输入隔离(原创32x64点阵头像)
Diffstat (limited to 'src/LivingVillage.Desktop')
-rw-r--r--src/LivingVillage.Desktop/DialogOverlay.fs193
-rw-r--r--src/LivingVillage.Desktop/Game.fs210
-rw-r--r--src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj1
3 files changed, 388 insertions, 16 deletions
diff --git a/src/LivingVillage.Desktop/DialogOverlay.fs b/src/LivingVillage.Desktop/DialogOverlay.fs
new file mode 100644
index 0000000..58ce33e
--- /dev/null
+++ b/src/LivingVillage.Desktop/DialogOverlay.fs
@@ -0,0 +1,193 @@
+namespace LivingVillage.Desktop
+
+open Microsoft.Xna.Framework
+
+/// P44 对话框/文字弹层:纯模型组件。
+/// 负责完整背景框几何、中文正确断行的自动换行、分页(下一页)与关闭语义、
+/// 开启时对世界交互的隔离判定,以及原创 32x64 点阵头像。
+/// 只做确定性纯计算,实际绘制在 Game.fs。
+module DialogOverlay =
+
+ let textScale = 2
+ let lineHeight = 18
+ let linesPerPage = 4
+ let paddingX = 16
+ let paddingY = 12
+ let titleGap = 6
+ let avatarWidth = 32
+ let avatarHeight = 64
+ let avatarGap = 12
+ let pageMarkerHeight = 20
+ let minFrameWidth = 480
+ let maxFrameWidth = 900
+ let bottomMargin = 24
+
+ type Layout =
+ { X: int
+ Y: int
+ Width: int
+ Height: int }
+
+ type Model =
+ { Title: string
+ Pages: string list list
+ Page: int
+ ShowAvatar: bool }
+
+ type View =
+ { Lines: string list
+ PageNumber: int
+ PageCount: int
+ HasPrev: bool
+ HasNext: bool }
+
+ // ---- 中文正确断行 ----
+
+ let private isWide (c: char) : bool = (CjkGlyphAtlas.sourceRectangle c).IsSome
+
+ /// 分词:CJK/可见字形单字成词,空格单独成词,拉丁/数字连续成词。
+ /// 这样中文可在任意字间断行,拉丁词不会被从中间劈开。
+ let private tokenize (paragraph: string) : string list =
+ let tokens = System.Collections.Generic.List<string>()
+ let buffer = System.Text.StringBuilder()
+ let flush () =
+ if buffer.Length > 0 then
+ tokens.Add(buffer.ToString())
+ buffer.Clear() |> ignore
+ for c in paragraph do
+ if isWide c then
+ flush ()
+ tokens.Add(string c)
+ elif c = ' ' then
+ flush ()
+ tokens.Add " "
+ else
+ buffer.Append(c) |> ignore
+ flush ()
+ List.ofSeq tokens
+
+ /// 把一段文字按像素宽度换行;显式 \n 强制换行。返回行列表。
+ let wrapText (maxWidth: int) (text: string) : string list =
+ let maxWidth = max 1 maxWidth
+ text.Split('\n')
+ |> Array.toList
+ |> List.collect (fun paragraph ->
+ let lines = System.Collections.Generic.List<string>()
+ let current = System.Text.StringBuilder()
+ let mutable currentWidth = 0
+ let flushLine () =
+ lines.Add(current.ToString().TrimEnd([| ' ' |]))
+ current.Clear() |> ignore
+ currentWidth <- 0
+ for token in tokenize paragraph do
+ let tokenWidth = FloatingPrompt.measureWidth token textScale
+ if current.Length > 0 && currentWidth + tokenWidth > maxWidth then
+ flushLine ()
+ if not (token = " " && current.Length = 0) then
+ current.Append(token) |> ignore
+ currentWidth <- currentWidth + tokenWidth
+ if current.Length > 0 then flushLine ()
+ if lines.Count = 0 then [ "" ] else List.ofSeq lines)
+
+ // ---- 分页 ----
+
+ let frameWidth (viewportWidth: int) : int =
+ max minFrameWidth (min maxFrameWidth (viewportWidth - 64))
+
+ let textWidth (viewportWidth: int) (showAvatar: bool) : int =
+ frameWidth viewportWidth - 2 * paddingX - (if showAvatar then avatarWidth + avatarGap else 0)
+
+ let create (title: string) (texts: string list) (viewportWidth: int) (showAvatar: bool) : Model =
+ let maxWidth = textWidth viewportWidth showAvatar
+ let body = texts |> List.collect (wrapText maxWidth)
+ let pages = if body.IsEmpty then [ [] ] else List.chunkBySize linesPerPage body
+ { Title = title
+ Pages = pages
+ Page = 0
+ ShowAvatar = showAvatar }
+
+ let pageCount (model: Model) : int = max 1 model.Pages.Length
+
+ let private clampPage (model: Model) : int =
+ if model.Pages.IsEmpty then 0 else max 0 (min (model.Pages.Length - 1) model.Page)
+
+ let view (model: Model) : View =
+ let page = clampPage model
+ let total = pageCount model
+ { Lines = (if model.Pages.IsEmpty then [] else model.Pages.[page])
+ PageNumber = page + 1
+ PageCount = total
+ HasPrev = page > 0
+ HasNext = page < total - 1 }
+
+ /// 下一页;已在最后一页时返回 None,调用方保持当前页(翻页边界不崩)。
+ let nextPage (model: Model) : Model option =
+ let page = clampPage model
+ if page < pageCount model - 1 then Some { model with Page = page + 1 } else None
+
+ /// 上一页;已在第一页时返回 None。
+ let prevPage (model: Model) : Model option =
+ let page = clampPage model
+ if page > 0 then Some { model with Page = page - 1 } else None
+
+ // ---- 关闭与隔离 ----
+
+ /// 关闭当前弹层。
+ let close (_: Model option) : Model option = None
+
+ /// 弹层开启时禁止任何世界交互穿透。
+ let blocksWorldInteraction (overlayOpen: bool) : bool = overlayOpen
+
+ /// 世界交互提示在弹层开启时隐藏。
+ let promptVisible (overlayOpen: bool) : bool = not overlayOpen
+
+ // ---- 几何 ----
+
+ let private headerHeight = 8 * textScale + titleGap
+
+ let rect (viewportWidth: int) (viewportHeight: int) (model: Model) : Layout =
+ let current = view model
+ let width = frameWidth viewportWidth
+ let bodyLines = max 1 current.Lines.Length
+ let bodyHeight = bodyLines * lineHeight
+ let contentHeight = headerHeight + max bodyHeight (if model.ShowAvatar then avatarHeight else 0)
+ let height = contentHeight + 2 * paddingY + pageMarkerHeight
+ { X = (viewportWidth - width) / 2
+ Y = viewportHeight - height - bottomMargin
+ Width = width
+ Height = height }
+
+ let textOriginX (layout: Layout) (showAvatar: bool) : int =
+ layout.X + paddingX + (if showAvatar then avatarWidth + avatarGap else 0)
+
+ let textOriginY (layout: Layout) : int = layout.Y + paddingY + headerHeight
+
+ let avatarRect (layout: Layout) : Layout =
+ { X = layout.X + paddingX
+ Y = layout.Y + paddingY + headerHeight
+ Width = avatarWidth
+ Height = avatarHeight }
+
+ // ---- 原创 32x64 点阵头像(8x16 @ scale 4)----
+
+ let avatarColumns = 8
+ let avatarRows = 16
+ let avatarScale = 4
+
+ let avatarPattern : string list =
+ [ "00000000"
+ "00111100"
+ "01111110"
+ "01111110"
+ "01111110"
+ "00111100"
+ "00011000"
+ "00011000"
+ "00111100"
+ "01111110"
+ "11111111"
+ "11111111"
+ "11111111"
+ "11011011"
+ "10000001"
+ "00000000" ]
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() =
diff --git a/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj b/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj
index 2901dce..76e9b06 100644
--- a/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj
+++ b/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj
@@ -19,6 +19,7 @@
<Compile Include="SampleScript.fs" />
<Compile Include="M6Presentation.fs" />
<Compile Include="HudLayout.fs" />
+ <Compile Include="DialogOverlay.fs" />
<Compile Include="PerformanceSummary.fs" />
<Compile Include="TitleScreen.fs" />