summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/LivingVillage.Desktop.Tests/AssetPackTests.fs32
-rw-r--r--src/LivingVillage.Desktop/Assets/cc0-tiles.pngbin388 -> 816 bytes
-rw-r--r--src/LivingVillage.Desktop/VillageArt.fs95
3 files changed, 112 insertions, 15 deletions
diff --git a/src/LivingVillage.Desktop.Tests/AssetPackTests.fs b/src/LivingVillage.Desktop.Tests/AssetPackTests.fs
index fc235d0..46513f9 100644
--- a/src/LivingVillage.Desktop.Tests/AssetPackTests.fs
+++ b/src/LivingVillage.Desktop.Tests/AssetPackTests.fs
@@ -36,11 +36,41 @@ type AssetPackTests () =
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>(cc0TileCount, width / Sim.tilePixels)
+ Assert.AreEqual<int>(cc0TileAtlasWidth, width)
Assert.AreEqual<int>(2, Sim.tilePixels / cc0TileSourcePixels)
Assert.AreEqual<int>(0, Sim.tilePixels % cc0TileSourcePixels)
[<TestMethod>]
+ member _.Cc0StructureTileIndicesFitTheAtlasAndWaterHasTwoPhases () =
+ // Every P34 self-drawn structure cell must sit inside the 12-tile atlas band.
+ let indices =
+ [ cc0TileIndexWaterA
+ cc0TileIndexWaterB
+ cc0TileIndexRoofRidge
+ cc0TileIndexRoofEave
+ cc0TileIndexWallWood
+ cc0TileIndexWallWindow
+ cc0TileIndexRailing
+ cc0TileIndexDoor ]
+ for index in indices do
+ Assert.IsTrue(index >= 0 && index < cc0TileCount, sprintf "cc0 tile index %d out of atlas" index)
+ let width, _ = pngDimensions (assetPath cc0TileAtlasRelativePath)
+ Assert.IsTrue(width >= (List.max indices + 1) * Sim.tilePixels, "atlas must cover every structure index")
+ // Two-phase water: phase depends only on tick/tile, always in {A, B}, and both
+ // phases appear across a row of tiles in one frame.
+ Assert.AreEqual<int>(2, cc0WaterFrameCount)
+ let phases =
+ [ for tileX in 0 .. 15 -> cc0WaterTileIndexAt 0L tileX 0 ]
+ |> Set.ofList
+ Assert.IsTrue((phases = Set.ofList [ cc0TileIndexWaterA; cc0TileIndexWaterB ]),
+ "one frame must show both water phases across the river")
+ for tileX in 0 .. 15 do
+ for tileY in 0 .. 15 do
+ let index = cc0WaterTileIndexAt 12345L tileX tileY
+ Assert.IsTrue(index = cc0TileIndexWaterA || index = cc0TileIndexWaterB)
+
+ [<TestMethod>]
member _.Cc0CharacterAtlasIsIntegerMultipleOfTheCellGrid () =
let width, height = pngDimensions (assetPath cc0CharacterAtlasRelativePath)
Assert.AreEqual<int>(cc0CharacterAtlasWidth, width)
diff --git a/src/LivingVillage.Desktop/Assets/cc0-tiles.png b/src/LivingVillage.Desktop/Assets/cc0-tiles.png
index 204eba2..afa46a4 100644
--- a/src/LivingVillage.Desktop/Assets/cc0-tiles.png
+++ b/src/LivingVillage.Desktop/Assets/cc0-tiles.png
Binary files differ
diff --git a/src/LivingVillage.Desktop/VillageArt.fs b/src/LivingVillage.Desktop/VillageArt.fs
index 9a034aa..80eed07 100644
--- a/src/LivingVillage.Desktop/VillageArt.fs
+++ b/src/LivingVillage.Desktop/VillageArt.fs
@@ -553,7 +553,20 @@ module VillageArt =
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
+ // P34 structure slice: indices into cc0-tiles.png. Kenney Tiny Farm ships no
+ // water / roof / wall / railing / door art, so indices 4-11 are self-drawn
+ // (provenance in docs/素材来源.md), laid out by this fixed contract.
+ let cc0TileCount = 12
+ let cc0TileIndexWaterA = 4
+ let cc0TileIndexWaterB = 5
+ let cc0TileIndexRoofRidge = 6
+ let cc0TileIndexRoofEave = 7
+ let cc0TileIndexWallWood = 8
+ let cc0TileIndexWallWindow = 9
+ let cc0TileIndexRailing = 10
+ let cc0TileIndexDoor = 11
+ let cc0WaterFrameCount = 2
+ let cc0TileAtlasWidth = Sim.tilePixels * cc0TileCount
let cc0TileAtlasHeight = Sim.tilePixels
let cc0CharacterAtlasRelativePath = "Assets/cc0-characters.png"
// Source Puny band is 16x24, upscaled exactly 2x to a 32x48 cell; atlas packs
@@ -568,6 +581,16 @@ module VillageArt =
let cc0CharacterAtlasWidth =
cc0CharacterCellWidth * cc0CharacterVariantCount * cc0CharacterDirectionCount * cc0CharacterFrameCount
+ /// Deterministic 2-phase water per tile: `(tick/32 + x + 2y) mod 2`, so one frame
+ /// shows both ripple phases across the river. Mirrors the 3-phase Jiangnan
+ /// `waterFrameTickAt`, but only two self-drawn CC0 frames exist.
+ let cc0WaterTileIndexAt (tick: int64) (tileX: int) (tileY: int) : int =
+ let phase = int ((tick / 32L + int64 (tileX + 2 * tileY)) % 2L)
+ if phase = 0 then cc0TileIndexWaterA else cc0TileIndexWaterB
+
+ let cc0TileRect (index: int) : XnaRectangle =
+ XnaRectangle(index * Sim.tilePixels, 0, Sim.tilePixels, Sim.tilePixels)
+
/// 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 =
@@ -792,6 +815,20 @@ module VillageArt =
let draw position sprite =
drawWorldTile spriteBatch textures camera viewportWidth viewportHeight position sprite tint
+ // P34: with the CC0 pack on, individual sample structures draw from the
+ // real/self-drawn cc0-tiles atlas; otherwise every draw falls back to jiangnan.
+ let drawCc0At (position: TilePosition) (index: int) =
+ match textures.Cc0TileAtlas with
+ | Some cc0 ->
+ drawCc0Tile
+ spriteBatch
+ cc0
+ tint
+ (cc0TileRect index)
+ (position.X * Sim.tilePixels - int camera.X)
+ (position.Y * Sim.tilePixels - int camera.Y)
+ | None -> ()
+
// Each water tile picks its phase from its own world coordinates, so a single
// frame shows the three ripple phases across the river instead of one global frame.
let waterSpriteAt (position: TilePosition) =
@@ -800,15 +837,41 @@ module VillageArt =
| 1 -> WaterSpriteB
| _ -> WaterSpriteC
+ // P34 water: the self-drawn CC0 atlas only has two frames, so it picks the
+ // 2-phase index; the jiangnan atlas keeps its 3-phase ripple.
+ let drawWater (position: TilePosition) =
+ match textures.Cc0TileAtlas with
+ | Some _ -> drawCc0At position (cc0WaterTileIndexAt tick position.X position.Y)
+ | None -> draw position (waterSpriteAt position)
+
+ // P34 structures: real/self-drawn roof, wall, window and door cells replace
+ // the jiangnan house tiles only when the CC0 pack is on.
+ let houseSpriteAt (position: TilePosition) (sprite: TileSprite) =
+ let cc0Index =
+ match textures.Cc0TileAtlas with
+ | Some _ ->
+ match sprite with
+ | RoofRidgeSprite -> Some cc0TileIndexRoofRidge
+ | RoofEaveSprite -> Some cc0TileIndexRoofEave
+ | WallWoodSprite -> Some cc0TileIndexWallWood
+ | WallWindowSprite -> Some cc0TileIndexWallWindow
+ | DoorSprite -> Some cc0TileIndexDoor
+ | _ -> None
+ | None -> None
+ match cc0Index with
+ | Some index -> drawCc0At position index
+ | None -> draw position sprite
+
// Procedural terrain for the 512x384 large world: water/pebble/peat codes map
// onto the jiangnan atlas sprites with deterministic ripple continuity.
// 生成器世界:1 水 / 2 石板路 / 3 泥炭 / 0 草地。泥炭按草地渲染,路走石板贴图。
let drawTerrainTile (position: TilePosition) (code: int) =
match code with
- | 1 -> draw position (waterSpriteAt position)
+ | 1 -> drawWater position
| 2 -> draw position StonePathSprite
- // P30 乡村空区:水田(4)走静态水面素材、菜畦(5)走菜地素材;均为成熟 tile。
- | 4 -> draw position WaterSprite
+ // P30 乡村空区:水田(4)走水波动画、菜畦(5)走菜地素材;均为成熟 tile。
+ // P34:水田/水网在 CC0 包下同样走自绘 2 相位水面。
+ | 4 -> drawWater position
| 5 -> draw position GardenSprite
| _ -> draw position GrassSprite
@@ -851,7 +914,7 @@ module VillageArt =
for localPosition, kind in planGrounds do
let position = worldTile plan localPosition
match kind with
- | Water -> draw position (waterSpriteAt position)
+ | Water -> drawWater position
| StonePath ->
match textures.Cc0TileAtlas with
| Some cc0 -> drawCc0Tile spriteBatch cc0 tint cc0PathRect (position.X * Sim.tilePixels - int camera.X) (position.Y * Sim.tilePixels - int camera.Y)
@@ -860,18 +923,22 @@ module VillageArt =
let drawHouse doorTile =
let position = worldTile plan doorTile
for dx in -2 .. 1 do
- draw (offsetTile position dx -2) RoofRidgeSprite
- draw (offsetTile position dx -1) RoofEaveSprite
- draw (offsetTile position -2 0) WallWoodSprite
- draw (offsetTile position -1 0) WallWindowSprite
- draw (offsetTile position 0 0) DoorSprite
- draw (offsetTile position 1 0) WallWoodSprite
+ houseSpriteAt (offsetTile position dx -2) RoofRidgeSprite
+ houseSpriteAt (offsetTile position dx -1) RoofEaveSprite
+ houseSpriteAt (offsetTile position -2 0) WallWoodSprite
+ houseSpriteAt (offsetTile position -1 0) WallWindowSprite
+ houseSpriteAt (offsetTile position 0 0) DoorSprite
+ houseSpriteAt (offsetTile position 1 0) WallWoodSprite
let drawBridge localPosition =
let position = worldTile plan localPosition
- draw position BridgeSprite
- draw (offsetTile position 0 1) BridgeSprite
- draw (offsetTile position 0 2) BridgeSprite
+ let bridgeTile p =
+ match textures.Cc0TileAtlas with
+ | Some _ -> drawCc0At p cc0TileIndexRailing
+ | None -> draw p BridgeSprite
+ bridgeTile position
+ bridgeTile (offsetTile position 0 1)
+ bridgeTile (offsetTile position 0 2)
let drawBamboo localPosition =
let position = worldTile plan localPosition