diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-22 09:21:24 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-22 09:21:24 +0800 |
| commit | 590f3d8d5cc031c0909241b329ad7f7f18035991 (patch) | |
| tree | 40b158de9188a1039d86ad0429cf706b4ac60478 /src | |
| parent | 383ed44d22c318a44e7f54a652cd0f825f3962d7 (diff) | |
| download | living-village-590f3d8d5cc031c0909241b329ad7f7f18035991.tar.gz | |
feat(desktop): P26 民居立面第二变体(挑檐/木格栅窗/院门)沿河沿路交替
Diffstat (limited to 'src')
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/PrototypeTests.fs | 26 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/Assets/jiangnan-world.png | bin | 3847 -> 4147 bytes | |||
| -rw-r--r-- | src/LivingVillage.Desktop/VillageArt.fs | 50 |
3 files changed, 60 insertions, 16 deletions
diff --git a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs index e58139c..a38228f 100644 --- a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs +++ b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs @@ -1104,6 +1104,23 @@ type PrototypeTests () = // 953775FA 硬对齐由 --performance-baseline 于 finalWorld 支线证据。 [<TestMethod>] + member _.HouseFacadeVariantsAlternateAlongTheVillage () = + // P26:512 大图上两种民居立面都要出现,且按位置确定性交替(纯函数)。 + let map = MapGen.generateWithSize 512 384 (uint64 4242) + let variants = map.Buildings |> List.map VillageArt.houseFacadeVariant + Assert.IsTrue(Set.ofList variants |> Set.contains 0, "variant A must appear") + Assert.IsTrue(Set.ofList variants |> Set.contains 1, "variant B must appear") + let repeated = MapGen.generateWithSize 512 384 (uint64 4242) + Assert.AreEqual<(int list)>(variants, repeated.Buildings |> List.map VillageArt.houseFacadeVariant) + // Neighbouring buildings along the same river bank must flip variant, i.e. not one solid run. + let flipped = + variants + |> List.pairwise + |> List.filter (fun (a, b) -> a <> b) + |> List.length + Assert.IsTrue(flipped > 0, "facades must alternate, not stay one variant") + + [<TestMethod>] member _.JiangnanArtAssetsArePackagedAsRealPngFiles () = let requiredAssets = [ "Assets/jiangnan-world.png" @@ -1116,9 +1133,10 @@ type PrototypeTests () = Assert.AreEqual<string list>([], missing, sprintf "Missing Jiangnan PNG assets: %s" (String.concat ", " missing)) - // The baked world atlas must match the 28-slot layout the renderer indexes into - // (P17 added 石灯笼/水缸/摊贩摊位 as slots 25..27). - Assert.AreEqual<int>(28, VillageArt.worldTileCount) + // The baked world atlas must match the 31-slot layout the renderer indexes into + // (P17 added 石灯笼/水缸/摊贩摊位 as slots 25..27; P26 added the variant-B house + // parts 屋檐挑出/木格栅窗/院门 as slots 28..30). + Assert.AreEqual<int>(31, VillageArt.worldTileCount) let worldPath = Path.Combine(AppContext.BaseDirectory, "Assets/jiangnan-world.png") let header = File.ReadAllBytes worldPath let be32 offset = @@ -1128,7 +1146,7 @@ type PrototypeTests () = ||| int header.[offset + 3] let atlasWidth : int = be32 16 let atlasHeight : int = be32 20 - Assert.IsTrue((atlasWidth = VillageArt.worldAtlasWidth), "world atlas width must match 28 x 32px slots") + Assert.IsTrue((atlasWidth = VillageArt.worldAtlasWidth), "world atlas width must match 31 x 32px slots") Assert.IsTrue((atlasHeight = VillageArt.worldAtlasHeight), "world atlas height must stay 32px") [<TestMethod>] diff --git a/src/LivingVillage.Desktop/Assets/jiangnan-world.png b/src/LivingVillage.Desktop/Assets/jiangnan-world.png Binary files differindex 6e97728..ea2866d 100644 --- a/src/LivingVillage.Desktop/Assets/jiangnan-world.png +++ b/src/LivingVillage.Desktop/Assets/jiangnan-world.png diff --git a/src/LivingVillage.Desktop/VillageArt.fs b/src/LivingVillage.Desktop/VillageArt.fs index 12c17ba..f303620 100644 --- a/src/LivingVillage.Desktop/VillageArt.fs +++ b/src/LivingVillage.Desktop/VillageArt.fs @@ -154,6 +154,9 @@ module VillageArt = | StoneLanternSprite | WaterVatSprite | StallSprite + | RoofEaveOverhangSprite + | WallLatticeSprite + | CourtyardDoorSprite type private XnaColor = Microsoft.Xna.Framework.Color type private XnaRectangle = Microsoft.Xna.Framework.Rectangle @@ -496,7 +499,7 @@ module VillageArt = let characterAtlasRelativePath = "Assets/jiangnan-characters.png" let interiorAtlasRelativePath = "Assets/jiangnan-interior.png" - let worldTileCount = 28 + let worldTileCount = 31 let worldAtlasWidth = Sim.tilePixels * worldTileCount let worldAtlasHeight = Sim.tilePixels let characterAtlasWidth = 32 * 5 * 4 * 6 @@ -570,6 +573,9 @@ module VillageArt = | StoneLanternSprite -> 25 | WaterVatSprite -> 26 | StallSprite -> 27 + | RoofEaveOverhangSprite -> 28 + | WallLatticeSprite -> 29 + | CourtyardDoorSprite -> 30 XnaRectangle(index * Sim.tilePixels, 0, Sim.tilePixels, Sim.tilePixels) let private tilePosition x y : TilePosition = @@ -603,6 +609,13 @@ module VillageArt = let private fillRect (spriteBatch: SpriteBatch) (pixel: Texture2D) (color: XnaColor) (x: int) (y: int) (w: int) (h: int) = spriteBatch.Draw(pixel, XnaRectangle(x, y, max 1 w, max 1 h), color) + /// P26 民居立面变体:按位置确定性交替两种立面(沿河沿路分布)。 + /// 0 = 黛瓦脊 + 木墙/窗 + 门(P24 原样);1 = 挑檐挑出 + 木格栅窗 + 院门。 + /// 纯函数、不改变任何瓦片/可达性语义;64x48 与 256x192 的 tiles checksum 不受影响。 + let houseFacadeVariant (building: MapGen.Building) : int = + let remainder = (building.Left + building.Top) % 2 + if remainder < 0 then remainder + 2 else remainder + /// 生成器世界里的桥面与沿河民居(白墙黑瓦色块 + 门);坐标由调用方按相机换算。 let private drawMapStructures (spriteBatch: SpriteBatch) @@ -618,18 +631,31 @@ module VillageArt = for building in map.Buildings do let left = building.Left let right = building.Left + building.Width - 1 - // P24:改用 256x192 同款民宅部件——黛瓦(脊/檐)+ 木墙/窗 + 门。 - for row in building.Top .. building.DoorY - 1 do - let roofSprite = if row = building.Top then RoofRidgeSprite else RoofEaveSprite - let source = tileSourceRectangle roofSprite + let drawCell sprite x y = + spriteBatch.Draw(textures.TileAtlas, XnaRectangle(tileX x, tileY y, cell, cell), tileSourceRectangle sprite, XnaColor.White) + match houseFacadeVariant building with + | 0 -> + // 变体 A(P24):黛瓦(脊/檐)+ 木墙/窗 + 门。 + for row in building.Top .. building.DoorY - 1 do + let roofSprite = if row = building.Top then RoofRidgeSprite else RoofEaveSprite + for x in left .. right do + drawCell roofSprite x row + for x in left .. right do + let wallSprite = + if x = building.DoorX then DoorSprite + elif (x - left) % 2 = 0 then WallWindowSprite + else WallWoodSprite + drawCell wallSprite x building.DoorY + | _ -> + // 变体 B(P26):挑檐向两侧各出一格 + 木格栅窗 + 院门。 + for x in (left - 1) .. (right + 1) do + drawCell RoofEaveOverhangSprite x building.Top for x in left .. right do - spriteBatch.Draw(textures.TileAtlas, XnaRectangle(tileX x, tileY row, cell, cell), source, XnaColor.White) - for x in left .. right do - let wallSprite = - if x = building.DoorX then DoorSprite - elif (x - left) % 2 = 0 then WallWindowSprite - else WallWoodSprite - spriteBatch.Draw(textures.TileAtlas, XnaRectangle(tileX x, tileY building.DoorY, cell, cell), tileSourceRectangle wallSprite, XnaColor.White) + let wallSprite = + if x = building.DoorX then CourtyardDoorSprite + elif (x - left) % 2 = 0 then WallLatticeSprite + else WallWoodSprite + drawCell wallSprite x building.DoorY /// 桥头灯笼瓦片:每座桥每列的最上/最下一格(桥两端),用于挂灯笼与夜间暖光。 let bridgeLanternTiles (map: MapGen.Result) : (int * int) list = |
