summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop
diff options
context:
space:
mode:
Diffstat (limited to 'src/LivingVillage.Desktop')
-rw-r--r--src/LivingVillage.Desktop/Game.fs127
-rw-r--r--src/LivingVillage.Desktop/MapGen.fs346
-rw-r--r--src/LivingVillage.Desktop/VillageArt.fs59
3 files changed, 483 insertions, 49 deletions
diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs
index 5f6e99e..3a9a004 100644
--- a/src/LivingVillage.Desktop/Game.fs
+++ b/src/LivingVillage.Desktop/Game.fs
@@ -307,6 +307,8 @@ type LivingVillageGame() as this =
| v -> match Int32.TryParse v with (true, value) when value > 0 -> value | _ -> 0
// P28 夜景取证:LV_MAPGEN_NIGHT=1 时整图按 23:30 的真实 tint + 月光 veil 渲染。
let mapGenNight = Environment.GetEnvironmentVariable("LV_MAPGEN_NIGHT") = "1"
+ // P30 证据钩子:LV_MAPGEN_P30_SHOT=1 时按 P30 规格出图(整图 day/night + 田块局部 + 路网弯曲局部)。
+ let mapGenP30Shot = Environment.GetEnvironmentVariable("LV_MAPGEN_P30_SHOT") = "1"
let mapGenNightTick = if mapGenNight then M6Presentation.resolveStartTick (Some 23.5) false else 0L
let mutable mapGenResult = Unchecked.defaultof<MapGen.Result>
let mutable mapGenGenMs = 0.0
@@ -972,9 +974,132 @@ type LivingVillageGame() as this =
printfn "mapgen-shot=done"
this.Exit()
+ /// P30 证据:把地图任意 tile 矩形以 cell px/tile 真实离屏渲染,含 tint 与可选 veil/夜光。
+ member private this.RenderMapRegion (tileX0: int) (tileY0: int) (tilesW: int) (tilesH: int) (cell: int) (tint: Color) (veilTick: int64 option) : int * Texture2D =
+ let targetWidth = max 1 (tilesW * cell)
+ let targetHeight = max 1 (tilesH * cell)
+ use target = new RenderTarget2D(this.GraphicsDevice, targetWidth, targetHeight)
+ this.GraphicsDevice.SetRenderTarget target
+ this.GraphicsDevice.Clear(Color(18, 26, 20))
+ spriteBatch.Begin()
+ let glowScale =
+ match veilTick with
+ | Some tick -> (M6Presentation.profileAtTick tick).LanternGlow
+ | None -> 0.0f
+ let drawn =
+ VillageArt.drawMapRegion spriteBatch artTextures pixel tileX0 tileY0 tilesW tilesH cell mapGenResult tint glowScale
+ match veilTick with
+ | Some tick ->
+ let golden = M6Presentation.goldenVeilAlpha tick
+ if golden > 0 then
+ spriteBatch.Draw(pixel, Rectangle(0, 0, targetWidth, targetHeight), VillageArt.premultiply 255 150 70 golden)
+ let moon = M6Presentation.moonlightVeilAlpha tick
+ if moon > 0 then
+ let mr, mg, mb = M6Presentation.moonlightRgb
+ spriteBatch.Draw(pixel, Rectangle(0, 0, targetWidth, targetHeight), VillageArt.premultiply mr mg mb moon)
+ | None -> ()
+ spriteBatch.End()
+ this.GraphicsDevice.SetRenderTarget null
+ let data = Array.zeroCreate<Color> (targetWidth * targetHeight)
+ target.GetData<Color>(data)
+ let texture = new Texture2D(this.GraphicsDevice, targetWidth, targetHeight)
+ texture.SetData(data)
+ (drawn, texture)
+
+ /// P30 田块局部取景:以「窗口内田块最密」的散落农舍为中心(含田块/树丛/院落),
+ /// 优先完整落入画面的农舍;否则退化为首个田块。确定性纯函数。
+ member private this.P30FieldRegion () : int * int =
+ let w = mapGenResult.Width
+ let h = mapGenResult.Height
+ let regionW = 128
+ let regionH = 64
+ let clampX x = max 0 (min (max 0 (w - regionW)) x)
+ let clampY y = max 0 (min (max 0 (h - regionH)) y)
+ let isField code = code = int MapGen.GroundTile.PaddyField || code = int MapGen.GroundTile.VegetablePlot
+ let countFields (cx: int) (cy: int) =
+ let x0 = max 0 (cx - regionW / 2)
+ let x1 = min (w - 1) (cx + regionW / 2)
+ let y0 = max 0 (cy - regionH / 2)
+ let y1 = min (h - 1) (cy + regionH / 2)
+ let mutable n = 0
+ for y in y0 .. y1 do
+ for x in x0 .. x1 do
+ if isField mapGenResult.Tiles.[y * w + x] then n <- n + 1
+ n
+ let interior =
+ mapGenResult.Farmhouses
+ |> List.filter (fun b -> b.Top >= 16 && b.Top <= h - 80 && b.Left >= regionW / 2 && b.Left <= w - regionW / 2)
+ let candidates = if interior.IsEmpty then mapGenResult.Farmhouses else interior
+ match candidates |> List.sortBy (fun b -> (-(countFields (b.Left + b.Width / 2) (b.Top + 1)), b.Top, b.Left)) |> List.tryHead with
+ | Some b -> (clampX (b.Left + b.Width / 2 - regionW / 2), clampY (b.Top - regionH / 2))
+ | None ->
+ let mutable found = None
+ let mutable y = 0
+ while found.IsNone && y < h do
+ let mutable x = 0
+ while found.IsNone && x < w do
+ let code = mapGenResult.Tiles.[y * w + x]
+ if isField code then found <- Some(x, y)
+ x <- x + 1
+ y <- y + 1
+ match found with
+ | Some (x, y) -> (clampX (x - regionW / 2), clampY (y - regionH / 2))
+ | None -> (clampX (w / 2 - regionW / 2), clampY (h / 2 - regionH / 2))
+
+ /// P30 路网弯曲局部取景:以主路中段某个弯折列为横向中心,纵向对准主路中心线。
+ member private this.P30RoadRegion () : int * int =
+ let w = mapGenResult.Width
+ let h = mapGenResult.Height
+ let regionW = 128
+ let regionH = 64
+ let clampX x = max 0 (min (max 0 (w - regionW)) x)
+ let clampY y = max 0 (min (max 0 (h - regionH)) y)
+ let center = mapGenResult.MainRoadCenter
+ let roadY = if w > 0 then center.[w / 2] else h / 2
+ let bendX =
+ [ for x in 1 .. w - 2 do
+ if abs (center.[x] - center.[x - 1]) > 0 then yield x ]
+ |> List.tryFind (fun x -> x > w / 4 && x < (3 * w) / 4)
+ |> Option.defaultValue (w / 2)
+ (clampX (bendX - regionW / 2), clampY (roadY - regionH / 2))
+
+ /// P30 证据出图:整图 3072x2304(6px/tile)day/night + 田块局部 512x256(4px/tile)
+ /// + 路网弯曲局部 512x256(4px/tile)。全部真实离屏渲染,拍完即退出。
+ member private this.DrawP30Shot () =
+ System.IO.Directory.CreateDirectory recordDirectory |> ignore
+ let save (name: string) (texture: Texture2D) =
+ use stream = System.IO.File.Create(sprintf "%s/%s.png" recordDirectory name)
+ texture.SaveAsPng(stream, texture.Width, texture.Height)
+ let nightTick = M6Presentation.resolveStartTick (Some 23.5) false
+ let nightTint = (M6Presentation.profileAtTick nightTick).WorldTint
+ let fullW = mapGenResult.Width * 6
+ let fullH = mapGenResult.Height * 6
+ let _, dayTexture = this.RenderMapGen fullW fullH 0 0 true Color.White None
+ use _day = dayTexture
+ save (sprintf "p30-map-%dx%d-seed%d-day" mapGenResult.Width mapGenResult.Height (int mapGenResult.Seed)) dayTexture
+ let _, nightTexture = this.RenderMapGen fullW fullH 0 0 true nightTint (Some nightTick)
+ use _night = nightTexture
+ save (sprintf "p30-map-%dx%d-seed%d-night" mapGenResult.Width mapGenResult.Height (int mapGenResult.Seed)) nightTexture
+ let fx, fy = this.P30FieldRegion ()
+ let _, fieldDay = this.RenderMapRegion fx fy 128 64 4 Color.White None
+ use _fieldDay = fieldDay
+ save "p30-field-cluster-4x-day" fieldDay
+ let _, fieldNight = this.RenderMapRegion fx fy 128 64 4 nightTint (Some nightTick)
+ use _fieldNight = fieldNight
+ save "p30-field-cluster-4x-night" fieldNight
+ let rx, ry = this.P30RoadRegion ()
+ let _, roadDay = this.RenderMapRegion rx ry 128 64 4 Color.White None
+ use _roadDay = roadDay
+ save "p30-roadserp-4x-day" roadDay
+ printfn "p30-shot=done map=%dx%d field_region=(%d,%d) road_region=(%d,%d) night=%b" mapGenResult.Width mapGenResult.Height fx fy rx ry mapGenNight
+ this.Exit()
+
override this.Draw(gameTime: GameTime) =
if mapGenShotMode then
- this.DrawMapGenShot()
+ if mapGenP30Shot then
+ this.DrawP30Shot()
+ else
+ this.DrawMapGenShot()
else
if splashActive then
this.DrawSplash()
diff --git a/src/LivingVillage.Desktop/MapGen.fs b/src/LivingVillage.Desktop/MapGen.fs
index a9d6c9f..abd4a7e 100644
--- a/src/LivingVillage.Desktop/MapGen.fs
+++ b/src/LivingVillage.Desktop/MapGen.fs
@@ -10,11 +10,14 @@ open LivingVillage.Kernel
module MapGen =
/// 地面码,与既有 ProceduralMap / 渲染映射保持一致。
+ /// P30:大图乡村空区新增 4=水田 / 5=菜畦(成熟 tile 素材;非水故仍可通行)。
type GroundTile =
| Grass = 0
| Water = 1
| Stone = 2
| Peat = 3
+ | PaddyField = 4
+ | VegetablePlot = 5
type Bounds =
{ MinX: int
@@ -67,6 +70,17 @@ module MapGen =
X: int
Y: int }
+ /// P30 树丛种类:竹丛 / 灌木丛;纯视觉层(不写瓦片、不影响通行),仅大图乡村空区生成。
+ type GroveKind =
+ | BambooClump
+ | ShrubClump
+
+ /// P30 树丛:单格树/灌木,成丛由若干相邻格组成;渲染走既有 Bamboo/Shrub 成熟素材。
+ type Grove =
+ { Kind: GroveKind
+ X: int
+ Y: int }
+
type Result =
{ Width: int
Height: int
@@ -79,6 +93,12 @@ module MapGen =
Paths: (int * int) list
Buildings: Building list
Decorations: RiverDecoration list
+ /// P30 散落农舍:与沿河组团民居分列,避免污染组团规模统计;门贴草地/院落路可达。
+ Farmhouses: Building list
+ /// P30 乡村树丛:纯视觉,不入瓦片、不影响通行与可达性。
+ Groves: Grove list
+ /// P30 主路中心线(逐列 y):默认图为笔直 roadY;大图为确定性缓弯(相邻列步长 <=1)。
+ MainRoadCenter: int array
ReachableTiles: int
ReachabilityOk: bool
BridgeCrossingsOk: bool }
@@ -103,6 +123,8 @@ module MapGen =
/// P24:仅对大于 256x192 的图追加沿河民居(泊松散布),保证既有 checksum 不变。
/// P28:大图沿河民居由 10 增至 16(+6 座,宽度 2/3/4 变化),默认图仍为 0、不消耗 RNG。
/// P29:大图额外启用聚落组团发牌种子(独立 clusterRng),默认图保持 0、不进入组团分支。
+ /// P30:大图沿河/沿路民居目标由 16 增至 30(组团 2-5 座为主),并新增乡村空区填充与主路弯曲,
+ /// 全部只在 large 分支执行;64x48/256x192 判定不进入、不消费任何额外 RNG。
let paramsForSize (width: int) (height: int) (seed: uint64) : Params =
let base_ = defaultParams seed
let large = width > 256 || height > 192
@@ -110,7 +132,7 @@ module MapGen =
Width = width
Height = height
RiverCount = max base_.RiverCount (max 1 (height / 96))
- ExtraRiversideHouses = (if large then 16 else 0)
+ ExtraRiversideHouses = (if large then 30 else 0)
DecorativeStones = not large
RiverDecorations = large
ClusterSeed = (if large then seed ^^^ 0xC1A57E2UL else 0UL) }
@@ -254,6 +276,8 @@ module MapGen =
let mutable bridges: (int * int) list = []
let mutable paths: (int * int) list = []
let mutable buildings: Building list = []
+ let mutable farmhouses: Building list = []
+ let mutable groves: Grove list = []
let pathSet = System.Collections.Generic.HashSet<int>()
let addPath x y =
if inMap x y && tiles.[idx x y] <> int GroundTile.Water then
@@ -264,8 +288,29 @@ module MapGen =
structureRng <- nextState
lo + int (raw % uint64 (max 1 (hi - lo + 1)))
let roadY = (core.MinY + core.MaxY) / 2
+ // P30 主路柔化:仅大图把主街按逐点确定性微扰成缓弯(曲率 ±3 格、相邻列步长 <=1,
+ // 遇弯补一格保证 4 连通);默认图仍为笔直主街,故 64x48/256x192 输出逐字节不变。
+ let largeMap = p.ExtraRiversideHouses > 0
+ let roadCenter =
+ let arr = Array.create w roadY
+ if largeMap then
+ let roadSeed = p.Seed ^^^ 0x5E9B0AUL
+ let mutable prev = roadY
+ for x in 1 .. w - 2 do
+ let noise = valueNoise (float32 x) 0.0f roadSeed 130.0f
+ let target = roadY + int (System.Math.Round(3.0 * float (noise - 0.5f) * 2.0))
+ let y = max (prev - 1) (min (prev + 1) target)
+ arr.[x] <- y
+ prev <- y
+ // 两边界补与首/末列相接,避免边缘断头。
+ arr.[0] <- arr.[1]
+ arr.[w - 1] <- arr.[w - 2]
+ arr
+ let roadYAt (x: int) = roadCenter.[max 0 (min (w - 1) x)]
for x in 1 .. w - 2 do
- addPath x roadY
+ if x > 1 && roadYAt x <> roadYAt (x - 1) then
+ addPath x (roadYAt (x - 1))
+ addPath x (roadYAt x)
let bridgeColumns =
[ for river in riverBands do
let mutable picked: int list = []
@@ -284,8 +329,9 @@ module MapGen =
let yy = river.CenterY + dy
if inMap (bx + dx) yy then
bridges <- (bx + dx, yy) :: bridges
- let yLo = min (river.CenterY - 1) roadY
- let yHi = max (river.CenterY + river.Width) roadY
+ let roadYb = roadYAt bx
+ let yLo = min (river.CenterY - 1) roadYb
+ let yHi = max (river.CenterY + river.Width) roadYb
for y in yLo .. yHi do
addPath bx y
let farY = if river.CenterY < core.MinY then river.CenterY - 2 else river.CenterY + river.Width + 1
@@ -311,12 +357,12 @@ module MapGen =
buildings <- building :: buildings
addPath building.DoorX building.DoorY
- // 5b) P24/P28/P29:大图沿河聚落组团。仅在 ExtraRiversideHouses>0 时执行,只落在核心区外
+ // 5b) P24/P28/P29/P30:大图沿河聚落组团。仅在 ExtraRiversideHouses>0 时执行,只落在核心区外
// 草地并避免与既有民居重叠,故 64x48/256x192 输出逐字节不变。
- // P29:不改「沿路随机散点」,而是沿横向石板路(主街 + 河岸路)成排成组落位:
- // 每组 2-4 座、组内 1 格间隙、组间留 4-6 格空档,形成「沿河成排 / 桥头小广场」的
- // 聚落肌理;组发牌使用独立 clusterRng(group seeding),不触碰既有 structureRng,
- // 桥/路/灯笼等基础结构坐标不受影响。宽度仍按 2/3/4 加权(附属小筑/民居/大宅)。
+ // P29:沿横向石板路(主街 + 河岸路)成排成组落位,组发牌用独立 clusterRng(group seeding),
+ // 不触碰既有 structureRng,桥/路/灯笼等基础结构坐标不受影响。宽度按 2/3/4 加权。
+ // P30:目标密度由 16 增至 30,组团规模改为 2-5 座;含基础岸屋的路段优先布组,
+ // 使每座基础民居并入 >=2 座的组团,消除大面积孤立单屋。
if p.ExtraRiversideHouses > 0 then
let footprintFree (left: int) (top: int) (width: int) =
left >= 1 && left + width - 1 <= w - 2 && top >= 1 && top + 1 <= h - 2
@@ -331,6 +377,13 @@ module MapGen =
left < b.Left + b.Width && b.Left < left + width
&& top < b.Top + b.Height && b.Top < top + 2)
// 横向路段的连续格(主街/河岸路),先按 (y,x) 排序再按 group seed 打散成确定性布线顺序。
+ // P30:含既有岸屋的路段优先处理,保证每座基础民居都能被并进 2-5 座的组团。
+ let runHasBase (runY: int) (x0: int) (x1: int) =
+ buildings
+ |> List.exists (fun b ->
+ (b.Top = runY - 2 || b.Top = runY + 1)
+ && b.Left <= x1
+ && b.Left + b.Width - 1 >= x0)
let horizontalRuns =
[ for y in 1 .. h - 2 do
let mutable x = 1
@@ -342,51 +395,230 @@ module MapGen =
if x - x0 >= 4 then yield (y, x0, x - 1)
else
x <- x + 1 ]
- |> List.sortBy (fun (y, x0, _) -> (hash2 y x0 p.ClusterSeed))
+ |> List.sortBy (fun (y, x0, x1) -> ((if runHasBase y x0 x1 then 0 else 1), hash2 y x0 p.ClusterSeed))
let mutable clusterRng = p.ClusterSeed
let nextCluster () =
let nextState, raw = splitmix clusterRng
clusterRng <- nextState
raw
let mutable extraPlaced = 0
- for (runY, runStart, runEnd) in horizontalRuns do
- if extraPlaced < p.ExtraRiversideHouses then
- let runLength = runEnd - runStart + 1
- // 主街长路段多放,河岸短路各放一小簇。
- let quota = min (p.ExtraRiversideHouses - extraPlaced) (if runLength >= 64 then 6 else 3)
- // 默认屋在路北(门贴路南缘);北侧容不下时改南侧(沿用既有岸屋「门在顶排」口径)。
+ // 在一条横向路段的 [lo,hi] 区间内铺一个 2-5 座组团:门贴路缘、组内 1 格间隙、
+ // 宽度按 2/3/4 加权。先做局部试排,只有落满 >=2 座、或能与既有民居并组时才提交,
+ // 从源头消除「孤立单屋」。返回实际落位数。
+ let nearFootprint (a: Building) (b: Building) =
+ let xGap = max (a.Left - (b.Left + b.Width - 1)) (b.Left - (a.Left + a.Width - 1))
+ let yGap = max (a.Top - (b.Top + b.Height - 1)) (b.Top - (a.Top + a.Height - 1))
+ xGap <= 2 && yGap <= 2
+ let placeGroup (runY: int) (lo: int) (hi: int) (desired: int) : int =
+ if hi - lo + 1 < 4 then
+ 0
+ else
let sideFits above =
let top = if above then runY - 2 else runY + 1
- [ runStart .. min runEnd (runStart + 4) ]
- |> List.exists (fun x -> footprintFree x top 3)
+ [ lo .. min hi (lo + 4) ] |> List.exists (fun x -> footprintFree x top 3)
let above = if sideFits true then true else not (sideFits false)
- let mutable x = runStart + int (nextCluster () % 2UL)
- let mutable placedHere = 0
- while placedHere < quota && x + 1 <= runEnd do
- let groupSize = 2 + int (nextCluster () % 3UL)
- for _ in 1 .. groupSize do
- if placedHere < quota && x + 1 <= runEnd then
- let widthRoll = int (nextCluster () % 10UL)
- let buildingWidth = if widthRoll < 5 then 3 elif widthRoll < 8 then 2 else 4
- let doorX = x + buildingWidth / 2
- let doorY = if above then runY - 1 else runY + 1
- let top = if above then doorY - 1 else doorY
- let left = doorX - buildingWidth / 2
- if footprintFree left top buildingWidth && not (overlaps left top buildingWidth) then
- let building =
- { Left = left
- Top = top
- Width = buildingWidth
- Height = 2
- DoorX = doorX
- DoorY = doorY }
- buildings <- building :: buildings
- addPath building.DoorX building.DoorY
- extraPlaced <- extraPlaced + 1
- placedHere <- placedHere + 1
- x <- x + buildingWidth + 1
- // 组间空档:保持可控行距与组团边界。
- x <- x + 4 + int (nextCluster () % 3UL)
+ let mutable x = max lo (min (hi - 1) (lo + int (nextCluster () % 3UL)))
+ let tentative = ResizeArray<Building>()
+ let mutable guard = 0
+ while tentative.Count < desired && x <= hi - 1 && guard < 48 do
+ let widthRoll = int (nextCluster () % 10UL)
+ let buildingWidth = if widthRoll < 5 then 3 elif widthRoll < 8 then 2 else 4
+ let doorX = x + buildingWidth / 2
+ let doorY = if above then runY - 1 else runY + 1
+ let top = if above then doorY - 1 else doorY
+ let left = doorX - buildingWidth / 2
+ let clashes =
+ tentative
+ |> Seq.exists (fun b ->
+ left < b.Left + b.Width && b.Left < left + buildingWidth
+ && top < b.Top + b.Height && b.Top < top + 2)
+ if doorX <= hi && footprintFree left top buildingWidth && not (overlaps left top buildingWidth) && not clashes then
+ tentative.Add
+ { Left = left
+ Top = top
+ Width = buildingWidth
+ Height = 2
+ DoorX = doorX
+ DoorY = doorY }
+ x <- x + buildingWidth + 1
+ else
+ x <- x + 1
+ guard <- guard + 1
+ let connected (t: Building) =
+ (buildings |> List.exists (fun b -> nearFootprint b t))
+ || (tentative |> Seq.exists (fun o -> not (obj.ReferenceEquals(o, t)) && nearFootprint o t))
+ let keep = tentative |> Seq.filter connected |> Seq.toList
+ for building in keep do
+ buildings <- building :: buildings
+ addPath building.DoorX building.DoorY
+ keep.Length
+ // P30 基础岸屋配对:为每座尚孤立的基础民居在同行贴 1 格补一座,保证并入 >=2 座组团。
+ let baseBuildings = buildings
+ let sameBuilding (a: Building) (c: Building) =
+ a.Left = c.Left && a.Top = c.Top && a.Width = c.Width && a.DoorX = c.DoorX && a.DoorY = c.DoorY
+ for b in baseBuildings do
+ let isolated =
+ buildings
+ |> List.forall (fun o -> sameBuilding o b || not (nearFootprint o b))
+ if isolated then
+ let widthRoll = int (nextCluster () % 10UL)
+ let buildingWidth = if widthRoll < 5 then 3 elif widthRoll < 8 then 2 else 4
+ let candidates = [ b.Left + b.Width + 1; b.Left - 1 - buildingWidth ]
+ let mutable done_ = false
+ for left in candidates do
+ if not done_
+ && left >= 1
+ && left + buildingWidth - 1 <= w - 2
+ && footprintFree left b.Top buildingWidth
+ && not (overlaps left b.Top buildingWidth) then
+ let doorX = left + buildingWidth / 2
+ buildings <-
+ { Left = left
+ Top = b.Top
+ Width = buildingWidth
+ Height = 2
+ DoorX = doorX
+ DoorY = b.DoorY }
+ :: buildings
+ addPath doorX b.DoorY
+ done_ <- true
+ for (runY, runStart, runEnd) in horizontalRuns do
+ if extraPlaced < p.ExtraRiversideHouses then
+ let runLength = runEnd - runStart + 1
+ // 长路段容纳多个组团,短河岸路各一个。
+ let groupsForRun = max 1 (min 3 (runLength / 24))
+ for group in 0 .. groupsForRun - 1 do
+ if extraPlaced < p.ExtraRiversideHouses then
+ let lo = runStart + (group * runLength) / groupsForRun
+ let hi = runStart + ((group + 1) * runLength) / groupsForRun - 1
+ let desired = min (p.ExtraRiversideHouses - extraPlaced) (2 + int (nextCluster () % 4UL))
+ let placed = placeGroup runY lo hi desired
+ extraPlaced <- extraPlaced + placed
+
+ // 5d) P30 乡村空区填充:上部/下部空带按确定性网格铺田块(水田/菜畦走成熟 tile
+ // 素材,写入 4/5 号非水瓦片,仍可通行)、树丛(纯视觉)与少量散落农舍(门贴院落路)。
+ // 仅在 large 分支执行:默认图不进入、不消费任何额外 RNG;田块离水面 >=3 格,
+ // 避开路网/核心/民居,故不影响河道连续性与 BFS 可达性。
+ if p.ExtraRiversideHouses > 0 then
+ let mutable countrysideRng = p.ClusterSeed ^^^ 0xF1E1D5EEDUL
+ let nextCountry () =
+ let nextState, raw = splitmix countrysideRng
+ countrysideRng <- nextState
+ raw
+ // 水面垂直距离:河流整幅横贯,故只需按 y 计算;边界列留 99(不会被选中)。
+ let waterDistance = Array.create (w * h) 99
+ for river in riverBands do
+ for x in 1 .. w - 2 do
+ for y in 0 .. h - 1 do
+ let d =
+ if y < river.CenterY then river.CenterY - y
+ elif y > river.CenterY + river.Width - 1 then y - (river.CenterY + river.Width - 1)
+ else 0
+ let i = idx x y
+ if d < waterDistance.[i] then waterDistance.[i] <- d
+ let structureBlocked = Array.create (w * h) false
+ for y in core.MinY .. core.MaxY do
+ for x in core.MinX .. core.MaxX do
+ structureBlocked.[idx x y] <- true
+ for (px, py) in paths do
+ structureBlocked.[idx px py] <- true
+ let markBuilding (b: Building) =
+ for yy in b.Top .. b.Top + b.Height - 1 do
+ for xx in b.Left .. b.Left + b.Width - 1 do
+ if inMap xx yy then structureBlocked.[idx xx yy] <- true
+ for b in buildings do
+ markBuilding b
+ let freeGround (x: int) (y: int) (minWater: int) =
+ inMap x y
+ && tiles.[idx x y] = int GroundTile.Grass
+ && waterDistance.[idx x y] >= minWater
+ && not structureBlocked.[idx x y]
+ let cellW = 18
+ let cellH = 14
+ let tryField (cellX: int) (cellY: int) (roll: uint64) =
+ let fw = 6 + int ((roll >>> 8) % 9UL) // 6..14
+ let fh = 4 + int ((roll >>> 16) % 7UL) // 4..10
+ let maxOffX = max 0 (cellW - 4 - fw)
+ let maxOffY = max 0 (cellH - 4 - fh)
+ let left = cellX + 2 + int ((roll >>> 24) % uint64 (maxOffX + 1))
+ let top = cellY + 2 + int ((roll >>> 32) % uint64 (maxOffY + 1))
+ let isPaddy = (roll >>> 40) % 2UL = 0UL
+ let free =
+ left >= 1
+ && top >= 1
+ && left + fw - 1 <= w - 2
+ && top + fh - 1 <= h - 2
+ && [ for dy in 0 .. fh - 1 do
+ for dx in 0 .. fw - 1 -> (left + dx, top + dy) ]
+ |> List.forall (fun (x, y) -> freeGround x y 3)
+ if free then
+ let code = if isPaddy then int GroundTile.PaddyField else int GroundTile.VegetablePlot
+ for dy in 0 .. fh - 1 do
+ for dx in 0 .. fw - 1 do
+ tiles.[idx (left + dx) (top + dy)] <- code
+ true
+ else
+ false
+ let tryGrove (cellX: int) (cellY: int) (roll: uint64) =
+ let gw = 2 + int ((roll >>> 8) % 3UL) // 2..4
+ let gh = 1 + int ((roll >>> 12) % 2UL) // 1..2
+ let left = cellX + 2 + int ((roll >>> 16) % uint64 (max 1 (cellW - 4 - gw + 1)))
+ let top = cellY + 2 + int ((roll >>> 24) % uint64 (max 1 (cellH - 4 - gh + 1)))
+ let kind = if (roll >>> 32) % 2UL = 0UL then BambooClump else ShrubClump
+ let cells = [ for dy in 0 .. gh - 1 do for dx in 0 .. gw - 1 -> (left + dx, top + dy) ]
+ if cells |> List.forall (fun (x, y) -> freeGround x y 2) then
+ groves <- (cells |> List.map (fun (x, y) -> { Kind = kind; X = x; Y = y })) @ groves
+ true
+ else
+ false
+ let tryFarmhouse (cellX: int) (cellY: int) (roll: uint64) =
+ let fw = 2 + int ((roll >>> 8) % 2UL) // 2..3
+ let left = cellX + 2 + int ((roll >>> 12) % uint64 (max 1 (cellW - 4 - fw + 1)))
+ let top = cellY + 2 + int ((roll >>> 20) % uint64 (max 1 (cellH - 4 - 2 + 1)))
+ let doorX = left + fw / 2
+ let doorY = top + 1
+ let free =
+ left >= 1
+ && top >= 1
+ && left + fw - 1 <= w - 2
+ && top + 1 <= h - 2
+ && [ for dy in 0 .. 1 do
+ for dx in 0 .. fw - 1 -> (left + dx, top + dy) ]
+ |> List.forall (fun (x, y) -> freeGround x y 3)
+ if free then
+ farmhouses <-
+ { Left = left
+ Top = top
+ Width = fw
+ Height = 2
+ DoorX = doorX
+ DoorY = doorY }
+ :: farmhouses
+ addPath doorX doorY
+ // 屋后小菜畦 2 行(留在本网格内,避免与邻格重叠)。
+ let gardenFree =
+ top + 3 <= h - 2
+ && [ for dy in 0 .. 1 do
+ for dx in 0 .. fw - 1 -> (left + dx, top + 2 + dy) ]
+ |> List.forall (fun (x, y) -> freeGround x y 3)
+ if gardenFree then
+ for dy in 0 .. 1 do
+ for dx in 0 .. fw - 1 do
+ tiles.[idx (left + dx) (top + 2 + dy)] <- int GroundTile.VegetablePlot
+ true
+ else
+ false
+ let mutable cellY = 0
+ while cellY < h - 2 do
+ let mutable cellX = 0
+ while cellX < w - 2 do
+ let roll = nextCountry ()
+ let bucket = int (roll % 100UL)
+ if bucket < 58 then tryField cellX cellY roll |> ignore
+ elif bucket < 82 then tryGrove cellX cellY roll |> ignore
+ elif bucket < 84 then tryFarmhouse cellX cellY roll |> ignore
+ cellX <- cellX + cellW
+ cellY <- cellY + cellH
// 5c) P26:河岸装饰(护岸石垒/芦苇/垂柳),仅大图启用。纯视觉层,不写入任何瓦片,
// 故 64x48/256x192 的 tiles checksum 与默认世界完全不受影响;装饰只落在紧邻水面的
@@ -510,7 +742,7 @@ module MapGen =
let visited = flood (fst spawns.Head) (snd spawns.Head)
let spawnsOk = spawns |> List.forall (fun (x, y) -> visited.[idx x y])
- let doorsOk = buildings |> List.forall (fun b -> visited.[idx b.DoorX b.DoorY])
+ let doorsOk = (buildings @ farmhouses) |> List.forall (fun b -> visited.[idx b.DoorX b.DoorY])
let bridgesOk = bridges |> List.forall (fun (x, y) -> visited.[idx x y])
let crossingsOk =
bridgeColumns
@@ -528,6 +760,9 @@ module MapGen =
Paths = paths
Buildings = buildings
Decorations = decorations
+ Farmhouses = farmhouses
+ Groves = groves
+ MainRoadCenter = roadCenter
ReachableTiles = reachableTiles
ReachabilityOk = spawnsOk && doorsOk && bridgesOk
BridgeCrossingsOk = crossingsOk }
@@ -567,6 +802,25 @@ module MapGen =
.Append(decorCode decoration.Kind).Append(':')
.Append(decoration.X).Append(':').Append(decoration.Y).Append(';')
|> ignore
+ builder.Append('|') |> ignore
+ for building in map.Farmhouses do
+ builder
+ .Append(building.Left).Append(':').Append(building.Top).Append(':')
+ .Append(building.Width).Append(':').Append(building.Height).Append(':')
+ .Append(building.DoorX).Append(':').Append(building.DoorY).Append(';')
+ |> ignore
+ builder.Append('|') |> ignore
+ let groveCode = function
+ | BambooClump -> 0
+ | ShrubClump -> 1
+ for grove in map.Groves do
+ builder
+ .Append(groveCode grove.Kind).Append(':')
+ .Append(grove.X).Append(':').Append(grove.Y).Append(';')
+ |> ignore
+ builder.Append('|') |> ignore
+ for y in map.MainRoadCenter do
+ builder.Append(y).Append(',') |> ignore
builder.ToString()
/// 可行走判定:非水,或位于桥面上(桥面仍标记为水但可通行)。
diff --git a/src/LivingVillage.Desktop/VillageArt.fs b/src/LivingVillage.Desktop/VillageArt.fs
index 3e52546..4faa886 100644
--- a/src/LivingVillage.Desktop/VillageArt.fs
+++ b/src/LivingVillage.Desktop/VillageArt.fs
@@ -656,7 +656,7 @@ module VillageArt =
spriteBatch.Draw(textures.TileAtlas, XnaRectangle(tileX x, tileY y, cell, cell), tileSourceRectangle sprite, XnaColor.White)
for (x, y) in map.Bridges do
spriteBatch.Draw(textures.TileAtlas, XnaRectangle(tileX x, tileY y, cell, cell), bridgeSource, XnaColor.White)
- for building in map.Buildings do
+ let drawHouse (building: MapGen.Building) =
let left = building.Left
let right = building.Left + building.Width - 1
match houseFacadeVariant building with
@@ -690,6 +690,11 @@ module VillageArt =
let wallSprite =
if x = building.DoorX then CourtyardDoorSprite else WallLatticeSprite
drawCell wallSprite x building.DoorY
+ for building in map.Buildings do
+ drawHouse building
+ // P30 散落农舍与沿河民居共用同一套立面纯函数。
+ for farmhouse in map.Farmhouses do
+ drawHouse farmhouse
// P26 河岸装饰:护岸石垒 / 芦苇 / 垂柳(垂柳占上下两格),只在大图生成。
for decoration in map.Decorations do
match decoration.Kind with
@@ -698,6 +703,11 @@ module VillageArt =
| MapGen.RiverDecorationKind.Willow ->
drawCell WillowTopSprite decoration.X decoration.Y
drawCell WillowBaseSprite decoration.X (decoration.Y + 1)
+ // P30 乡村树丛:竹丛 / 灌木丛(纯视觉,不写瓦片、不影响通行)。
+ for grove in map.Groves do
+ match grove.Kind with
+ | MapGen.BambooClump -> drawCell BambooSprite grove.X grove.Y
+ | MapGen.ShrubClump -> drawCell ShrubSprite grove.X grove.Y
/// 桥头灯笼瓦片:每座桥每列的最上/最下一格(桥两端),用于挂灯笼与夜间暖光。
let bridgeLanternTiles (map: MapGen.Result) : (int * int) list =
@@ -710,10 +720,11 @@ module VillageArt =
/// P29 夜景灯光源瓦片:桥头灯笼 + 民居门窗暖光(确定性纯函数,便于单测)。
/// 每户至少门格发光;变体 A 再取第一扇横向窗,变体 B(整面格栅窗)与门同格故不另加。
+ /// P30:散落农舍的门窗暖光一并计入。
let nightLightTiles (map: MapGen.Result) : (int * int) list =
let lanterns = bridgeLanternTiles map
let houseLights =
- map.Buildings
+ (map.Buildings @ map.Farmhouses)
|> List.collect (fun building ->
let windowTiles =
if houseFacadeVariant building = 0 then
@@ -772,6 +783,9 @@ module VillageArt =
match code with
| 1 -> draw position (waterSpriteAt position)
| 2 -> draw position StonePathSprite
+ // P30 乡村空区:水田(4)走静态水面素材、菜畦(5)走菜地素材;均为成熟 tile。
+ | 4 -> draw position WaterSprite
+ | 5 -> draw position GardenSprite
| _ -> draw position GrassSprite
let drawBaseTile (position: TilePosition) =
@@ -907,6 +921,9 @@ module VillageArt =
match code with
| 1 -> WaterSprite
| 2 -> StonePathSprite
+ // P30 乡村空区:水田走静态水面、菜畦走菜地素材。
+ | 4 -> WaterSprite
+ | 5 -> GardenSprite
| _ -> GrassSprite
for y in 0 .. map.Height - 1 do
for x in 0 .. map.Width - 1 do
@@ -942,6 +959,8 @@ module VillageArt =
| 1 -> WaterSpriteB
| _ -> WaterSpriteC
| 2 -> StonePathSprite
+ | 4 -> WaterSprite
+ | 5 -> GardenSprite
| _ -> GrassSprite
let mutable drawn = 0
for y in y0 .. y1 do
@@ -954,6 +973,42 @@ module VillageArt =
drawNightGlows spriteBatch pixel map (4 * Sim.tilePixels) (int (150.0f * glowScale)) tileX tileY Sim.tilePixels
drawn
+ /// P30 证据用:把地图的任意 tile 矩形 [tileX0,tileY0,tilesW,tilesH] 以 cell px/tile 真实
+ /// 离屏渲染到 target(含民居/田块/树丛/桥/河岸装饰与可选夜光),返回绘制瓦片数。
+ /// 与 drawMapFitted/drawMapViewport 共用 drawMapStructures / drawNightGlows 纯函数。
+ let drawMapRegion
+ (spriteBatch: SpriteBatch)
+ (textures: ArtTextures)
+ (pixel: Texture2D)
+ (tileX0: int)
+ (tileY0: int)
+ (tilesW: int)
+ (tilesH: int)
+ (cell: int)
+ (map: MapGen.Result)
+ (tint: XnaColor)
+ (glowScale: float32) : int =
+ let tileX x = (x - tileX0) * cell
+ let tileY y = (y - tileY0) * cell
+ let spriteOf code =
+ match code with
+ | 1 -> WaterSprite
+ | 2 -> StonePathSprite
+ | 4 -> WaterSprite
+ | 5 -> GardenSprite
+ | _ -> GrassSprite
+ let mutable drawn = 0
+ for y in tileY0 .. tileY0 + tilesH - 1 do
+ for x in tileX0 .. tileX0 + tilesW - 1 do
+ if x >= 0 && x < map.Width && y >= 0 && y < map.Height then
+ let code = map.Tiles.[y * map.Width + x]
+ spriteBatch.Draw(textures.TileAtlas, XnaRectangle(tileX x, tileY y, cell, cell), tileSourceRectangle (spriteOf code), tint)
+ 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
+ drawn
+
let drawCharacter
(spriteBatch: SpriteBatch)
(textures: ArtTextures)