diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-28 08:33:20 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-28 08:33:20 +0800 |
| commit | 0e9948b7067e07875a9fe81879be0ae46664a785 (patch) | |
| tree | 4c0decedfb6826d7fb26248a4dea25e41a60ffcc /src | |
| parent | d92afbcb787eb91c4e6d001b1f0de04f0fc5813d (diff) | |
| download | living-village-0e9948b7067e07875a9fe81879be0ae46664a785.tar.gz | |
feat(art): P33 接入 CC0 真实像素素材样板(地面+角色竖直切片)
- 修复 make-cc0-art.py 地块取样间距:packed sheet 无间距(16px pitch),旧 17px 误采错格
- 重选草地/石板格 (9,8)/(10,8)/(4,8)/(4,9),整数 2x NEAREST 重建 cc0-tiles.png
- VillageArt 暴露 selectedGround/CharacterAtlasRelativePath 纯函数,保留 LV_ASSET_PACK=cc0
开关与程序绘制江南图集 fallback
- 新增 AssetPackTests:图集加载尺寸、整数倍缩放、开关/回退断言
- docs/素材来源.md 记录来源/许可/sha256/构建/开关;同步 assets-and-licenses 与维护说明 P33 小节
- docs/evidence/p33-atlas-*.png 昼/夜/2x 特写/程序绘制对照(check-no-white-blocks 0 命中)
Diffstat (limited to 'src')
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/AssetPackTests.fs | 73 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj | 1 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/Assets/cc0-tiles.png | bin | 524 -> 388 bytes | |||
| -rw-r--r-- | src/LivingVillage.Desktop/VillageArt.fs | 36 |
4 files changed, 104 insertions, 6 deletions
diff --git a/src/LivingVillage.Desktop.Tests/AssetPackTests.fs b/src/LivingVillage.Desktop.Tests/AssetPackTests.fs new file mode 100644 index 0000000..fc235d0 --- /dev/null +++ b/src/LivingVillage.Desktop.Tests/AssetPackTests.fs @@ -0,0 +1,73 @@ +namespace LivingVillage.Desktop.Tests + +open System +open System.IO +open Microsoft.VisualStudio.TestTools.UnitTesting +open LivingVillage.Kernel +open LivingVillage.Kernel.Sim +open LivingVillage.Desktop.VillageArt + +/// P33 vertical slice: the provenanced CC0 asset pack (Kenney Tiny Farm ground + +/// Puny Characters) must load at the documented 32x32 / 32x48 shapes, be an exact +/// integer 2x of the 16px / 16x24 source sprites, and stay behind the +/// `LV_ASSET_PACK=cc0` switch so the procedural Jiangnan atlas is the fallback. +[<TestClass>] +type AssetPackTests () = + + let assetPath (relative: string) = + Path.Combine(AppContext.BaseDirectory, relative) + + let pngDimensions (path: string) : int * int = + let bytes = File.ReadAllBytes path + let signature = [| 0x89uy; 0x50uy; 0x4Euy; 0x47uy |] + for i in 0 .. 3 do + Assert.IsTrue(signature.[i] = bytes.[i], sprintf "%s is not a PNG" path) + let be32 (offset: int) = + (int bytes.[offset] <<< 24) + ||| (int bytes.[offset + 1] <<< 16) + ||| (int bytes.[offset + 2] <<< 8) + ||| int bytes.[offset + 3] + be32 16, be32 20 + + [<TestMethod>] + member _.Cc0GroundAtlasIsIntegerMultipleOfTheTileGrid () = + let width, height = pngDimensions (assetPath cc0TileAtlasRelativePath) + Assert.AreEqual<int>(cc0TileAtlasWidth, width) + Assert.AreEqual<int>(cc0TileAtlasHeight, height) + Assert.AreEqual<int>(0, width % Sim.tilePixels) + Assert.AreEqual<int>(0, height % Sim.tilePixels) + Assert.AreEqual<int>(4, width / Sim.tilePixels) + Assert.AreEqual<int>(2, Sim.tilePixels / cc0TileSourcePixels) + Assert.AreEqual<int>(0, Sim.tilePixels % cc0TileSourcePixels) + + [<TestMethod>] + member _.Cc0CharacterAtlasIsIntegerMultipleOfTheCellGrid () = + let width, height = pngDimensions (assetPath cc0CharacterAtlasRelativePath) + Assert.AreEqual<int>(cc0CharacterAtlasWidth, width) + Assert.AreEqual<int>(cc0CharacterCellHeight, height) + Assert.AreEqual<int>(0, width % cc0CharacterCellWidth) + Assert.AreEqual<int>(0, height % cc0CharacterCellHeight) + Assert.AreEqual<int>( + cc0CharacterVariantCount * cc0CharacterDirectionCount * cc0CharacterFrameCount, + width / cc0CharacterCellWidth) + Assert.AreEqual<int>(2, cc0CharacterCellWidth / cc0CharacterSourceWidth) + Assert.AreEqual<int>(2, cc0CharacterCellHeight / cc0CharacterSourceHeight) + + [<TestMethod>] + member _.AssetPackSwitchSelectsRealPackAndFallsBackToProcedural () = + let previous = Environment.GetEnvironmentVariable "LV_ASSET_PACK" + try + Environment.SetEnvironmentVariable("LV_ASSET_PACK", null) + Assert.IsFalse(useCc0Pack (), "unset switch must keep the procedural fallback") + Assert.IsTrue((selectedGroundAtlasRelativePath ()).IsNone, "fallback ground is procedural") + Assert.AreEqual<string>(characterAtlasRelativePath, selectedCharacterAtlasRelativePath ()) + + Environment.SetEnvironmentVariable("LV_ASSET_PACK", "cc0") + Assert.IsTrue(useCc0Pack (), "cc0 switch enables the real pack") + Assert.IsTrue(selectedGroundAtlasRelativePath () = Some cc0TileAtlasRelativePath) + Assert.AreEqual<string>(cc0CharacterAtlasRelativePath, selectedCharacterAtlasRelativePath ()) + + Environment.SetEnvironmentVariable("LV_ASSET_PACK", "kenney") + Assert.IsFalse(useCc0Pack (), "unknown pack value falls back to procedural") + finally + Environment.SetEnvironmentVariable("LV_ASSET_PACK", previous) diff --git a/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj b/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj index d4fa1f5..d32a21f 100644 --- a/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj +++ b/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj @@ -15,6 +15,7 @@ <Compile Include="MapGenTests.fs" /> <Compile Include="PerformanceSummaryTests.fs" /> <Compile Include="M3BatchOutputTests.fs" /> + <Compile Include="AssetPackTests.fs" /> <Compile Include="SampleTests.fs" /> </ItemGroup> diff --git a/src/LivingVillage.Desktop/Assets/cc0-tiles.png b/src/LivingVillage.Desktop/Assets/cc0-tiles.png Binary files differindex 1e43bc8..204eba2 100644 --- a/src/LivingVillage.Desktop/Assets/cc0-tiles.png +++ b/src/LivingVillage.Desktop/Assets/cc0-tiles.png diff --git a/src/LivingVillage.Desktop/VillageArt.fs b/src/LivingVillage.Desktop/VillageArt.fs index 4faa886..9a034aa 100644 --- a/src/LivingVillage.Desktop/VillageArt.fs +++ b/src/LivingVillage.Desktop/VillageArt.fs @@ -544,22 +544,46 @@ module VillageArt = else texture + /// Fallback switch for the real-pixel asset pack (P33 vertical slice). When + /// `LV_ASSET_PACK=cc0`, the sample village swaps its ground + villager atlases for + /// the provenanced CC0 packs; any other value keeps the procedural Jiangnan atlases. let useCc0Pack () : bool = System.Environment.GetEnvironmentVariable("LV_ASSET_PACK") = "cc0" let cc0TileAtlasRelativePath = "Assets/cc0-tiles.png" + // Source ground tiles are 16x16, upscaled exactly 2x to one Sim.tilePixels cell. + let cc0TileSourcePixels = 16 let cc0TileAtlasWidth = Sim.tilePixels * 4 + let cc0TileAtlasHeight = Sim.tilePixels let cc0CharacterAtlasRelativePath = "Assets/cc0-characters.png" + // Source Puny band is 16x24, upscaled exactly 2x to a 32x48 cell; atlas packs + // 5 variants x 4 directions x 6 frames. + let cc0CharacterSourceWidth = 16 + let cc0CharacterSourceHeight = 24 + let cc0CharacterCellWidth = 32 + let cc0CharacterCellHeight = 48 + let cc0CharacterVariantCount = 5 + let cc0CharacterDirectionCount = 4 + let cc0CharacterFrameCount = 6 + let cc0CharacterAtlasWidth = + cc0CharacterCellWidth * cc0CharacterVariantCount * cc0CharacterDirectionCount * cc0CharacterFrameCount + + /// Ground atlas selection: `Some cc0-tiles.png` when the pack is on, else `None` + /// so the renderer keeps its procedural Jiangnan ground as the fallback. + let selectedGroundAtlasRelativePath () : string option = + if useCc0Pack () then Some cc0TileAtlasRelativePath else None + + /// Character atlas selection: real CC0 Puny sprites when the pack is on, else the + /// procedural Jiangnan character atlas (fallback). + let selectedCharacterAtlasRelativePath () : string = + if useCc0Pack () then cc0CharacterAtlasRelativePath else characterAtlasRelativePath let loadTextures (device: GraphicsDevice) : ArtTextures = let cc0 = - if useCc0Pack () then - Some(loadTexture device cc0TileAtlasRelativePath cc0TileAtlasWidth Sim.tilePixels) - else - None - let characterPath = if useCc0Pack () then cc0CharacterAtlasRelativePath else characterAtlasRelativePath + selectedGroundAtlasRelativePath () + |> Option.map (fun path -> loadTexture device path cc0TileAtlasWidth cc0TileAtlasHeight) { TileAtlas = loadTexture device worldAtlasRelativePath worldAtlasWidth worldAtlasHeight - CharacterAtlas = loadTexture device characterPath characterAtlasWidth characterAtlasHeight + CharacterAtlas = loadTexture device (selectedCharacterAtlasRelativePath ()) cc0CharacterAtlasWidth cc0CharacterCellHeight InteriorAtlas = loadTexture device interiorAtlasRelativePath interiorAtlasWidth interiorAtlasHeight Cc0TileAtlas = cc0 } |
