diff options
| -rw-r--r-- | scripts/make-jiangnan-art.py | 22 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/PrototypeTests.fs | 38 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/Assets/jiangnan-world.png | bin | 1483 -> 1576 bytes | |||
| -rw-r--r-- | src/LivingVillage.Desktop/Game.fs | 1 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/VillageArt.fs | 47 |
5 files changed, 88 insertions, 20 deletions
diff --git a/scripts/make-jiangnan-art.py b/scripts/make-jiangnan-art.py index 98e357c..26db3fc 100644 --- a/scripts/make-jiangnan-art.py +++ b/scripts/make-jiangnan-art.py @@ -2,7 +2,7 @@ """Deterministic pixel-art atlases for LivingVillage.Desktop. Generates three RGBA PNGs into src/LivingVillage.Desktop/Assets: - jiangnan-world.png 416x32 (13 ground/prop tiles, 32px each) + jiangnan-world.png 448x32 (14 ground/prop tiles, 32px each) jiangnan-characters.png 1024x48 (4 variants x 4 directions x 2 frames, 32x48 cells) jiangnan-interior.png 392x96 (variable-size bottom-aligned interior cells) @@ -99,7 +99,7 @@ def verify_png(path, expected_w, expected_h): def build_world_atlas(): - c = Canvas(416, 32) + c = Canvas(448, 32) tile = 32 def slot(index): @@ -294,6 +294,22 @@ def build_world_atlas(): c.rect(x0 + 19, 18, 3, 3, (196, 158, 74, 255)) c.rect(x0 + 6, 29, 20, 3, (83, 52, 39, 255)) + x0 = slot(13) + c.rect(x0, 0, 32, 32, water) + c.rect(x0 + 4, 3, 2, 26, water_deep) + c.rect(x0 + 19, 8, 2, 22, water_deep) + for y in (9, 20, 29): + for x in range(3, 29): + if (x * 3 + y) % 5 != 0: + c.set(x0 + x, y, water_light) + for y in (14, 25): + for x in range(5, 27): + if (x + y * 2) % 6 == 0: + c.set(x0 + x, y, water_deep) + for sx, sy in ((9, 6), (24, 13), (15, 24), (5, 19)): + c.set(x0 + sx, sy, (214, 231, 238, 255)) + c.set(x0 + sx + 1, sy, (214, 231, 238, 120)) + return c @@ -513,7 +529,7 @@ def build_interior_atlas(): def main(): ASSET_DIR.mkdir(parents=True, exist_ok=True) targets = [ - ("jiangnan-world.png", build_world_atlas(), 416, 32), + ("jiangnan-world.png", build_world_atlas(), 448, 32), ("jiangnan-characters.png", build_character_atlas(), 1024, 48), ("jiangnan-interior.png", build_interior_atlas(), 392, 96), ] diff --git a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs index 06b3250..7959d27 100644 --- a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs +++ b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs @@ -301,8 +301,42 @@ type PrototypeTests () = Assert.IsTrue(footprint.Contains({ X = door.X + dx; Y = door.Y - 2 } : TilePosition), sprintf "missing ridge tile dx=%d" dx) Assert.IsTrue(footprint.Contains({ X = door.X + dx; Y = door.Y - 1 } : TilePosition), sprintf "missing eave tile dx=%d" dx) Assert.IsTrue(footprint.Contains({ X = door.X + dx; Y = door.Y } : TilePosition), sprintf "missing wall tile dx=%d" dx) - // 12 house tiles + 2 bridge + 3 bamboo + 2 garden. - Assert.AreEqual<int>(19, footprint.Count) + // 12 house tiles + 3 bridge + 3 bamboo + 2 garden. + Assert.AreEqual<int>(20, footprint.Count) + + [<TestMethod>] + member _.WaterRippleFramesAreDeterministicAndTwoPhase () = + Assert.AreEqual<int>(0, VillageArt.waterFrameTick 0L) + Assert.AreEqual<int>(0, VillageArt.waterFrameTick 31L) + Assert.AreEqual<int>(1, VillageArt.waterFrameTick 32L) + Assert.AreEqual<int>(1, VillageArt.waterFrameTick 63L) + Assert.AreEqual<int>(0, VillageArt.waterFrameTick 64L) + Assert.AreEqual<int>(VillageArt.waterFrameTick 100L, VillageArt.waterFrameTick 100L) + + [<TestMethod>] + member _.RiverBandSpansTheSceneWithBankPathsAndCrossing () = + let plan = VillageArt.sampleRenderPlan () + let footprintOf (value: VillageArt.JiangnanRenderPlan) = VillageArt.planFootprint value + let waters = plan.Grounds |> List.filter (fun (_, kind) -> kind = VillageArt.Water) |> List.map fst |> Set.ofList + let paths = plan.Grounds |> List.filter (fun (_, kind) -> kind = VillageArt.StonePath) |> List.map fst |> Set.ofList + let door = scene.Homes |> List.head |> fun home -> home.Door + + // The river must run the full width of the decorated scene as a readable water band. + Assert.AreEqual<int>(20, [ 0 .. 19 ] |> List.filter (fun x -> waters.Contains({ X = x; Y = 12 } : TilePosition)) |> List.length) + Assert.IsTrue(waters.Contains({ X = 6; Y = 13 } : TilePosition)) + // Banks stay walkable stone, and the door row connects to the north bank. + for x in 0 .. 19 do + if x <> 6 && x <> 7 && x <> 8 then + Assert.IsTrue(paths.Contains({ X = x; Y = 11 } : TilePosition), sprintf "missing north bank x=%d" x) + for y in 6 .. 10 do + Assert.IsTrue(paths.Contains({ X = 6; Y = y } : TilePosition), sprintf "missing door connector y=%d" y) + // Bridge footprint crosses the full river between the banks. + let bridge = plan.Props |> List.find (fun prop -> prop.Kind = VillageArt.Bridge) + let expectedBridge : TilePosition = { X = 6; Y = 11 } + Assert.AreEqual<TilePosition>(expectedBridge, bridge.Position) + for y in 11 .. 13 do + let bridgeTile = VillageArt.worldTile plan { X = 6; Y = y } + Assert.IsTrue(footprintOf plan |> List.contains bridgeTile, sprintf "bridge tile y=%d" y) [<TestMethod>] member _.FacingUsesTheActualMovementOrTargetVector () = diff --git a/src/LivingVillage.Desktop/Assets/jiangnan-world.png b/src/LivingVillage.Desktop/Assets/jiangnan-world.png Binary files differindex 766405f..ca4a521 100644 --- a/src/LivingVillage.Desktop/Assets/jiangnan-world.png +++ b/src/LivingVillage.Desktop/Assets/jiangnan-world.png diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs index a5049f3..4871e80 100644 --- a/src/LivingVillage.Desktop/Game.fs +++ b/src/LivingVillage.Desktop/Game.fs @@ -576,6 +576,7 @@ type LivingVillageGame() as this = viewport.Width viewport.Height renderPlan + world.Tick profile.WorldTint let avatarSpec = VillageArt.avatarSpriteSpec avatarFacing world.Tick diff --git a/src/LivingVillage.Desktop/VillageArt.fs b/src/LivingVillage.Desktop/VillageArt.fs index 5883a97..a66474b 100644 --- a/src/LivingVillage.Desktop/VillageArt.fs +++ b/src/LivingVillage.Desktop/VillageArt.fs @@ -102,6 +102,7 @@ module VillageArt = | WallWoodSprite | WallWindowSprite | DoorSprite + | WaterSpriteB type private XnaColor = Microsoft.Xna.Framework.Color type private XnaRectangle = Microsoft.Xna.Framework.Rectangle @@ -143,15 +144,7 @@ module VillageArt = { Origin = position 25 18 Grounds = - [ ground Water 10 2 - ground Water 10 3 - ground Water 10 4 - ground Water 10 5 - ground Water 10 6 - ground Water 10 7 - ground Water 11 4 - ground Water 11 5 - ground StonePath 3 5 + [ ground StonePath 3 5 ground StonePath 4 5 ground StonePath 5 5 ground StonePath 6 5 @@ -161,10 +154,26 @@ module VillageArt = ground StonePath 10 5 ground StonePath 11 5 ground StonePath 6 6 - ground StonePath 6 7 ] + ground StonePath 6 7 + ground StonePath 6 8 + ground StonePath 6 9 + ground StonePath 6 10 + for x in 0 .. 5 do + ground StonePath x 11 + for x in 9 .. 19 do + ground StonePath x 11 + for x in 1 .. 18 do + ground StonePath x 14 + ground Water 6 11 + ground Water 7 11 + ground Water 8 11 + for x in 0 .. 19 do + ground Water x 12 + for x in 2 .. 17 do + ground Water x 13 ] Props = [ renderProp House 6 5 - renderProp Bridge 10 5 + renderProp Bridge 6 11 renderProp BambooGrove 3 2 renderProp VegetablePatch 8 7 ] Interior = @@ -190,7 +199,7 @@ module VillageArt = [ for dx in -2 .. 1 do for dy in -2 .. 0 do yield position (x + dx) (y + dy) ] - | Bridge -> [ position x y; position x (y + 1) ] + | Bridge -> [ position x y; position x (y + 1); position x (y + 2) ] | BambooGrove -> [ position x y; position x (y + 1); position (x + 1) y ] | VegetablePatch -> [ position x y; position (x + 1) y ]) |> many @@ -212,6 +221,10 @@ module VillageArt = let animationFrame (tick: int64) : AnimationFrame = if positiveModulo64 8L tick < 4L then FrameOne else FrameTwo + /// Water surface ripple phase: flips every 32 simulation ticks, fully deterministic. + let waterFrameTick (tick: int64) : int = + int (positiveModulo64 2L (tick / 32L)) + let directionFromVector (vector: Vec2) : FacingDirection = if abs vector.X < 0.001f && abs vector.Y < 0.001f then SouthFacing @@ -271,7 +284,7 @@ module VillageArt = let characterAtlasRelativePath = "Assets/jiangnan-characters.png" let interiorAtlasRelativePath = "Assets/jiangnan-interior.png" - let worldTileCount = 13 + let worldTileCount = 14 let worldAtlasWidth = Sim.tilePixels * worldTileCount let worldAtlasHeight = Sim.tilePixels let characterAtlasWidth = 32 * 4 * 4 * 2 @@ -313,6 +326,7 @@ module VillageArt = | WallWoodSprite -> 10 | WallWindowSprite -> 11 | DoorSprite -> 12 + | WaterSpriteB -> 13 XnaRectangle(index * Sim.tilePixels, 0, Sim.tilePixels, Sim.tilePixels) let private tilePosition x y : TilePosition = @@ -350,9 +364,11 @@ module VillageArt = (viewportWidth: int) (viewportHeight: int) (plan: JiangnanRenderPlan) + (tick: int64) (tint: XnaColor) = let draw position sprite = drawWorldTile spriteBatch textures camera viewportWidth viewportHeight position sprite tint + let rippleFrame = waterFrameTick tick let tx0 = max 0 (int camera.X / Sim.tilePixels) let ty0 = max 0 (int camera.Y / Sim.tilePixels) @@ -366,7 +382,7 @@ module VillageArt = let position = worldTile plan localPosition let sprite = match kind with - | Water -> WaterSprite + | Water -> if rippleFrame = 0 then WaterSprite else WaterSpriteB | StonePath -> StonePathSprite draw position sprite @@ -383,7 +399,8 @@ module VillageArt = let drawBridge localPosition = let position = worldTile plan localPosition draw position BridgeSprite - draw (offsetTile position 1 0) BridgeSprite + draw (offsetTile position 0 1) BridgeSprite + draw (offsetTile position 0 2) BridgeSprite let drawBamboo localPosition = let position = worldTile plan localPosition |
