diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-22 12:41:06 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-22 12:41:06 +0800 |
| commit | 92bcc9b9fc4eeea4f11913a0de2a34811b262dfd (patch) | |
| tree | 7fa25b2a3fc2ebeea7b2fe2cbc6285848d7c0a26 /src | |
| parent | 4034f55ee0b9cbd8c9db5f850e10e21e15fb7274 (diff) | |
| download | living-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')
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/MapGenTests.fs | 112 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/Game.fs | 127 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/MapGen.fs | 346 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/VillageArt.fs | 59 |
4 files changed, 590 insertions, 54 deletions
diff --git a/src/LivingVillage.Desktop.Tests/MapGenTests.fs b/src/LivingVillage.Desktop.Tests/MapGenTests.fs index d83a144..3ff2904 100644 --- a/src/LivingVillage.Desktop.Tests/MapGenTests.fs +++ b/src/LivingVillage.Desktop.Tests/MapGenTests.fs @@ -36,6 +36,25 @@ type MapGenTests () = let nx, ny = x + dx, y + dy if nx >= 0 && nx < map.Width && ny >= 0 && ny < map.Height && map.Tiles.[ny * map.Width + nx] = int MapGen.GroundTile.Water then 1 else 0) + /// P30:把民居按「足迹邻近(两轴间隙 <=2 格)」聚成组团,返回各组规模。 + let houseGroups (houses: MapGen.Building list) : int list = + let arr = houses |> List.sortBy (fun b -> b.Top, b.Left) |> Array.ofList + let n = arr.Length + let parent = Array.init n id + let rec find i = if parent.[i] = i then i else (parent.[i] <- find parent.[i]; parent.[i]) + let union a b = + let ra = find a + let rb = find b + if ra <> rb then parent.[ra] <- rb + let near (a: MapGen.Building) (b: MapGen.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 + for i in 0 .. n - 1 do + for j in i + 1 .. n - 1 do + if near arr.[i] arr.[j] then union i j + [ for i in 0 .. n - 1 -> find i ] |> List.groupBy id |> List.map (fun (_, group) -> group.Length) + [<TestMethod>] member _.SameSeedGeneratesByteIdenticalTerrain () = let a = MapGen.generateWithSize 64 48 (uint64 4242) @@ -256,20 +275,20 @@ type MapGenTests () = Assert.IsTrue(a.Buildings = b.Buildings) Assert.AreEqual<string>(MapGen.serialize a, MapGen.serialize b) - /// P28:512 沿河民居由 10 -> 16 增密(+6),并出现 2/3/4 三档宽度(附属小筑/民居/大宅), - /// 形成聚落层次;默认图仍为 0,不消耗 RNG,故 64/256 checksum 锁定不变。 + /// P28/P30:512 沿河民居增密并出现 2/3/4 三档宽度(附属小筑/民居/大宅),形成聚落层次; + /// 默认图仍为 0,不消耗 RNG,故 64/256 checksum 锁定不变。 [<TestMethod>] member _.LargeMapHouseDensityHierarchyOnlyGrowsLargeMaps () = - Assert.AreEqual<int>(16, (MapGen.paramsForSize 512 384 (uint64 1)).ExtraRiversideHouses) + Assert.AreEqual<int>(30, (MapGen.paramsForSize 512 384 (uint64 1)).ExtraRiversideHouses) Assert.AreEqual<int>(0, (MapGen.paramsForSize 64 48 (uint64 1)).ExtraRiversideHouses) Assert.AreEqual<int>(0, (MapGen.paramsForSize 256 192 (uint64 1)).ExtraRiversideHouses) let map = MapGen.generateWithSize 512 384 (uint64 4242) - Assert.IsTrue(map.Buildings.Length >= 22, sprintf "P28 512 must place >=22 houses, saw %d" map.Buildings.Length) + Assert.IsTrue(map.Buildings.Length >= 28, sprintf "P30 512 must place >=28 houses, saw %d" map.Buildings.Length) let widths = map.Buildings |> List.map (fun b -> b.Width) |> Set.ofList Assert.IsTrue(widths.Contains 2, "outbuilding width 2 must appear") Assert.IsTrue(widths.Contains 3, "house width 3 must appear") Assert.IsTrue(widths.Contains 4, "hall width 4 must appear") - Assert.AreEqual<int>(24, map.Buildings.Length) + Assert.IsTrue(map.Buildings.Length + map.Farmhouses.Length >= 32, "total houses (clusters + farmsteads) must be >=32") [<TestMethod>] member _.DefaultSizeHouseCountUnchanged () = @@ -439,6 +458,89 @@ type MapGenTests () = let c = MapGen.generateWithSize 512 384 (uint64 7) Assert.AreNotEqual<string>(MapGen.serialize a, MapGen.serialize c) + // ---- P30:512 民居增密 + 组团规模 + 乡村空区填充 + 主路柔化 ---- + + [<TestMethod>] + member _.LargeMapGroupsAreTwoToFiveWithFewIsolated () = + let map = MapGen.generateWithSize 512 384 (uint64 4242) + let groups = houseGroups map.Buildings + Assert.IsTrue(groups.Length > 0, "512 must form house groups") + let singletons = groups |> List.filter (fun n -> n < 2) |> List.length + Assert.IsTrue(singletons <= 2, sprintf "isolated single-house groups must be <=2, saw %d" singletons) + let inRange = groups |> List.filter (fun n -> n >= 2 && n <= 5) |> List.length + Assert.IsTrue(inRange * 2 >= groups.Length, "most groups must hold 2-5 houses") + // 另一 seed 同样不产孤立组,验证不是对 4242 的过拟合。 + let other = houseGroups (MapGen.generateWithSize 512 384 (uint64 7)).Buildings + Assert.IsTrue((other |> List.filter (fun n -> n < 2) |> List.length) <= 2) + + [<TestMethod>] + member _.LargeMapCountrysideFillsEmptyBandsWithMatureTiles () = + let map = MapGen.generateWithSize 512 384 (uint64 4242) + let isField code = code = int MapGen.GroundTile.PaddyField || code = int MapGen.GroundTile.VegetablePlot + let paddy = map.Tiles |> Array.filter (fun c -> c = int MapGen.GroundTile.PaddyField) |> Array.length + let veg = map.Tiles |> Array.filter (fun c -> c = int MapGen.GroundTile.VegetablePlot) |> Array.length + Assert.IsTrue(paddy > 0, "paddy fields must appear on 512") + Assert.IsTrue(veg > 0, "vegetable plots must appear on 512") + Assert.IsTrue(paddy + veg > 4000, sprintf "fields must cover a meaningful share, saw %d" (paddy + veg)) + Assert.IsTrue(map.Groves.Length > 0, "tree groves must appear") + Assert.IsTrue(map.Farmhouses.Length > 0, "scattered farmsteads must appear") + // 上部/下部空带都要有田块,避免大面积纯草。 + let topBand = [ for y in 0 .. 90 do for x in 0 .. map.Width - 1 -> map.Tiles.[y * map.Width + x] ] |> List.filter isField |> List.length + let bottomBand = [ for y in 296 .. map.Height - 1 do for x in 0 .. map.Width - 1 -> map.Tiles.[y * map.Width + x] ] |> List.filter isField |> List.length + Assert.IsTrue(topBand > 100, sprintf "top band must contain fields, saw %d" topBand) + Assert.IsTrue(bottomBand > 100, sprintf "bottom band must contain fields, saw %d" bottomBand) + // 默认图完全不进入乡村分支。 + let small = MapGen.generateWithSize 256 192 (uint64 4242) + Assert.AreEqual<int>(0, small.Groves.Length) + Assert.AreEqual<int>(0, small.Farmhouses.Length) + Assert.AreEqual<int>(0, small.Tiles |> Array.filter isField |> Array.length) + + [<TestMethod>] + member _.FieldsStayOnLandAwayFromWaterPathsAndCore () = + let map = MapGen.generateWithSize 512 384 (uint64 4242) + let pathSet = System.Collections.Generic.HashSet<int>(map.Paths |> List.map (fun (x, y) -> y * map.Width + x)) + let isField code = code = int MapGen.GroundTile.PaddyField || code = int MapGen.GroundTile.VegetablePlot + for y in 0 .. map.Height - 1 do + for x in 0 .. map.Width - 1 do + let i = y * map.Width + x + if isField map.Tiles.[i] then + Assert.AreNotEqual<int>(int MapGen.GroundTile.Water, map.Tiles.[i]) + Assert.IsFalse(pathSet.Contains i, sprintf "field (%d,%d) must not sit on a path" x y) + let inCore = x >= map.Core.MinX && x <= map.Core.MaxX && y >= map.Core.MinY && y <= map.Core.MaxY + Assert.IsFalse(inCore, sprintf "field (%d,%d) must stay outside the core" x y) + + [<TestMethod>] + member _.LargeMapFarmsteadDoorsAreReachableAndDeterministic () = + let map = MapGen.generateWithSize 512 384 (uint64 4242) + Assert.IsTrue(map.ReachabilityOk, "512 reachability must hold with farmsteads") + let visited = MapGen.floodFill map (fst map.Spawns.Head) (snd map.Spawns.Head) + for farmhouse in map.Farmhouses do + Assert.IsTrue(MapGen.isWalkable map farmhouse.DoorX farmhouse.DoorY, sprintf "farmstead door (%d,%d) must be walkable" farmhouse.DoorX farmhouse.DoorY) + Assert.IsTrue(visited.[farmhouse.DoorY * map.Width + farmhouse.DoorX], sprintf "farmstead door (%d,%d) unreachable" farmhouse.DoorX farmhouse.DoorY) + let other = MapGen.generateWithSize 512 384 (uint64 4242) + Assert.IsTrue(map.Farmhouses = other.Farmhouses) + Assert.IsTrue(map.Groves = other.Groves) + Assert.IsTrue(map.Tiles = other.Tiles) + Assert.AreEqual<string>(MapGen.serialize map, MapGen.serialize other) + + [<TestMethod>] + member _.LargeMapMainRoadIsDeterministicSerpentine () = + let map = MapGen.generateWithSize 512 384 (uint64 4242) + let other = MapGen.generateWithSize 512 384 (uint64 4242) + // 同 seed 两次逐字节一致(含主路中心线)。 + Assert.IsTrue(map.MainRoadCenter = other.MainRoadCenter, "road centerline must be byte-identical across runs") + Assert.AreEqual<string>(MapGen.serialize map, MapGen.serialize other) + // 逐点微扰:相邻列步长 <=1,且相对中段基准确有 >=2 格弯曲。 + map.MainRoadCenter + |> Array.pairwise + |> Array.iter (fun (a, b) -> Assert.IsTrue(abs (b - a) <= 1, sprintf "road step must be <=1, saw %d -> %d" a b)) + let baseY = map.MainRoadCenter.[map.Width / 2] + Assert.IsTrue(map.MainRoadCenter |> Array.exists (fun y -> abs (y - baseY) >= 2), "main road must bend by >=2 tiles") + // 默认图主街仍笔直(不进入新分支)。 + let small = MapGen.generateWithSize 256 192 (uint64 4242) + let smallBase = small.MainRoadCenter.[small.Width / 2] + Assert.IsTrue(small.MainRoadCenter |> Array.forall (fun y -> y = smallBase), "default map road must stay straight") + [<TestMethod>] member _.VisibleRangeClampsAtLegacyMapEdges () = let x0, y0, x1, y1 = MapGen.visibleTileRange 64 48 0 0 2048 1536 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) |
