From 3460c31b3bce6ee417e1aae95071992ba60fbf63 Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Mon, 28 Sep 2026 15:58:39 +0800 Subject: feat(p39): cc0 中式元素确定性摆放 + 夜间灯笼光晕接入 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - VillageArt: 新增 splitmix/hash2/tileIsWater 与 cc0BridgeTiles/cc0BoatTile/ cc0LanternTiles/cc0StallTiles/cc0ReedTiles,drawWorld cc0 分支叠加五类元素 - 奇偶 spacing 边界:摆放守卫 spacing<2 返回空集,消除 spacing=1 同余重叠 - nightLightTilesWith(includeRoadLanterns) + drawNightGlows 新参数; 旧 nightLightTiles 语义不变,Game.fs riverscape 分支按 cc0 开启路灯光晕 - LV_P39_SHOT 证据 hook:Update 准备、Draw 落盘(PrepareP39Shot/CaptureP39Shot) - 不写瓦片/不改可达性/不动 Sim.step --- src/LivingVillage.Desktop/Game.fs | 96 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) (limited to 'src/LivingVillage.Desktop/Game.fs') diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs index 8e58727..7b1dfb1 100644 --- a/src/LivingVillage.Desktop/Game.fs +++ b/src/LivingVillage.Desktop/Game.fs @@ -288,6 +288,13 @@ type LivingVillageGame() as this = let mutable p37Step = 0 let mutable p37Hold = 0 let mutable p37Captured = false + // P39 证据钩子:LV_P39_SHOT=1 时在真实 256x192 生成器世界里,把相机分别对准 + // 「桥+乌篷船+芦苇」与「主路石灯笼+菜摊」,拍昼/夜整帧后退出。位置来自 + // VillageArt 的确定性摆放纯函数,含真实 tint/光晕,非 mockup。 + let p39ShotMode = Environment.GetEnvironmentVariable("LV_P39_SHOT") = "1" + let mutable p39Step = 0 + let mutable p39PendingName = "" + let mutable p39Pending = false let mutable autoplayFrames = 0 let mutable flowStep = 0 let mutable flowHold = 0 @@ -618,6 +625,72 @@ type LivingVillageGame() as this = this.Exit() | _ -> () + /// P39 证据:真实生成器世界里对准 P39 中式元素拍昼/夜整帧。 + /// 相机对准「桥+船+苇」与「主路石灯笼+菜摊」两处,输出 1280x720 真实帧 + /// (含 M6Presentation 的昼/夜 tint、golden/moonlight veil 与 lantern glow)。 + /// 由于软件渲染下 Update 可能一帧内跑多次,截图统一在 `Draw` 末尾落盘(见 + /// `CaptureP39Shot`),确保落盘的是「新状态已经画进后台缓冲」的那一帧。 + member private this.PrepareP39Shot () = + let placeCameraOn (tileX: int) (tileY: int) = + world <- { world with Avatar = { world.Avatar with Pos = { X = float32 (tileX * Sim.tilePixels); Y = float32 (tileY * Sim.tilePixels) } } } + this.CenterCamera() + let setHour (hour: float) = + let tick = M6Presentation.resolveStartTick (Some hour) false + world <- { world with Tick = tick; Time = float tick * Sim.dtSeconds } + let dayHour = 12.0 + let nightHour = 23.5 + if not p39Pending then + match riverscapeMap with + | None -> this.Exit() + | Some map -> + let boat = VillageArt.cc0BoatTile map + // 取「离船最近的桥」的中点取景,保证桥/船/苇落在同一帧同一河段。 + let bridgeTarget = + match boat with + | Some (boatX, boatY) -> + let nearest = + map.Bridges + |> List.minBy (fun (bx, by) -> abs (bx - boatX) + abs (by - boatY)) + ((fst nearest + boatX) / 2, (snd nearest + boatY) / 2) + | None -> + match map.Bridges |> List.sortBy (fun (x, y) -> (x, y)) |> List.tryHead with + | Some (bx, by) -> (bx, by) + | None -> (map.Core.MinX, map.Core.MinY) + let lanternTarget = + match VillageArt.cc0LanternTiles map 12 with + | (lx, ly) :: _ -> (lx, ly) + | [] -> bridgeTarget + match p39Step with + | 0 -> + setHour dayHour + placeCameraOn (fst bridgeTarget) (snd bridgeTarget) + p39PendingName <- "p39-day" + p39Pending <- true + | 1 -> + setHour nightHour + placeCameraOn (fst bridgeTarget) (snd bridgeTarget) + p39PendingName <- "p39-night" + p39Pending <- true + | 2 -> + setHour nightHour + placeCameraOn (fst lanternTarget) (snd lanternTarget) + p39PendingName <- "p39-lantern-night" + p39Pending <- true + | _ -> + printfn "p39-shot=done" + this.Exit() + + /// P39 证据:在 `Draw` 末尾把刚画好的后台缓冲落盘(软件渲染下 Update 可能一帧多跑, + /// 只有 Draw 之后的缓冲区才是当前状态的真实帧)。 + member private this.CaptureP39Shot () = + if p39Pending then + p39Pending <- false + System.IO.Directory.CreateDirectory recordDirectory |> ignore + this.SaveBackBuffer(sprintf "%s/%s.png" recordDirectory p39PendingName) + printfn "p39-shot=%s tick=%d pos=(%.0f,%.0f) camera=(%.0f,%.0f)" + p39PendingName world.Tick world.Avatar.Pos.X world.Avatar.Pos.Y camera.X camera.Y + p39Step <- p39Step + 1 + /// P20 第三步取证:真走位巡游(Sim.step 驱动)沿桥/路/民居门移动,用于桥面行走帧与昼夜录制。 member private this.RunRiverscapeTour () = if not riverscapeTourWatchStarted then @@ -916,6 +989,15 @@ type LivingVillageGame() as this = this.RunStoryShotSetup() elif autoplayFlow then this.RunAutoplayFlow() + elif p39ShotMode 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.PrepareP39Shot() elif p37ShotMode then this.RunP37Shot() elif menu.Page = Playing then @@ -1295,6 +1377,8 @@ type LivingVillageGame() as this = this.Exit() if p36ShotMode && menu.Page = Playing && not relationView then this.CaptureP36PromptShot() + if p39ShotMode && menu.Page = Playing then + this.CaptureP39Shot() /// 启动画面:分层天色 + 屋脊/水面剪影 + 标题淡入上浮 + 跳过提示,全部为帧计数纯函数。 member private this.DrawSplash() = @@ -1509,7 +1593,17 @@ type LivingVillageGame() as this = let tileY y = y * Sim.tilePixels - int camera.Y match riverscapeMap with | Some map -> - VillageArt.drawNightGlows spriteBatch pixel map (4 * Sim.tilePixels) (int (150.0f * glow)) tileX tileY Sim.tilePixels + // P39:cc0 模式下把主路石灯笼并入夜光;fallback 保持原灯光源。 + VillageArt.drawNightGlows + spriteBatch + pixel + map + (4 * Sim.tilePixels) + (int (150.0f * glow)) + tileX + tileY + Sim.tilePixels + artTextures.Cc0TileAtlas.IsSome | None -> for prop in renderPlan.Props do match prop.Kind with -- cgit v1.2.3