summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-28 16:33:26 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-28 16:33:26 +0800
commitb94ae560c652e8a5e759630acc079d834673c95a (patch)
treea372a69ff9e81723d4961f4358c3ba4e3e5b5b74 /src
parentafefc9b6ed4f464753c1009c6594557b07b325e9 (diff)
downloadliving-village-b94ae560c652e8a5e759630acc079d834673c95a.tar.gz
feat(p40): 石板路连贯带 + 红灯笼入夜光 + LV_P40_SHOT 证据钩子
- VillageArt: cc0PavingTiles 纯函数(路径/门口/桥相连石板格),drawTerrainTile cc0 分支只铺连贯石板带(19)、孤立散布石板改画草地;fallback/大世界原样 - cc0RedLanternTiles 门口红灯笼(20),并入 nightLightTilesWith 复用 P14 预乘 alpha 光晕 - Game: LV_P40_SHOT 钩子拍 p40-day/night/boat(PrepareP40Shot/CaptureP40Shot) - 不写瓦片/不改可达性/不动 Sim.step
Diffstat (limited to 'src')
-rw-r--r--src/LivingVillage.Desktop/Game.fs69
-rw-r--r--src/LivingVillage.Desktop/VillageArt.fs70
2 files changed, 136 insertions, 3 deletions
diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs
index 7b1dfb1..8c0b8a1 100644
--- a/src/LivingVillage.Desktop/Game.fs
+++ b/src/LivingVillage.Desktop/Game.fs
@@ -295,6 +295,12 @@ type LivingVillageGame() as this =
let mutable p39Step = 0
let mutable p39PendingName = ""
let mutable p39Pending = false
+ // P40 证据钩子:LV_P40_SHOT=1 时在真实 256x192 生成器世界里拍
+ // p40-day / p40-night / p40-boat-closeup(相机贴近乌篷船,便于 2x 特写)。
+ let p40ShotMode = Environment.GetEnvironmentVariable("LV_P40_SHOT") = "1"
+ let mutable p40Step = 0
+ let mutable p40PendingName = ""
+ let mutable p40Pending = false
let mutable autoplayFrames = 0
let mutable flowStep = 0
let mutable flowHold = 0
@@ -691,6 +697,59 @@ type LivingVillageGame() as this =
p39PendingName world.Tick world.Avatar.Pos.X world.Avatar.Pos.Y camera.X camera.Y
p39Step <- p39Step + 1
+ /// P40 证据:真实生成器世界里拍昼 / 夜整帧与乌篷船近景。位置取自确定性摆放纯函数,
+ /// 含真实 tint 与 P14 光晕;相机贴近船体便于 2x NEAREST 特写。
+ member private this.PrepareP40Shot () =
+ 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 p40Pending then
+ match riverscapeMap with
+ | None -> this.Exit()
+ | Some map ->
+ let boat =
+ match VillageArt.cc0BoatTile map with
+ | Some (bx, by) -> (bx, by)
+ | 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 boatShot = (fst boat + 1, snd boat)
+ match p40Step with
+ | 0 ->
+ setHour dayHour
+ placeCameraOn (fst boat) (snd boat)
+ p40PendingName <- "p40-day"
+ p40Pending <- true
+ | 1 ->
+ setHour nightHour
+ placeCameraOn (fst boat) (snd boat)
+ p40PendingName <- "p40-night"
+ p40Pending <- true
+ | 2 ->
+ setHour dayHour
+ placeCameraOn (fst boatShot) (snd boatShot)
+ p40PendingName <- "p40-boat"
+ p40Pending <- true
+ | _ ->
+ printfn "p40-shot=done"
+ this.Exit()
+
+ /// P40 证据:与 P39 相同,在 `Draw` 末尾落盘当前后台缓冲。
+ member private this.CaptureP40Shot () =
+ if p40Pending then
+ p40Pending <- false
+ System.IO.Directory.CreateDirectory recordDirectory |> ignore
+ this.SaveBackBuffer(sprintf "%s/%s.png" recordDirectory p40PendingName)
+ printfn "p40-shot=%s tick=%d pos=(%.0f,%.0f) camera=(%.0f,%.0f)"
+ p40PendingName world.Tick world.Avatar.Pos.X world.Avatar.Pos.Y camera.X camera.Y
+ p40Step <- p40Step + 1
+
/// P20 第三步取证:真走位巡游(Sim.step 驱动)沿桥/路/民居门移动,用于桥面行走帧与昼夜录制。
member private this.RunRiverscapeTour () =
if not riverscapeTourWatchStarted then
@@ -998,6 +1057,14 @@ type LivingVillageGame() as this =
else this.DispatchMenuInput Confirm
elif menu.Page = Playing then
this.PrepareP39Shot()
+ elif p40ShotMode 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.PrepareP40Shot()
elif p37ShotMode then
this.RunP37Shot()
elif menu.Page = Playing then
@@ -1379,6 +1446,8 @@ type LivingVillageGame() as this =
this.CaptureP36PromptShot()
if p39ShotMode && menu.Page = Playing then
this.CaptureP39Shot()
+ if p40ShotMode && menu.Page = Playing then
+ this.CaptureP40Shot()
/// 启动画面:分层天色 + 屋脊/水面剪影 + 标题淡入上浮 + 跳过提示,全部为帧计数纯函数。
member private this.DrawSplash() =
diff --git a/src/LivingVillage.Desktop/VillageArt.fs b/src/LivingVillage.Desktop/VillageArt.fs
index e08cde4..0427dea 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 = 19
+ let cc0TileCount = 21
let cc0TileIndexWaterA = 4
let cc0TileIndexWaterB = 5
let cc0TileIndexRoofRidge = 6
@@ -574,6 +574,10 @@ module VillageArt =
let cc0TileIndexStoneLantern = 16
let cc0TileIndexStall = 17
let cc0TileIndexReeds = 18
+ // P40 appended elements (indices 19-20). P40 also redrew the boat (14/15) and
+ // sharpened the stone lantern (16); indices 0-13 and 17-18 stay byte-identical.
+ let cc0TileIndexPaving = 19
+ let cc0TileIndexRedLantern = 20
let cc0WaterFrameCount = 2
let cc0TileAtlasWidth = Sim.tilePixels * cc0TileCount
let cc0TileAtlasHeight = Sim.tilePixels
@@ -704,6 +708,47 @@ module VillageArt =
&& (tileIsWater map x (y - 1) || tileIsWater map x (y + 1)) then
yield (x, y) ]
+ /// P40 石板路衔接:返回「与通行网络相连」的石板格索引集合。判据为纯几何:
+ /// 该格是石板(code 2),且它本身或四邻属于 `map.Paths`(主路/桥引道/门口)或
+ /// `map.Bridges`(桥面)。稀疏的 `DecorativeStones` 散布石板因远离路网被排除,
+ /// 于是 cc0 模式下石板只沿路径/门口/桥形成连贯带(孤立石板改画草地)。
+ /// 纯函数,只依赖 map(seed 内确定),不写瓦片、不改可达性。
+ let cc0PavingTiles (map: MapGen.Result) : Set<int> =
+ if map.Width <= 0 then
+ Set.empty
+ else
+ let indexOf x y = y * map.Width + x
+ let inMap x y = x >= 0 && x < map.Width && y >= 0 && y < map.Height
+ let isStone x y =
+ inMap x y && map.Tiles.[indexOf x y] = int MapGen.GroundTile.Stone
+ let anchors =
+ (map.Paths @ map.Bridges)
+ |> List.map (fun (x, y) -> indexOf x y)
+ |> Set.ofList
+ let isAnchor x y = inMap x y && Set.contains (indexOf x y) anchors
+ let connected x y =
+ isAnchor x y
+ || isAnchor (x - 1) y
+ || isAnchor (x + 1) y
+ || isAnchor x (y - 1)
+ || isAnchor x (y + 1)
+ [ for y in 0 .. map.Height - 1 do
+ for x in 0 .. map.Width - 1 do
+ if isStone x y && connected x y then yield indexOf x y ]
+ |> Set.ofList
+
+ /// P40 红灯笼:在每户民居门口确定性挂一盏红纸灯笼(门口 y,取门格)。
+ /// 与石灯笼形态区分:红纸圆身 + 金穗底座;位置为 (map) 纯函数,同 seed 不变。
+ /// 仅 cc0 模式绘制,且并入 `nightLightTilesWith` 的既有 P14 预乘 alpha 光晕路径。
+ let cc0RedLanternTiles (map: MapGen.Result) : (int * int) list =
+ if map.Width <= 0 then
+ []
+ else
+ (map.Buildings @ map.Farmhouses)
+ |> List.map (fun building -> (building.DoorX, building.DoorY))
+ |> List.filter (fun (x, y) -> x >= 0 && x < map.Width && y >= 0 && y < map.Height)
+ |> List.distinct
+
/// 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 =
@@ -883,9 +928,12 @@ module VillageArt =
/// P30:散落农舍的门窗暖光一并计入。
/// P39:`includeRoadLanterns` 为真时把主路石灯笼(`cc0LanternTiles`)计入,使夜帧灯笼
/// 光晕复用同一光晕路径;仅 cc0 模式为真,故 fallback(江南程序图集)不受影响。
+ /// P40:同一开关下把门口红灯笼(`cc0RedLanternTiles`)也计入;红灯笼与石灯笼复用
+ /// 同一条 P14 预乘 alpha 径向光晕路径,不新增光照系统。
let nightLightTilesWith (includeRoadLanterns: bool) (map: MapGen.Result) : (int * int) list =
let lanterns = bridgeLanternTiles map
let roadLanterns = if includeRoadLanterns then cc0LanternTiles map 12 else []
+ let redLanterns = if includeRoadLanterns then cc0RedLanternTiles map else []
let houseLights =
(map.Buildings @ map.Farmhouses)
|> List.collect (fun building ->
@@ -896,7 +944,7 @@ module VillageArt =
else
[ (building.DoorX, building.DoorY) ]
(building.DoorX, building.DoorY) :: windowTiles)
- lanterns @ roadLanterns @ houseLights |> List.distinct
+ lanterns @ roadLanterns @ redLanterns @ houseLights |> List.distinct
/// 既有夜景灯光源(不含 P39 主路石灯笼),保持既有调用与断言语义不变。
let nightLightTiles (map: MapGen.Result) : (int * int) list =
@@ -983,13 +1031,26 @@ module VillageArt =
| Some index -> drawCc0At position index
| None -> draw position sprite
+ // P40 石板路衔接:cc0 模式下只在「与路网/门口/桥相连」的石板格铺连贯石板带,
+ // 稀疏孤立装饰石板改画草地,于是场地上不再出现游离石板。fallback(无 cc0 图集)
+ // 与无 mapGen 的大世界保持原样,故既有输出逐字节不变。
+ let cc0PavingSet =
+ match mapGen, textures.Cc0TileAtlas with
+ | Some map, Some _ -> Some (map.Width, cc0PavingTiles map)
+ | _ -> None
+
// Procedural terrain for the 512x384 large world: water/pebble/peat codes map
// onto the jiangnan atlas sprites with deterministic ripple continuity.
// 生成器世界:1 水 / 2 石板路 / 3 泥炭 / 0 草地。泥炭按草地渲染,路走石板贴图。
let drawTerrainTile (position: TilePosition) (code: int) =
match code with
| 1 -> drawWater position
- | 2 -> draw position StonePathSprite
+ | 2 ->
+ match cc0PavingSet with
+ | Some (mapWidth, paving) when Set.contains (position.Y * mapWidth + position.X) paving ->
+ drawCc0At position cc0TileIndexPaving
+ | Some _ -> draw position GrassSprite
+ | None -> draw position StonePathSprite
// P30 乡村空区:水田(4)走水波动画、菜畦(5)走菜地素材;均为成熟 tile。
// P34:水田/水网在 CC0 包下同样走自绘 2 相位水面。
| 4 -> drawWater position
@@ -1053,6 +1114,9 @@ module VillageArt =
drawCc0Map stallX stallY cc0TileIndexStall
for (reedX, reedY) in cc0ReedTiles map 4 do
drawCc0Map reedX reedY cc0TileIndexReeds
+ // P40 红灯笼:门口挂一盏,与主路石灯笼形态区分;夜光晕走同一 P14 路径。
+ for (lanternRedX, lanternRedY) in cc0RedLanternTiles map do
+ drawCc0Map lanternRedX lanternRedY cc0TileIndexRedLantern
| None -> ()
| None -> ()