summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/LivingVillage.Desktop/Game.fs96
-rw-r--r--src/LivingVillage.Desktop/VillageArt.fs163
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