From 92bcc9b9fc4eeea4f11913a0de2a34811b262dfd Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Tue, 22 Sep 2026 12:41:06 +0800 Subject: feat(desktop): P30 512 民居增密 + 乡村空区填充 + 主路柔化 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 锁定不变。 --- src/LivingVillage.Desktop/Game.fs | 127 +++++++++++++++++++++++++++++++++++++- 1 file changed, 126 insertions(+), 1 deletion(-) (limited to 'src/LivingVillage.Desktop/Game.fs') 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 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 (targetWidth * targetHeight) + target.GetData(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() -- cgit v1.2.3