summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop/VillageArt.fs
diff options
context:
space:
mode:
Diffstat (limited to 'src/LivingVillage.Desktop/VillageArt.fs')
-rw-r--r--src/LivingVillage.Desktop/VillageArt.fs163
1 files changed, 155 insertions, 8 deletions
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