diff options
Diffstat (limited to 'src/LivingVillage.Desktop/Game.fs')
| -rw-r--r-- | src/LivingVillage.Desktop/Game.fs | 257 |
1 files changed, 180 insertions, 77 deletions
diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs index 1b9508b..3f3a1d8 100644 --- a/src/LivingVillage.Desktop/Game.fs +++ b/src/LivingVillage.Desktop/Game.fs @@ -97,6 +97,7 @@ type LivingVillageGame() as this = let autoplay = (Environment.GetEnvironmentVariable("LV_AUTOPLAY") = "1") || (Environment.GetEnvironmentVariable("LV_MAP_TOUR") = "1") let autoplayFlow = Environment.GetEnvironmentVariable("LV_AUTOPLAY_FLOW") = "1" let sampleMode = Environment.GetEnvironmentVariable("LV_AUTOPLAY_SAMPLE") = "1" + let menuShotMode = Environment.GetEnvironmentVariable("LV_AUTOPLAY_MENU_SHOT") = "1" let daylightAutoplay = Environment.GetEnvironmentVariable("LV_AUTOPLAY_DAYLIGHT") = "1" let automated = autoplay || autoplayFlow let modeName = @@ -140,6 +141,10 @@ type LivingVillageGame() as this = let mutable simulationControl = startSimulationControl let mutable legacyStepsPerFrame = (if sampleMode then None else initialLegacyStepsPerFrame) let mutable menu = MenuState.create startSimulationControl false + let mutable splashActive = true + let mutable splashFrame = 0L + let mutable menuEntranceFrame = 0L + let mutable menuShotFrame = 0L let mutable autoplayFrames = 0 let mutable flowStep = 0 let mutable flowHold = 0 @@ -313,6 +318,12 @@ type LivingVillageGame() as this = printfn (if nextState.TimedOut then "sample result=timeout" else "sample result=ok") this.Exit() + /// P16-FIX:菜单截图模式。停在开始界面,按确定性帧表向下切到「读档」,帧表见 MenuShotScript。 + member private this.RunMenuShot () = + menuShotFrame <- menuShotFrame + 1L + if menuShotFrame = MenuShotScript.downFrame then + this.DispatchMenuInput Down + member private this.RunMapTour () = let (tileX, tileY) = tourWaypoints.[tourIndex % tourWaypoints.Length] let position : Vec2 = @@ -481,34 +492,21 @@ type LivingVillageGame() as this = printfn "flow=return-menu" flowStep <- 2 | 2 when menu.Page = MainMenu -> - moveDown 3 - this.DispatchMenuInput Confirm - printfn "flow=settings=open" - this.DispatchMenuInput Down + moveDown 1 this.DispatchMenuInput Confirm - this.DispatchMenuInput Back - printfn "flow=settings=back" + printfn "flow=load" flowStep <- 3 - | 3 when menu.Page = MainMenu -> - moveDown 4 - this.DispatchMenuInput Confirm - printfn "flow=controls=open" - flowStep <- 30 - | 30 -> - flowHold <- flowHold + 1 - if flowHold >= 60 || menu.Page <> Controls then - flowHold <- 0 - flowStep <- 31 - | 31 -> + | 3 -> this.DispatchMenuInput Back - printfn "flow=controls=back" + printfn "flow=back" flowStep <- 4 | 4 when menu.Page = MainMenu -> - this.DispatchMenuInput Confirm this.SaveWorld "flow=save" |> ignore flowStep <- 5 - | 5 when menu.Page = Playing -> - this.LoadWorld "flow=load" |> ignore + | 5 when menu.Page = MainMenu -> + moveDown 1 + this.DispatchMenuInput Confirm + printfn "flow=load-again" flowStep <- 6 | 6 -> this.OpenMainMenu() @@ -540,12 +538,24 @@ type LivingVillageGame() as this = let kb = Keyboard.GetState() let pressed (key: Keys) = kb.IsKeyDown(key) && not (prevKb.IsKeyDown(key)) let pressedAny (keys: Keys list) = keys |> List.exists pressed - if autoplayFlow then - this.RunAutoplayFlow() - elif menu.Page = Playing then - this.UpdatePlaying gameTime kb pressed pressedAny + if splashActive then + splashFrame <- splashFrame + 1L + let skip = LaunchScreen.skipRequested (pressed Keys.Escape) (pressed Keys.Space) + if skip || LaunchScreen.isComplete splashFrame then + splashActive <- false + menuEntranceFrame <- 0L + printfn (if skip then "splash=skip frame=%d" else "splash=done frame=%d") splashFrame else - this.UpdateMenu pressed + if autoplayFlow then + this.RunAutoplayFlow() + elif menu.Page = Playing then + this.UpdatePlaying gameTime kb pressed pressedAny + elif menuShotMode then + this.RunMenuShot() + else + this.UpdateMenu pressed + if menu.Page <> Playing then + menuEntranceFrame <- menuEntranceFrame + 1L match pauseHelpControl with | Some original when menu.Page = Playing -> let _, restoredControl = MenuState.exitPauseHelp original menu @@ -605,8 +615,37 @@ type LivingVillageGame() as this = | None -> () spriteBatch.End() + /// 将当前后台缓冲存为 PNG(取证用;record 与菜单截图共用)。 + member private this.SaveBackBuffer (path: string) = + let width = this.GraphicsDevice.PresentationParameters.BackBufferWidth + let height = this.GraphicsDevice.PresentationParameters.BackBufferHeight + let buffer = Array.zeroCreate<Color> (width * height) + this.GraphicsDevice.GetBackBufferData<Color>(buffer) + use texture = new Texture2D(this.GraphicsDevice, width, height) + texture.SetData(buffer) + use stream = System.IO.File.Create(path) + texture.SaveAsPng(stream, width, height) + + /// P16-FIX:按 MenuShotScript 帧表落菜单/启动画面截图,拍完即退出。 + member private this.CaptureMenuShot () = + let save name = + System.IO.Directory.CreateDirectory recordDirectory |> ignore + this.SaveBackBuffer(sprintf "%s/%s.png" recordDirectory name) + if splashActive then + if splashFrame = MenuShotScript.splashMidFrame then + save MenuShotScript.splashMidName + elif menuShotFrame = MenuShotScript.initialFrame then + save MenuShotScript.initialName + elif menuShotFrame = MenuShotScript.loadFrame then + save MenuShotScript.loadName + elif menuShotFrame = MenuShotScript.exitFrame then + printfn "menu-shot=done frames=%d" menuShotFrame + this.Exit() + override this.Draw(gameTime: GameTime) = - if menu.Page <> Playing then + if splashActive then + this.DrawSplash() + elif menu.Page <> Playing then this.DrawMenu() elif relationView then this.DrawRelationView() @@ -616,18 +655,62 @@ type LivingVillageGame() as this = if recordMode && sampleMode then recordIndex <- recordIndex + 1 if recordIndex % recordEvery = 0 then - let directory = recordDirectory - System.IO.Directory.CreateDirectory directory |> ignore - let width, height = - this.GraphicsDevice.PresentationParameters.BackBufferWidth, - this.GraphicsDevice.PresentationParameters.BackBufferHeight - let buffer = Array.zeroCreate<Color> (width * height) - this.GraphicsDevice.GetBackBufferData<Color>(buffer) - use texture = new Texture2D(this.GraphicsDevice, width, height) - texture.SetData(buffer) - let path = sprintf "%s/rec-frame-%04d.png" directory recordIndex - use stream = System.IO.File.Create(path) - texture.SaveAsPng(stream, width, height) + System.IO.Directory.CreateDirectory recordDirectory |> ignore + this.SaveBackBuffer(sprintf "%s/rec-frame-%04d.png" recordDirectory recordIndex) + if menuShotMode then + this.CaptureMenuShot() + + /// 启动画面:分层天色 + 屋脊/水面剪影 + 标题淡入上浮 + 跳过提示,全部为帧计数纯函数。 + member private this.DrawSplash() = + let viewport = this.GraphicsDevice.Viewport + let width = viewport.Width + let height = viewport.Height + let layout = TitleScreen.layout width height + let reveal = LaunchScreen.revealProgress splashFrame + let veil = LaunchScreen.veilAlpha splashFrame + let alpha (value: int) = int (float32 value * reveal) + spriteBatch.Begin() + + // 分层天色:沿用标题画面色带,绝不整屏纯色或大黑块。 + let bandCount = List.length layout.SkyBands + let bandHeight = height / bandCount + List.iteri (fun index (band: TitleScreen.ColorRgb) -> + let y0 = index * bandHeight + let y1 = if index = bandCount - 1 then height else y0 + bandHeight + spriteBatch.Draw(pixel, Rectangle(0, y0, width, y1 - y0), Color(band.R, band.G, band.B))) layout.SkyBands + + // 屋脊剪影随暗幕退场而上浮就位。 + let drop = int (28.0f * veil) + for x in 0 .. Sim.tilePixels .. width - 1 do + spriteBatch.Draw(artTextures.TileAtlas, Rectangle(x, layout.RidgeY + drop, Sim.tilePixels, Sim.tilePixels), VillageArt.atlasTileRectangle (fst TitleScreen.roofSlots), Color.White) + spriteBatch.Draw(artTextures.TileAtlas, Rectangle(x, layout.EaveY + drop, Sim.tilePixels, Sim.tilePixels), VillageArt.atlasTileRectangle (snd TitleScreen.roofSlots), Color.White) + + // 水面色带反射天色,随显现度提高而淡出。 + let waterColor = Color(150, 170, 178, int (255.0f * (1.0f - reveal))) + for row in layout.WaterRows do + for x in 0 .. Sim.tilePixels .. width - 1 do + spriteBatch.Draw(artTextures.TileAtlas, Rectangle(x, row, Sim.tilePixels, Sim.tilePixels), VillageArt.atlasTileRectangle TitleScreen.waterSlot, waterColor) + + // 标题:淡入 + 上浮,先画深色投影再画亮面。 + let titleRise = int (24.0f * (1.0f - reveal)) + PixelText.draw spriteBatch pixel cjkAtlas (layout.TitleX + 4) (layout.TitleY + titleRise + 4) layout.TitleScale (Color(12, 20, 28, alpha 255)) LaunchScreen.title + PixelText.draw spriteBatch pixel cjkAtlas layout.TitleX (layout.TitleY + titleRise) layout.TitleScale (Color(235, 246, 232, alpha 255)) LaunchScreen.title + + // 英文副标题保持同一 CJK/点阵字体路径。 + let subtitleScale = 2 + let subtitleWidth = FloatingPrompt.measureWidth LaunchScreen.subtitle subtitleScale + PixelText.draw spriteBatch pixel cjkAtlas ((width - subtitleWidth) / 2) (layout.TitleY + 78) subtitleScale (Color(180, 210, 214, alpha 255)) LaunchScreen.subtitle + + // 跳过提示:轻微呼吸闪烁,落在底部独立窄条上(非整屏黑块)。 + let blink = if (splashFrame / 20L) % 2L = 0L then 220 else 130 + let hintScale = 2 + let hintWidth = FloatingPrompt.measureWidth LaunchScreen.skipHint hintScale + let hintX = (width - hintWidth) / 2 + let hintY = height - 56 + spriteBatch.Draw(pixel, Rectangle(hintX - 10, hintY - 8, hintWidth + 20, 8 * hintScale + 16), Color(10, 16, 24, int (170.0f * (0.5f + 0.5f * reveal)))) + spriteBatch.Draw(pixel, Rectangle(hintX - 10, hintY - 8, hintWidth + 20, 2), Color(92, 180, 190, alpha 220)) + PixelText.draw spriteBatch pixel cjkAtlas hintX hintY hintScale (Color(220, 235, 225, alpha blink)) LaunchScreen.skipHint + spriteBatch.End() member private this.DrawMenu() = let viewport = this.GraphicsDevice.Viewport @@ -678,7 +761,7 @@ type LivingVillageGame() as this = spriteBatch.Draw(pixel, Rectangle(0, y0, width, y1 - y0), Color(band.R, band.G, band.B))) layout.SkyBands // Deterministic 2s entrance: skyline settles from above as the progress unwinds. - let entrance = TitleScreen.entranceProgress hudClock + let entrance = TitleScreen.entranceProgress menuEntranceFrame let skylineDrop = int (36.0f * entrance) List.iteri (fun column slot -> let x = column * Sim.tilePixels @@ -810,52 +893,72 @@ type LivingVillageGame() as this = this.DrawM5Overlay() member private this.DrawM5Overlay() = + this.DrawHudBar() + if m5View.Panel = WorldPanel then + this.DrawTransientStatus() + else + this.DrawPanel() + + /// 顶部窄信息条:时段/时间/倍速/体力/钱币,半透明渐变 + 强调线,绝非大黑块。 + member private this.DrawHudBar() = let viewport = this.GraphicsDevice.Viewport - let textScale = 2 - let lineHeight = 18 let profile = M6Presentation.profileAtTick world.Tick - let timeLine = sprintf "时间 %s %s" (M6Presentation.dayNightLabel profile.Mode) (M6Presentation.clockLabel simulationControl) - let persistentLines = - timeLine :: (m5View.Prompt |> Option.toList) - let dialogueLines = - match m5View.Panel, m5View.Menu with - | DialoguePanel, Some menu -> - menu.Options - |> List.mapi (fun index intent -> sprintf "%d %s" (index + 1) (M5Interaction.intentLine intent)) - |> fun options -> [ sprintf "互动:村民 %d" (M5Interaction.menuNpcNumber menu) ] @ options - | _ -> [] + let palette = HudLayout.palette profile.Mode + let placed = HudLayout.layout (HudLayout.segments world simulationControl) + let barHeight = HudLayout.barHeight + let textY = (barHeight - 8 * HudLayout.textScale) / 2 + spriteBatch.Begin() + // 竖向渐变:上半略亮、下半压暗,避免整条纯色。 + spriteBatch.Draw(pixel, Rectangle(0, 0, viewport.Width, barHeight / 2), palette.BarTop) + spriteBatch.Draw(pixel, Rectangle(0, barHeight / 2, viewport.Width, barHeight - barHeight / 2), palette.BarBottom) + spriteBatch.Draw(pixel, Rectangle(0, barHeight, viewport.Width, HudLayout.accentHeight), palette.Accent) + for (x, _, segment) in placed do + PixelText.draw spriteBatch pixel cjkAtlas x textY HudLayout.textScale palette.Label segment.Label + let labelAdvanced = FloatingPrompt.measureWidth (segment.Label + " ") HudLayout.textScale + PixelText.draw spriteBatch pixel cjkAtlas (x + labelAdvanced) textY HudLayout.textScale palette.Value segment.Value + spriteBatch.End() + + /// 常驻情境状态:顶部条下方的小药丸,仅 WorldPanel 且瞬态未过期时出现。 + member private this.DrawTransientStatus() = + let viewport = this.GraphicsDevice.Viewport let statusView = { m5View with StatusTick = Some hudStatusAt } - let statusLine = M5Interaction.transientStatusLine hudClock statusView |> Option.toList - let allLines = persistentLines @ dialogueLines @ statusLine - let cornerLines = M5Interaction.cornerStatusLines world - let panelWidth = min 360 (max 1 (viewport.Width - 32)) - let maxLines = max 1 ((viewport.Height - 48) / lineHeight) - let lines = allLines |> List.truncate maxLines - let panelHeight = lines.Length * lineHeight + 24 + match M5Interaction.transientStatusLine hudClock statusView with + | None -> () + | Some line -> + let scale = 2 + let width = FloatingPrompt.measureWidth line scale + let x = 16 + let y = HudLayout.barHeight + HudLayout.accentHeight + 8 + spriteBatch.Begin() + spriteBatch.Draw(pixel, Rectangle(x - 8, y - 6, width + 16, 8 * scale + 12), Color(16, 26, 32, 200)) + spriteBatch.Draw(pixel, Rectangle(x - 8, y - 6, 2, 8 * scale + 12), Color(92, 180, 190)) + PixelText.draw spriteBatch pixel cjkAtlas x y scale (Color(232, 242, 232)) line + spriteBatch.End() + + /// 打开的面板(对话/需求/观察/年鉴/任务):底部左侧轻量渐变板,沿用既有 panelLines。 + member private this.DrawPanel() = + let viewport = this.GraphicsDevice.Viewport + let textScale = 2 + let lineHeight = 18 + let lines = M5Interaction.panelLines world m5View + let panelWidth = min 380 (max 1 (viewport.Width - 32)) + let maxLines = max 1 ((viewport.Height - HudLayout.barHeight - 48) / lineHeight) + let visibleLines = lines |> List.truncate maxLines + let panelHeight = visibleLines.Length * lineHeight + 26 let panelX = 16 - let panelY = max 16 (viewport.Height - panelHeight - 16) - let panel = Rectangle(panelX, panelY, panelWidth, panelHeight) + let panelY = max (HudLayout.barHeight + 12) (viewport.Height - panelHeight - 16) let innerX = panelX + 12 let innerWidth = max 1 (panelWidth - 24) let maxChars = max 1 (innerWidth / (6 * textScale)) spriteBatch.Begin() - spriteBatch.Draw(pixel, panel, Color(15, 19, 25, 160)) + // 竖向渐变板(半透明,非纯黑),顶部与左侧各一条强调线。 + spriteBatch.Draw(pixel, Rectangle(panelX, panelY, panelWidth, panelHeight / 2), Color(30, 44, 52, 190)) + spriteBatch.Draw(pixel, Rectangle(panelX, panelY + panelHeight / 2, panelWidth, panelHeight - panelHeight / 2), Color(16, 24, 30, 190)) spriteBatch.Draw(pixel, Rectangle(panelX, panelY, panelWidth, 3), Color(92, 180, 190)) - lines - |> List.iteri (fun index line -> - let visible = if line.Length > maxChars then line.Substring(0, maxChars) else line - let color = if index = 0 then Color(220, 235, 220) else Color(190, 205, 210) - PixelText.draw spriteBatch pixel cjkAtlas innerX (panelY + 12 + index * lineHeight) textScale color visible) - - // Minimal top-right status strip: day counter + real money/energy values. - let cornerWidth = 232 - let cornerHeight = List.length cornerLines * lineHeight + 20 - let cornerX = viewport.Width - cornerWidth - 16 - let cornerY = 16 - spriteBatch.Draw(pixel, Rectangle(cornerX, cornerY, cornerWidth, cornerHeight), Color(15, 19, 25, 150)) - spriteBatch.Draw(pixel, Rectangle(cornerX, cornerY, cornerWidth, 3), Color(92, 180, 190)) - cornerLines + spriteBatch.Draw(pixel, Rectangle(panelX, panelY, 2, panelHeight), Color(92, 180, 190, 160)) + visibleLines |> List.iteri (fun index line -> let visible = if line.Length > maxChars then line.Substring(0, maxChars) else line - PixelText.draw spriteBatch pixel cjkAtlas (cornerX + 10) (cornerY + 10 + index * lineHeight) textScale (Color(220, 235, 220)) visible) + let color = if index = 0 then Color(226, 238, 226) else Color(192, 208, 212) + PixelText.draw spriteBatch pixel cjkAtlas innerX (panelY + 13 + index * lineHeight) textScale color visible) spriteBatch.End() |
