summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--scripts/make-jiangnan-art.py22
-rw-r--r--src/LivingVillage.Desktop.Tests/PrototypeTests.fs38
-rw-r--r--src/LivingVillage.Desktop/Assets/jiangnan-world.pngbin1483 -> 1576 bytes
-rw-r--r--src/LivingVillage.Desktop/Game.fs1
-rw-r--r--src/LivingVillage.Desktop/VillageArt.fs47
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
index 766405f..ca4a521 100644
--- a/src/LivingVillage.Desktop/Assets/jiangnan-world.png
+++ b/src/LivingVillage.Desktop/Assets/jiangnan-world.png
Binary files differ
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