diff options
Diffstat (limited to 'src/LivingVillage.Desktop')
| -rw-r--r-- | src/LivingVillage.Desktop/Game.fs | 96 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/VillageArt.fs | 163 |
2 files changed, 250 insertions, 9 deletions
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 diff --git a/src/LivingVillage.Desktop/VillageArt.fs b/src/LivingVillage.Desktop/VillageArt.fs index 80eed07..e08cde4 100644 --- a/src/LivingVillage.Desktop/VillageArt.fs +++ b/src/LivingVillage.Desktop/VillageArt.fs @@ -556,7 +556,7 @@ module VillageArt = // P34 structure slice: indices into cc0-tiles.png. Kenney Tiny Farm ships no // water / roof / wall / railing / door art, so indices 4-11 are self-drawn // (provenance in docs/素材来源.md), laid out by this fixed contract. - let cc0TileCount = 12 + let cc0TileCount = 19 let cc0TileIndexWaterA = 4 let cc0TileIndexWaterB = 5 let cc0TileIndexRoofRidge = 6 @@ -565,6 +565,15 @@ module VillageArt = let cc0TileIndexWallWindow = 9 let cc0TileIndexRailing = 10 let cc0TileIndexDoor = 11 + // P39 appended Jiangnan elements (indices 12-18) keep every P34 index above + // unchanged (backwards compatible); provenance in docs/素材来源.md. + let cc0TileIndexBridgeCenter = 12 + let cc0TileIndexBridgeEnd = 13 + let cc0TileIndexBoatLeft = 14 + let cc0TileIndexBoatRight = 15 + let cc0TileIndexStoneLantern = 16 + let cc0TileIndexStall = 17 + let cc0TileIndexReeds = 18 let cc0WaterFrameCount = 2 let cc0TileAtlasWidth = Sim.tilePixels * cc0TileCount let cc0TileAtlasHeight = Sim.tilePixels @@ -591,6 +600,110 @@ module VillageArt = let cc0TileRect (index: int) : XnaRectangle = XnaRectangle(index * Sim.tilePixels, 0, Sim.tilePixels, Sim.tilePixels) + // --- P39 CC0 element placement (deterministic pure functions) --------------- + // All placements are functions of (map, seed) only — no clock, no randomness — + // so the same seed yields byte-identical bridge/boat/lantern/stall/reed + // positions across day and night frames. They only paint visuals; no tile is + // written and no reachability is changed. + + /// Deterministic splitmix64 stream, mirroring MapGen's. Used to pick one boat + /// tile and to thin the roadside lantern/stall set without touching the kernel. + let private splitmix (state: uint64) : uint64 * uint64 = + let nextState = state + 0x9E3779B97F4A7C15UL + let mutable z = nextState + z <- (z ^^^ (z >>> 30)) * 0xBF58476D1CE4E5B9UL + z <- z ^^^ (z >>> 27) + z <- z * 0x94D049BB133111EBUL + z <- z ^^^ (z >>> 31) + (nextState, z) + + let private hash2 (x: int) (y: int) (seed: uint64) : uint64 = + snd (splitmix (seed ^^^ ((uint64 x * 0x4D3B9UL) + (uint64 y * 0x1D5UL)))) + + let private tileIsWater (map: MapGen.Result) (x: int) (y: int) : bool = + x >= 0 && x < map.Width && y >= 0 && y < map.Height + && map.Tiles.[y * map.Width + x] = int MapGen.GroundTile.Water + + /// P39 桥:每座桥的首列顶部一格铺 `bridge centre`,其后一格铺 `bridge end` + /// (上桥台阶)。沿用既有 `MapGen.Bridges`(桥面瓦片),只叠加装饰,不改通行。 + let cc0BridgeTiles (map: MapGen.Result) : (int * int * int) list = + map.Bridges + |> List.groupBy fst + |> List.sortBy fst + |> List.collect (fun (x, entries) -> + let ys = entries |> List.map snd |> List.sort + match ys with + | top :: _ -> [ (x, top, cc0TileIndexBridgeCenter); (x, top - 1, cc0TileIndexBridgeEnd) ] + | [] -> []) + + /// P39 乌篷船:在开阔水面确定性泊一艘 2 格船(tileX, tileY 为左半格)。 + /// 候选为「该格及其右邻皆为水且非桥面/桥台」,优先泊在距某座桥最近处 + /// (画面里桥、船、苇常同框),距离平局按 hash 裁决,保证同 seed 逐字节不变。 + let cc0BoatTile (map: MapGen.Result) : (int * int) option = + let bridgeSet = + map.Bridges |> List.map (fun (x, y) -> y * map.Width + x) |> Set.ofList + let isBridge x y = + x >= 0 && x < map.Width && y >= 0 && y < map.Height && Set.contains (y * map.Width + x) bridgeSet + let isOpen x y = + tileIsWater map x y + && tileIsWater map (x + 1) y + && not (isBridge x y) + && not (isBridge (x + 1) y) + && not (isBridge (x - 1) y) + let candidates = + [ for y in 0 .. map.Height - 1 do + for x in 1 .. map.Width - 3 do + if isOpen x y then yield (x, y) ] + let bridges = map.Bridges + let distanceToNearestBridge (x: int) (y: int) = + bridges + |> List.map (fun (bx, by) -> abs (bx - x) + abs (by - y)) + |> List.fold min System.Int32.MaxValue + match candidates with + | [] -> None + | _ -> + candidates + |> List.sortBy (fun (x, y) -> distanceToNearestBridge x y, hash2 x y 0x39B0A7UL) + |> List.tryHead + + /// P39 灯笼:沿主路每隔 `spacing` 列确定性取一格放石灯笼(避开水面)。 + /// `MainRoadCenter.[x]` 给出行 y,主路足够长,同 seed 间距恒定。 + /// 守卫 `spacing >= 2`:`spacing = 1` 时摊位偏移 `spacing/2 = 0` 会与灯笼同余而重叠, + /// 故视为退化输入直接返回空集(见 `cc0StallTiles` 与 P39ArtTests 的奇偶/退化用例)。 + let cc0LanternTiles (map: MapGen.Result) (spacing: int) : (int * int) list = + if map.Width <= 0 || spacing < 2 then + [] + else + [ for x in 1 .. map.Width - 2 do + if x % spacing = 0 then + let y = map.MainRoadCenter.[x] + if not (tileIsWater map x y) then yield (x, y) ] + + /// P39 路摊:在主路灯笼之间确定性取一格放菜摊(与灯笼错开 `spacing/2`)。 + /// 守卫 `spacing >= 2`:`spacing = 1` 时 `spacing/2 = 0` 与灯笼同余,会与灯笼重叠, + /// 故退化输入返回空集;`spacing >= 2` 时灯笼余数 0、摊位余数 `spacing/2 ∈ [1, spacing-1]`, + /// 两同余类天然互斥(奇偶皆成立,见单测)。 + let cc0StallTiles (map: MapGen.Result) (spacing: int) : (int * int) list = + if map.Width <= 0 || spacing < 2 then + [] + else + [ for x in 1 .. map.Width - 2 do + if x % spacing = spacing / 2 then + let y = map.MainRoadCenter.[x] + if not (tileIsWater map x y) then yield (x, y) ] + + /// P39 芦苇:沿岸线铺设——陆地格且其上下任一邻格为水(贴水岸),每隔 `spacing` 列取一格。 + let cc0ReedTiles (map: MapGen.Result) (spacing: int) : (int * int) list = + if map.Width <= 0 || spacing <= 0 then + [] + else + [ for y in 1 .. map.Height - 2 do + for x in 0 .. map.Width - 1 do + if x % spacing = 0 + && not (tileIsWater map x y) + && (tileIsWater map x (y - 1) || tileIsWater map x (y + 1)) then + yield (x, y) ] + /// Ground atlas selection: `Some cc0-tiles.png` when the pack is on, else `None` /// so the renderer keeps its procedural Jiangnan ground as the fallback. let selectedGroundAtlasRelativePath () : string option = @@ -768,8 +881,11 @@ module VillageArt = /// P29 夜景灯光源瓦片:桥头灯笼 + 民居门窗暖光(确定性纯函数,便于单测)。 /// 每户至少门格发光;变体 A 再取第一扇横向窗,变体 B(整面格栅窗)与门同格故不另加。 /// P30:散落农舍的门窗暖光一并计入。 - let nightLightTiles (map: MapGen.Result) : (int * int) list = + /// P39:`includeRoadLanterns` 为真时把主路石灯笼(`cc0LanternTiles`)计入,使夜帧灯笼 + /// 光晕复用同一光晕路径;仅 cc0 模式为真,故 fallback(江南程序图集)不受影响。 + let nightLightTilesWith (includeRoadLanterns: bool) (map: MapGen.Result) : (int * int) list = let lanterns = bridgeLanternTiles map + let roadLanterns = if includeRoadLanterns then cc0LanternTiles map 12 else [] let houseLights = (map.Buildings @ map.Farmhouses) |> List.collect (fun building -> @@ -780,7 +896,11 @@ module VillageArt = else [ (building.DoorX, building.DoorY) ] (building.DoorX, building.DoorY) :: windowTiles) - lanterns @ houseLights |> List.distinct + lanterns @ roadLanterns @ houseLights |> List.distinct + + /// 既有夜景灯光源(不含 P39 主路石灯笼),保持既有调用与断言语义不变。 + let nightLightTiles (map: MapGen.Result) : (int * int) list = + nightLightTilesWith false map /// P29 夜景光晕叠加层:对每个灯光源画一圈柔和径向衰减(半径 3-6 tile), /// 由 `lanternGlowLayers` 保证无硬方斑、无等亮台阶。纯绘制,不改瓦片/可达性。 @@ -792,10 +912,11 @@ module VillageArt = (peakAlpha: int) (tileX: int -> int) (tileY: int -> int) - (cell: int) : unit = + (cell: int) + (includeRoadLanterns: bool) : unit = if peakAlpha > 0 && radiusPixels > 0 then let half = cell / 2 - for (lightX, lightY) in nightLightTiles map do + for (lightX, lightY) in nightLightTilesWith includeRoadLanterns map do let centerX = tileX lightX + half let centerY = tileY lightY + half for (rect, color) in lanternGlowLayers centerX centerY radiusPixels 8 255 214 150 peakAlpha do @@ -907,6 +1028,32 @@ module VillageArt = drawMapStructures spriteBatch textures pixel mapTileX mapTileY Sim.tilePixels map for (lanternX, lanternY) in bridgeLanternTiles map do draw (tilePosition lanternX lanternY) LanternSprite + // P39 中式元素补绘:仅在 CC0 包开启时叠加到生成器世界(自绘 12-18 格)。 + // 桥/船/灯/摊/苇位置为 (map, seed) 纯函数,只叠加视觉、不写瓦片/不改可达性。 + match textures.Cc0TileAtlas with + | Some _ -> + let drawCc0Map (tileX: int) (tileY: int) (index: int) = + drawCc0Tile + spriteBatch + textures.Cc0TileAtlas.Value + tint + (cc0TileRect index) + (mapTileX tileX) + (mapTileY tileY) + for (bx, by, index) in cc0BridgeTiles map do + drawCc0Map bx by index + match cc0BoatTile map with + | Some (boatX, boatY) -> + drawCc0Map boatX boatY cc0TileIndexBoatLeft + drawCc0Map (boatX + 1) boatY cc0TileIndexBoatRight + | None -> () + for (lanternX, lanternY) in cc0LanternTiles map 12 do + drawCc0Map lanternX lanternY cc0TileIndexStoneLantern + for (stallX, stallY) in cc0StallTiles map 12 do + drawCc0Map stallX stallY cc0TileIndexStall + for (reedX, reedY) in cc0ReedTiles map 4 do + drawCc0Map reedX reedY cc0TileIndexReeds + | None -> () | None -> () // 回退(64x48 样例世界)或 LV_MAP_SCALE 大世界时仍绘制样例地面/道具。 @@ -1023,7 +1170,7 @@ module VillageArt = drawMapStructures spriteBatch textures pixel tileX tileY cell map if glowScale > 0.0f then // 半径 4 tile 的柔和径向衰减;随 cell 缩放,整图/放大帧观感一致。 - drawNightGlows spriteBatch pixel map (4 * cell) (int (150.0f * glowScale)) tileX tileY cell + drawNightGlows spriteBatch pixel map (4 * cell) (int (150.0f * glowScale)) tileX tileY cell false map.Width * map.Height /// P20 证据用:按相机做视口裁剪,只绘制可见瓦片(1:1 像素),返回实际绘制数。 @@ -1061,7 +1208,7 @@ module VillageArt = drawn <- drawn + 1 drawMapStructures spriteBatch textures pixel tileX tileY Sim.tilePixels map if glowScale > 0.0f then - drawNightGlows spriteBatch pixel map (4 * Sim.tilePixels) (int (150.0f * glowScale)) tileX tileY Sim.tilePixels + drawNightGlows spriteBatch pixel map (4 * Sim.tilePixels) (int (150.0f * glowScale)) tileX tileY Sim.tilePixels false drawn /// P30 证据用:把地图的任意 tile 矩形 [tileX0,tileY0,tilesW,tilesH] 以 cell px/tile 真实 @@ -1097,7 +1244,7 @@ module VillageArt = drawn <- drawn + 1 drawMapStructures spriteBatch textures pixel tileX tileY cell map if glowScale > 0.0f then - drawNightGlows spriteBatch pixel map (4 * cell) (int (150.0f * glowScale)) tileX tileY cell + drawNightGlows spriteBatch pixel map (4 * cell) (int (150.0f * glowScale)) tileX tileY cell false drawn let drawCharacter |
