diff options
| -rw-r--r-- | scripts/make-jiangnan-art.py | 73 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/DesktopTests.fs | 2 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/PrototypeTests.fs | 14 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/Assets/jiangnan-world.png | bin | 1098 -> 1483 bytes | |||
| -rw-r--r-- | src/LivingVillage.Desktop/VillageArt.fs | 47 |
5 files changed, 124 insertions, 12 deletions
diff --git a/scripts/make-jiangnan-art.py b/scripts/make-jiangnan-art.py index 478e732..98e357c 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 256x32 (8 ground/prop tiles, 32px each) + jiangnan-world.png 416x32 (13 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(256, 32) + c = Canvas(416, 32) tile = 32 def slot(index): @@ -227,6 +227,73 @@ def build_world_atlas(): c.set(x0 + sx + 1, row_y - 3, sprout_light) c.outline(x0, 0, 32, 32) + x0 = slot(8) + ridge_bar = (38, 42, 50, 255) + for y in range(14, 28): + c.rect(x0 + 3, y, 26, 1, ridge_bar) + c.rect(x0 + 3, 13, 26, 1, roof_light) + c.rect(x0 + 3, 27, 26, 1, roof_dark) + for end_x, end_w in ((0, 6), (26, 6)): + for step in range(5): + c.rect(x0 + end_x + (step if end_x == 0 else 0), 14 - step * 2, end_w, 2, ridge_bar) + c.rect(x0 + end_x, 6 if end_x == 0 else 8, end_w, 1, roof_light) + c.rect(x0 + 2, 27, 28, 1, roof_dark) + for x in range(7, 26, 6): + c.rect(x0 + x, 14, 1, 13, roof_dark) + + x0 = slot(9) + c.rect(x0, 0, 32, 14, roof) + for x in range(0, 32, 8): + c.rect(x0 + x, 0, 1, 14, roof_dark) + c.rect(x0, 3, 32, 1, roof_light) + c.rect(x0, 14, 32, 2, roof_dark) + c.rect(x0, 16, 32, 2, roof_light) + c.rect(x0, 18, 32, 3, (23, 31, 34, 70)) + for tip_x in (2, 29): + c.rect(x0 + tip_x, 12, 1, 4, roof_light) + + x0 = slot(10) + c.rect(x0, 0, 32, 32, wall) + c.rect(x0, 0, 32, 3, wood) + c.rect(x0, 0, 32, 1, wood_light) + c.rect(x0, 3, 32, 1, wood_dark) + for post_x in (0, 29): + c.rect(x0 + post_x, 0, 3, 32, wood) + c.rect(x0 + post_x, 0, 1, 32, wood_light) + c.rect(x0 + post_x + 2, 0, 1, 32, wood_dark) + c.rect(x0 + 3, 9, 26, 1, wall_seam) + c.rect(x0 + 3, 19, 26, 1, wall_seam) + c.rect(x0 + 3, 28, 26, 4, (128, 128, 126, 255)) + c.rect(x0 + 3, 28, 26, 1, (150, 150, 146, 255)) + + x0 = slot(11) + c.rect(x0, 0, 32, 32, wall) + c.rect(x0, 0, 32, 3, wood) + c.rect(x0, 0, 32, 1, wood_light) + c.rect(x0, 3, 32, 1, wood_dark) + c.rect(x0 + 7, 8, 18, 16, (94, 66, 40, 255)) + c.rect(x0 + 9, 10, 14, 12, (61, 45, 49, 255)) + for lx in (12, 16, 20): + c.rect(x0 + lx, 10, 1, 12, (196, 180, 152, 255)) + for ly in (14, 18): + c.rect(x0 + 9, ly, 14, 1, (196, 180, 152, 255)) + c.rect(x0 + 7, 8, 18, 1, (164, 111, 61, 255)) + c.rect(x0 + 3, 28, 26, 4, (128, 128, 126, 255)) + c.rect(x0 + 3, 28, 26, 1, (150, 150, 146, 255)) + + x0 = slot(12) + c.rect(x0, 0, 32, 32, wall) + c.rect(x0, 0, 32, 3, wood) + c.rect(x0, 0, 32, 1, wood_light) + c.rect(x0, 3, 32, 1, wood_dark) + c.rect(x0 + 6, 4, 20, 28, (94, 66, 40, 255)) + c.rect(x0 + 8, 6, 16, 26, wood) + c.rect(x0 + 8, 6, 16, 1, wood_light) + c.rect(x0 + 15, 6, 2, 26, wood_dark) + c.rect(x0 + 10, 12, 4, 1, wood_dark) + c.rect(x0 + 19, 18, 3, 3, (196, 158, 74, 255)) + c.rect(x0 + 6, 29, 20, 3, (83, 52, 39, 255)) + return c @@ -446,7 +513,7 @@ def build_interior_atlas(): def main(): ASSET_DIR.mkdir(parents=True, exist_ok=True) targets = [ - ("jiangnan-world.png", build_world_atlas(), 256, 32), + ("jiangnan-world.png", build_world_atlas(), 416, 32), ("jiangnan-characters.png", build_character_atlas(), 1024, 48), ("jiangnan-interior.png", build_interior_atlas(), 392, 96), ] diff --git a/src/LivingVillage.Desktop.Tests/DesktopTests.fs b/src/LivingVillage.Desktop.Tests/DesktopTests.fs index 526d1af..e9713e5 100644 --- a/src/LivingVillage.Desktop.Tests/DesktopTests.fs +++ b/src/LivingVillage.Desktop.Tests/DesktopTests.fs @@ -102,7 +102,7 @@ type DesktopTests () = let renderPlan = VillageArt.sampleRenderPlan () let blockedTiles = (renderPlan.Grounds |> List.map (fun (tile, _) -> VillageArt.worldTile renderPlan tile)) - @ (renderPlan.Props |> List.map (fun prop -> VillageArt.worldTile renderPlan prop.Position)) + @ VillageArt.planFootprint renderPlan |> List.map (fun tile -> tile.X, tile.Y) |> Set.ofList let sceneTileOf (position: Vec2) : TilePosition = diff --git a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs index a403223..06b3250 100644 --- a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs +++ b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs @@ -291,6 +291,20 @@ type PrototypeTests () = Assert.AreEqual<int>(4, first.Interior.Elements.Length) [<TestMethod>] + member _.HouseFacadeCoversRoofEaveAndWalledDoorRow () = + let plan = VillageArt.sampleRenderPlan () + let footprint = VillageArt.planFootprint plan |> Set.ofList + let door = scene.Homes |> List.head |> fun home -> VillageArt.worldTile plan home.Door + + // The enterable home must present a full facade: ridge, eave, and a walled row with the door. + for dx in -2 .. 1 do + 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) + + [<TestMethod>] member _.FacingUsesTheActualMovementOrTargetVector () = Assert.AreEqual<VillageArt.FacingDirection>(VillageArt.EastFacing, VillageArt.directionFromVector { X = 1.0f; Y = 0.1f }) Assert.AreEqual<VillageArt.FacingDirection>(VillageArt.WestFacing, VillageArt.directionFromVector { X = -1.0f; Y = 0.1f }) diff --git a/src/LivingVillage.Desktop/Assets/jiangnan-world.png b/src/LivingVillage.Desktop/Assets/jiangnan-world.png Binary files differindex 75dc906..766405f 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 03cd22e..5883a97 100644 --- a/src/LivingVillage.Desktop/VillageArt.fs +++ b/src/LivingVillage.Desktop/VillageArt.fs @@ -97,6 +97,11 @@ module VillageArt = | BridgeSprite | BambooSprite | GardenSprite + | RoofRidgeSprite + | RoofEaveSprite + | WallWoodSprite + | WallWindowSprite + | DoorSprite type private XnaColor = Microsoft.Xna.Framework.Color type private XnaRectangle = Microsoft.Xna.Framework.Rectangle @@ -158,7 +163,7 @@ module VillageArt = ground StonePath 6 6 ground StonePath 6 7 ] Props = - [ renderProp House 6 4 + [ renderProp House 6 5 renderProp Bridge 10 5 renderProp BambooGrove 3 2 renderProp VegetablePatch 8 7 ] @@ -173,6 +178,23 @@ module VillageArt = { X = plan.Origin.X + localPosition.X Y = plan.Origin.Y + localPosition.Y } + /// Every tile a prop paints over, in world coordinates. Used by draw ordering and by + /// spawn-overlap assertions so villagers never start inside a painted prop. + let planFootprint (plan: JiangnanRenderPlan) : TilePosition list = + let many positions = positions |> List.map (worldTile plan) + plan.Props + |> List.collect (fun prop -> + let x, y = prop.Position.X, prop.Position.Y + match prop.Kind with + | House -> + [ 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) ] + | BambooGrove -> [ position x y; position x (y + 1); position (x + 1) y ] + | VegetablePatch -> [ position x y; position (x + 1) y ]) + |> many + let private positiveModulo divisor value = let remainder = value % divisor if remainder < 0 then remainder + divisor else remainder @@ -249,7 +271,8 @@ module VillageArt = let characterAtlasRelativePath = "Assets/jiangnan-characters.png" let interiorAtlasRelativePath = "Assets/jiangnan-interior.png" - let worldAtlasWidth = Sim.tilePixels * 8 + let worldTileCount = 13 + let worldAtlasWidth = Sim.tilePixels * worldTileCount let worldAtlasHeight = Sim.tilePixels let characterAtlasWidth = 32 * 4 * 4 * 2 let characterAtlasHeight = 48 @@ -285,6 +308,11 @@ module VillageArt = | BridgeSprite -> 5 | BambooSprite -> 6 | GardenSprite -> 7 + | RoofRidgeSprite -> 8 + | RoofEaveSprite -> 9 + | WallWoodSprite -> 10 + | WallWindowSprite -> 11 + | DoorSprite -> 12 XnaRectangle(index * Sim.tilePixels, 0, Sim.tilePixels, Sim.tilePixels) let private tilePosition x y : TilePosition = @@ -342,12 +370,15 @@ module VillageArt = | StonePath -> StonePathSprite draw position sprite - let drawHouse localPosition = - let position = worldTile plan localPosition - for dx in -1 .. 1 do - draw (offsetTile position dx 0) RoofSprite - draw (offsetTile position dx 1) WallSprite - draw (offsetTile position 0 1) BridgeSprite + 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 let drawBridge localPosition = let position = worldTile plan localPosition |
