summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/LivingVillage.Desktop.Tests/MapGenTests.fs15
-rw-r--r--src/LivingVillage.Desktop.Tests/PrototypeTests.fs53
-rw-r--r--src/LivingVillage.Desktop/Assets/jiangnan-world.pngbin4468 -> 4492 bytes
-rw-r--r--src/LivingVillage.Desktop/Game.fs49
-rw-r--r--src/LivingVillage.Desktop/MapGen.fs23
-rw-r--r--src/LivingVillage.Desktop/VillageArt.fs23
6 files changed, 140 insertions, 23 deletions
diff --git a/src/LivingVillage.Desktop.Tests/MapGenTests.fs b/src/LivingVillage.Desktop.Tests/MapGenTests.fs
index 6486dd7..b737707 100644
--- a/src/LivingVillage.Desktop.Tests/MapGenTests.fs
+++ b/src/LivingVillage.Desktop.Tests/MapGenTests.fs
@@ -256,6 +256,21 @@ 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 锁定不变。
+ [<TestMethod>]
+ member _.LargeMapHouseDensityHierarchyOnlyGrowsLargeMaps () =
+ Assert.AreEqual<int>(16, (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)
+ 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)
+
[<TestMethod>]
member _.DefaultSizeHouseCountUnchanged () =
Assert.AreEqual<int>(4, (MapGen.generateWithSize 64 48 (uint64 4242)).Buildings.Length)
diff --git a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
index bc3425a..7f67280 100644
--- a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
+++ b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
@@ -1171,6 +1171,59 @@ type PrototypeTests () =
Assert.IsTrue((atlasWidth = VillageArt.worldAtlasWidth), "world atlas width must match 32 x 32px slots")
Assert.IsTrue((atlasHeight = VillageArt.worldAtlasHeight), "world atlas height must stay 32px")
+ /// P28:防复发。第二变体(P26)在 512 整图 ~2px/tile 下不可辨,根因是挑檐与第一变体同为
+ /// 近黑黛瓦。此回归直接解码 world 图集,断言变体 B 的挑檐(slot 28)平均亮度显著高于
+ /// 变体 A 的屋脊(slot 8),保证「配色差异」不会在后续编辑中被悄悄抹平。
+ [<TestMethod>]
+ member _.VariantBRoofPaletteIsClearlyLighterThanVariantA () =
+ let worldPath = Path.Combine(AppContext.BaseDirectory, "Assets/jiangnan-world.png")
+ let bytes = File.ReadAllBytes worldPath
+ let be32 offset =
+ (int bytes.[offset] <<< 24)
+ ||| (int bytes.[offset + 1] <<< 16)
+ ||| (int bytes.[offset + 2] <<< 8)
+ ||| int bytes.[offset + 3]
+ Assert.AreEqual<int>(0x89504E47, be32 0)
+ let width = be32 16
+ let height = be32 20
+ Assert.AreEqual<int>(VillageArt.worldAtlasWidth, width)
+ Assert.AreEqual<int>(VillageArt.worldAtlasHeight, height)
+ // Collect IDAT and inflate; the baked atlas is 8-bit RGBA, non-interlaced, filter 0.
+ let mutable offset = 8
+ use idat = new MemoryStream()
+ while offset < bytes.Length do
+ let length = be32 offset
+ let tag = System.Text.Encoding.ASCII.GetString(bytes, offset + 4, 4)
+ if tag = "IDAT" then idat.Write(bytes, offset + 8, length)
+ offset <- offset + 12 + length
+ idat.Position <- 0L
+ use inflater = new System.IO.Compression.ZLibStream(idat, System.IO.Compression.CompressionMode.Decompress)
+ use raw = new MemoryStream()
+ inflater.CopyTo raw
+ let data = raw.ToArray()
+ let stride = width * 4 + 1
+ let pixel (x: int) (y: int) =
+ let rowStart = y * stride
+ Assert.AreEqual<byte>(0uy, data.[rowStart])
+ let p = rowStart + 1 + x * 4
+ (int data.[p], int data.[p + 1], int data.[p + 2], int data.[p + 3])
+ let slotMeanBrightness (slot: int) =
+ let mutable sum = 0.0
+ let mutable count = 0
+ for y in 0 .. height - 1 do
+ for x in slot * 32 .. slot * 32 + 31 do
+ let (r, g, b, a) = pixel x y
+ if a > 0 then
+ sum <- sum + float (r + g + b) / 3.0
+ count <- count + 1
+ Assert.IsTrue(count > 0, sprintf "slot %d must have opaque pixels" slot)
+ sum / float count
+ let roofA = slotMeanBrightness 8 // RoofRidgeSprite: variant A near-black 黛瓦
+ let roofB = slotMeanBrightness 28 // RoofEaveOverhangSprite: variant B light blue-grey 瓦
+ Assert.IsTrue(
+ roofB > roofA + 15.0,
+ sprintf "variant B roof must be clearly lighter than variant A (A=%.1f B=%.1f)" roofA roofB)
+
[<TestMethod>]
member _.CjkGlyphManifestIsUniqueAndMatchesAtlasGrid () =
// 防复发:图集用字增长时,清单必须非空、每行恰 1 个字符、字符唯一,
diff --git a/src/LivingVillage.Desktop/Assets/jiangnan-world.png b/src/LivingVillage.Desktop/Assets/jiangnan-world.png
index 11145a8..b67dd01 100644
--- a/src/LivingVillage.Desktop/Assets/jiangnan-world.png
+++ b/src/LivingVillage.Desktop/Assets/jiangnan-world.png
Binary files differ
diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs
index 591d1b9..38d9e94 100644
--- a/src/LivingVillage.Desktop/Game.fs
+++ b/src/LivingVillage.Desktop/Game.fs
@@ -296,6 +296,18 @@ type LivingVillageGame() as this =
match Environment.GetEnvironmentVariable("LV_MAPGEN_SEED") with
| null | "" -> 4242UL
| v -> match UInt64.TryParse v with (true, value) -> value | _ -> 4242UL
+ // P28 证据钩子:额外把整图按指定像素目标渲染一次(用于 1 tile≈2px 的缩放可读性取证)。
+ let mapGenFitWidth =
+ match Environment.GetEnvironmentVariable("LV_MAPGEN_FIT_WIDTH") with
+ | null | "" -> 0
+ | v -> match Int32.TryParse v with (true, value) when value > 0 -> value | _ -> 0
+ let mapGenFitHeight =
+ match Environment.GetEnvironmentVariable("LV_MAPGEN_FIT_HEIGHT") with
+ | null | "" -> 0
+ | 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"
+ 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
@@ -862,16 +874,28 @@ type LivingVillageGame() as this =
this.Exit()
/// P20 证据:把一次 MapGen 绘制渲染到指定尺寸离屏 target,返回绘制瓦片数与纹理。
- member private this.RenderMapGen (targetWidth: int) (targetHeight: int) (camX: int) (camY: int) (drawFitted: bool) : int * Texture2D =
+ /// P28:可传入世界 tint 与可选 veil tick(夜景取证用)。
+ member private this.RenderMapGen (targetWidth: int) (targetHeight: int) (camX: int) (camY: int) (drawFitted: bool) (tint: Color) (veilTick: int64 option) : int * Texture2D =
use target = new RenderTarget2D(this.GraphicsDevice, targetWidth, targetHeight)
this.GraphicsDevice.SetRenderTarget target
this.GraphicsDevice.Clear(Color(18, 26, 20))
spriteBatch.Begin()
let drawn =
if drawFitted then
- VillageArt.drawMapFitted spriteBatch artTextures pixel targetWidth targetHeight mapGenResult Color.White
+ VillageArt.drawMapFitted spriteBatch artTextures pixel targetWidth targetHeight mapGenResult tint
else
- VillageArt.drawMapViewport spriteBatch artTextures pixel (Vector2(float32 camX, float32 camY)) targetWidth targetHeight mapGenResult 0L Color.White
+ VillageArt.drawMapViewport spriteBatch artTextures pixel (Vector2(float32 camX, float32 camY)) targetWidth targetHeight mapGenResult 0L tint
+ // 与 DrawWorldView 同一套纯函数 veil,保证取证夜景=真实渲染夜景。
+ 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)
@@ -885,16 +909,27 @@ type LivingVillageGame() as this =
System.IO.Directory.CreateDirectory recordDirectory |> ignore
let viewport = this.GraphicsDevice.Viewport
let mapCell = 6
- let fittedDrawn, fittedTexture = this.RenderMapGen (mapGenResult.Width * mapCell) (mapGenResult.Height * mapCell) 0 0 true
+ let mapTint = if mapGenNight then (M6Presentation.profileAtTick mapGenNightTick).WorldTint else Color.White
+ let mapVeil = if mapGenNight then Some mapGenNightTick else None
+ let fittedDrawn, fittedTexture = this.RenderMapGen (mapGenResult.Width * mapCell) (mapGenResult.Height * mapCell) 0 0 true mapTint mapVeil
use _fitted = fittedTexture
use stream = System.IO.File.Create(sprintf "%s/p20b-map-%dx%d-seed%d.png" recordDirectory mapGenResult.Width mapGenResult.Height (int mapGenResult.Seed))
fittedTexture.SaveAsPng(stream, fittedTexture.Width, fittedTexture.Height)
- printfn "mapgen-fitted drawn=%d png=%dx%d" fittedDrawn fittedTexture.Width fittedTexture.Height
+ printfn "mapgen-fitted drawn=%d png=%dx%d night=%b" fittedDrawn fittedTexture.Width fittedTexture.Height mapGenNight
+
+ // P28:整图按指定像素目标再渲染一次(真实帧)。1024x768 -> 512x384 图恰为 2px/tile,
+ // 用于证明两民居变体在「整图 1 tile≈2px」缩放下肉眼可分。
+ if mapGenFitWidth > 0 && mapGenFitHeight > 0 then
+ let fitDrawn, fitTexture = this.RenderMapGen mapGenFitWidth mapGenFitHeight 0 0 true mapTint mapVeil
+ use _fit = fitTexture
+ use fitStream = System.IO.File.Create(sprintf "%s/p28-fit-%dx%d-seed%d.png" recordDirectory mapGenFitWidth mapGenFitHeight (int mapGenResult.Seed))
+ fitTexture.SaveAsPng(fitStream, mapGenFitWidth, mapGenFitHeight)
+ printfn "mapgen-fit drawn=%d png=%dx%d night=%b" fitDrawn mapGenFitWidth mapGenFitHeight mapGenNight
let clampRange maximum value = max 0 (min (max 0 maximum) value)
let camX = clampRange (mapGenResult.Width * Sim.tilePixels - viewport.Width) ((mapGenResult.Width * Sim.tilePixels - viewport.Width) / 2)
let camY = clampRange (mapGenResult.Height * Sim.tilePixels - viewport.Height) ((mapGenResult.Height * Sim.tilePixels - viewport.Height) / 2)
- let culledDrawn, culledTexture = this.RenderMapGen viewport.Width viewport.Height camX camY false
+ let culledDrawn, culledTexture = this.RenderMapGen viewport.Width viewport.Height camX camY false Color.White None
use _culled = culledTexture
use culledStream = System.IO.File.Create(sprintf "%s/p20b-culled-%dx%d-seed%d.png" recordDirectory mapGenResult.Width mapGenResult.Height (int mapGenResult.Seed))
culledTexture.SaveAsPng(culledStream, viewport.Width, viewport.Height)
@@ -906,7 +941,7 @@ type LivingVillageGame() as this =
| (bx, by) :: _ ->
let detailCamX = clampRange (mapGenResult.Width * Sim.tilePixels - viewport.Width) (bx * Sim.tilePixels - viewport.Width / 2)
let detailCamY = clampRange (mapGenResult.Height * Sim.tilePixels - viewport.Height) (by * Sim.tilePixels - viewport.Height / 2)
- let detailDrawn, detailTexture = this.RenderMapGen viewport.Width viewport.Height detailCamX detailCamY false
+ let detailDrawn, detailTexture = this.RenderMapGen viewport.Width viewport.Height detailCamX detailCamY false Color.White None
use _detail = detailTexture
use detailStream = System.IO.File.Create(sprintf "%s/p20b-detail-%dx%d-seed%d.png" recordDirectory mapGenResult.Width mapGenResult.Height (int mapGenResult.Seed))
detailTexture.SaveAsPng(detailStream, detailTexture.Width, detailTexture.Height)
diff --git a/src/LivingVillage.Desktop/MapGen.fs b/src/LivingVillage.Desktop/MapGen.fs
index ce9fb3a..3705635 100644
--- a/src/LivingVillage.Desktop/MapGen.fs
+++ b/src/LivingVillage.Desktop/MapGen.fs
@@ -97,6 +97,7 @@ module MapGen =
/// P23 尺寸自适应参数:河道数量随高度增长(约每 96 行一条),其余沿用默认。
/// 64x48 / 256x192 仍得到 2 条河道,与既有输出逐字节一致;512x384 得到 4 条。
/// P24:仅对大于 256x192 的图追加沿河民居(泊松散布),保证既有 checksum 不变。
+ /// P28:大图沿河民居由 10 增至 16(+6 座,宽度 2/3/4 变化),默认图仍为 0、不消耗 RNG。
let paramsForSize (width: int) (height: int) (seed: uint64) : Params =
let base_ = defaultParams seed
let large = width > 256 || height > 192
@@ -104,7 +105,7 @@ module MapGen =
Width = width
Height = height
RiverCount = max base_.RiverCount (max 1 (height / 96))
- ExtraRiversideHouses = (if large then 10 else 0)
+ ExtraRiversideHouses = (if large then 16 else 0)
DecorativeStones = not large
RiverDecorations = large }
@@ -307,18 +308,20 @@ module MapGen =
// 5b) P24:大图沿河民居加密。从既有路网瓦片出发,向四邻定向落位 3x2 白墙黛瓦民居,
// 门开向相邻路格并接入路网(BFS 可达);仅在 ExtraRiversideHouses>0 时执行,
// 且只在核心区外的草地落位,故 64x48/256x192 输出逐字节不变。
+ // P28:宽度按 2/3/4 加权变化(2=附属小筑、4=大宅),形成聚落层次而非等距重复;
+ // 数量 10 -> 16(沿河再增 6 座)。仍只在大图分支执行,默认图 RNG 不消耗,checksum 不变。
if p.ExtraRiversideHouses > 0 then
- let footprintFree (left: int) (top: int) =
- left >= 1 && left + 2 <= w - 2 && top >= 1 && top + 1 <= h - 2
- && [ for dx in 0 .. 2 do
+ let footprintFree (left: int) (top: int) (width: int) =
+ left >= 1 && left + width - 1 <= w - 2 && top >= 1 && top + 1 <= h - 2
+ && [ for dx in 0 .. width - 1 do
for dy in 0 .. 1 -> (left + dx, top + dy) ]
|> List.forall (fun (x, y) ->
tiles.[idx x y] = int GroundTile.Grass
&& not (inCore x y))
- let overlaps (left: int) (top: int) =
+ let overlaps (left: int) (top: int) (width: int) =
buildings
|> List.exists (fun b ->
- left < b.Left + b.Width && b.Left < left + 3
+ left < b.Left + b.Width && b.Left < left + width
&& top < b.Top + b.Height && b.Top < top + 2)
let directions = [ (0, -1); (0, 1); (-1, 0); (1, 0) ]
let mutable extraPlaced = 0
@@ -331,15 +334,17 @@ module MapGen =
if paths.Length > 0 then
let pathX, pathY = List.item (int (raw % uint64 paths.Length)) paths
let dirX, dirY = directions.[int ((raw >>> 32) % 4UL)]
+ let widthRoll = int ((raw >>> 40) % 10UL)
+ let buildingWidth = if widthRoll < 5 then 3 elif widthRoll < 8 then 2 else 4
let doorX = pathX + dirX
let doorY = pathY + dirY
- let left = doorX - 1
+ let left = doorX - buildingWidth / 2
let top = doorY - 1
- if footprintFree left top && not (overlaps left top) then
+ if footprintFree left top buildingWidth && not (overlaps left top buildingWidth) then
let building =
{ Left = left
Top = top
- Width = 3
+ Width = buildingWidth
Height = 2
DoorX = doorX
DoorY = doorY }
diff --git a/src/LivingVillage.Desktop/VillageArt.fs b/src/LivingVillage.Desktop/VillageArt.fs
index d2fcc1a..9a56de4 100644
--- a/src/LivingVillage.Desktop/VillageArt.fs
+++ b/src/LivingVillage.Desktop/VillageArt.fs
@@ -612,7 +612,8 @@ module VillageArt =
spriteBatch.Draw(pixel, XnaRectangle(x, y, max 1 w, max 1 h), color)
/// P26 民居立面变体:按位置确定性交替两种立面(沿河沿路分布)。
- /// 0 = 黛瓦脊 + 木墙/窗 + 门(P24 原样);1 = 挑檐挑出 + 木格栅窗 + 院门。
+ /// 0 = 黛瓦脊 + 木墙/窗 + 门(P24 原样);1 = 亮青灰挑檐(多叠一层、向两侧各出一格)+
+ /// 竖长木格栅窗 + 绿院门(P28 强化配色与轮廓,保证 512 整图 ~2px/tile 下两变体可分)。
/// 纯函数、不改变任何瓦片/可达性语义;64x48 与 256x192 的 tiles checksum 不受影响。
let houseFacadeVariant (building: MapGen.Building) : int =
let remainder = (building.Left + building.Top) % 2
@@ -649,14 +650,22 @@ module VillageArt =
else WallWoodSprite
drawCell wallSprite x building.DoorY
| _ ->
- // 变体 B(P26):挑檐向两侧各出一格 + 木格栅窗 + 院门。
- for x in (left - 1) .. (right + 1) do
- drawCell RoofEaveOverhangSprite x building.Top
+ // 变体 B(P26/P28):亮青灰挑檐向两侧各出一格 + 竖长木格栅窗 + 绿院门。
+ // P28 再向上叠一层挑檐,使 B 的轮廓比 A 高一格;水面之上不叠,避免屋顶压水。
+ let isWater x y =
+ x >= 0 && x < map.Width && y >= 0 && y < map.Height
+ && map.Tiles.[y * map.Width + x] = int MapGen.GroundTile.Water
+ for row in building.Top .. building.DoorY - 1 do
+ for x in (left - 1) .. (right + 1) do
+ drawCell RoofEaveOverhangSprite x row
+ let upper = building.Top - 1
+ if upper >= 0 then
+ for x in (left - 1) .. (right + 1) do
+ if not (isWater x upper) then
+ drawCell RoofEaveOverhangSprite x upper
for x in left .. right do
let wallSprite =
- if x = building.DoorX then CourtyardDoorSprite
- elif (x - left) % 2 = 0 then WallLatticeSprite
- else WallWoodSprite
+ if x = building.DoorX then CourtyardDoorSprite else WallLatticeSprite
drawCell wallSprite x building.DoorY
// P26 河岸装饰:护岸石垒 / 芦苇 / 垂柳(垂柳占上下两格),只在大图生成。
for decoration in map.Decorations do