summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop/Game.fs
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-22 12:41:06 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-22 12:41:06 +0800
commit92bcc9b9fc4eeea4f11913a0de2a34811b262dfd (patch)
tree7fa25b2a3fc2ebeea7b2fe2cbc6285848d7c0a26 /src/LivingVillage.Desktop/Game.fs
parent4034f55ee0b9cbd8c9db5f850e10e21e15fb7274 (diff)
downloadliving-village-92bcc9b9fc4eeea4f11913a0de2a34811b262dfd.tar.gz
feat(desktop): P30 512 民居增密 + 乡村空区填充 + 主路柔化
- MapGen(仅大图尺寸自适应分支,默认 64x48/256x192 不进入、不消费额外 RNG): ① ExtraRiversideHouses 16 -> 30,组团 2-5 座;含基础岸屋的路段优先布组并为每座孤立 基础民居贴格配对,placeGroup 试排后按「与既有/同组民居足迹间隙 <=2」过滤,孤立单屋不落地。 512 seed4242:组团民居 24 -> 45,规模 [5;4x6;3x2;2x3],0 组 <2 座。 ② 新增 GroundTile.PaddyField=4/VegetablePlot=5(成熟 tile;非水可通行)+ Result.Groves (纯视觉)+ Result.Farmhouses(门贴院落路、计入 BFS);18x14 确定性网格铺田块/树丛/农舍, 离水 >=3 格、避开路网/核心/民居。草地瓦片 96.3% -> 83.9%,水田 11595 + 菜畦 12877 格。 ③ Result.MainRoadCenter 逐列中心线,value noise(scale 130、幅度 ±3)确定性缓弯, 相邻列步长 <=1、遇弯补格保证 4 连通;默认图主街仍笔直。 - VillageArt:水田/菜畦/树丛/农舍渲染(复用成熟 sprite 与同一立面纯函数)、农舍门窗计入夜光、 新增 drawMapRegion 局部真实离屏渲染。 - Game:P30 证据钩子 LV_MAPGEN_P30_SHOT=1(整图 3072x2304 day/night + 田块/主路 512x256@4px)。 - 回归 +5:组团规模/空区填充/田块合规/农舍可达确定性/主路确定性弯曲。 Kernel 92 / Desktop 156 全绿;Release 0 警告 0 错误; final_digest=953775FAEB2F… 3 次一致、performance_determinism=PASS; 64x48=6042052、256x192=1284656962 锁定不变。
Diffstat (limited to 'src/LivingVillage.Desktop/Game.fs')
-rw-r--r--src/LivingVillage.Desktop/Game.fs127
1 files changed, 126 insertions, 1 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()