summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--scripts/make-cc0-art.py120
-rw-r--r--src/LivingVillage.Desktop.Tests/P40ArtTests.fs66
-rw-r--r--src/LivingVillage.Desktop/Assets/cc0-tiles.pngbin1834 -> 1739 bytes
3 files changed, 145 insertions, 41 deletions
diff --git a/scripts/make-cc0-art.py b/scripts/make-cc0-art.py
index 351c20a..3290fa0 100644
--- a/scripts/make-cc0-art.py
+++ b/scripts/make-cc0-art.py
@@ -235,6 +235,7 @@ PAVE_JOINT = (72, 72, 70, 255)
BAMBOO = (44, 58, 34, 255)
BAMBOO_HI = (86, 104, 62, 255)
BAMBOO_RIB = (28, 38, 22, 255)
+BAMBOO_DARK = (22, 30, 17, 255)
RED_LANTERN = (208, 58, 46, 255)
RED_LANTERN_HI = (238, 104, 84, 255)
RED_LANTERN_DARK = (150, 34, 28, 255)
@@ -284,54 +285,91 @@ def draw_bridge_end(c, ox):
def draw_boat(c, ox, right_half):
"""Wu Peng boat half (2 tiles wide): dark hull + tall curved bamboo awning.
- P40: the boat was scaled up to fill the 64x32 footprint so the wu peng (拱形
- 竹篷) reads at a glance from a bridge or bank. Each half is 32px wide; the
- awning is a tall elliptical bamboo arch spanning the middle of the 2-tile
- boat with visible ribs, and the hull is a solid dark band with an upturned
- bow/stern tip. The gunwale sits 8px down, leaving open water ripple above.
+ P40 fix: the awning is a *solid* arched span, not a row of 1px posts. For
+ every column across the awning the tile fills a horizontal band from the
+ arch top y(x) down to the deck line, so the silhouette reads as one filled
+ 拱形竹篷. The ridge (arch top plateau) is a continuous highlight band with no
+ transparent break. Each half is 32px wide; the right half mirrors the left,
+ so the two halves join into a single continuous arch with the ridge meeting
+ at the tile boundary (no broken arc).
+
+ Left half local profile y(x):
+ x < 1 : outside the awning (bow only)
+ x in [1,6) : rising arc 9,7,5,3,1 (stepped outer end)
+ x in [6,31]: ridge plateau y=1
+ The right half mirrors it (local lx -> 31-lx). The stepped end is capped with
+ one BAMBOO_DARK column; the hull has plank seams (y=14/22) and a waterline.
"""
c.fill(ox, 0, 32, 32, CLEAR)
- # --- Hull: solid dark band spanning the full cell, 8px down to 30px. ---
+ deck_y = 9
+
+ def top_at(x):
+ """Left-half local arch top row for column x, or None outside the awning."""
+ if x < 1:
+ return None
+ if x < 6:
+ # Rounded rise: 9,7,5,3,1 across x=1..5 so the ridge plateau starts at x=6.
+ return 9 - 2 * (x - 1)
+ return 1
+
+ # --- Hull: solid dark band, 8px down to 30px, with a raised bow/stern tip. ---
c.fill(ox, 8, 32, 22, BOAT_HULL)
c.fill(ox, 8, 32, 2, BOAT_HULL_DARK)
c.fill(ox, 28, 32, 2, BOAT_HULL_DARK)
- # --- Awning (wu peng): tall bamboo arch over the middle of the 2-tile boat. ---
- # Arc rises from the outer edge up to a high ridge, so both halves join into
- # one continuous arch. Left half: low at x=0, ridge near the inner (right) edge.
- arc = [
- (0, 12), (2, 9), (4, 6), (6, 4), (8, 3),
- (10, 2), (12, 1), (14, 1), (16, 1), (18, 1),
- (20, 1), (22, 1), (24, 2), (26, 3), (28, 4),
- (30, 5), (32, 6),
- ]
+ # Plank seams (y=14 / y=22) + waterline (bottom rows read as below the water).
+ c.hline(ox, 14, 32, BOAT_HULL_DARK)
+ c.hline(ox, 22, 32, BOAT_HULL_DARK)
+ c.hline(ox, 29, 32, BOAT_HULL_DARK)
+ c.hline(ox, 30, 32, BOAT_HULL_DARK)
if not right_half:
- # Left half: bow tip at the far left, rising; awning covers inner 20px.
- for i in range(24):
- half = 4 + i // 2
- c.fill(ox, 8 + i, min(32, half), 1, BOAT_HULL)
- c.fill(ox, 6, 4, 3, BOAT_HULL_DARK)
- c.fill(ox, 5, 2, 3, BOAT_HULL)
- for (x, top) in arc:
- c.fill(ox + x, top, 1, 32 - top, BAMBOO)
- c.fill(ox + 12, 1, 20, 2, BAMBOO_HI)
- for rib in (14, 18, 22, 26, 30):
- c.fill(ox + rib, 1, 1, 30, BAMBOO_RIB)
- c.fill(ox + 12, 30, 20, 2, BAMBOO_RIB)
- c.fill(ox + 12, 6, 2, 24, BAMBOO_HI)
+ c.fill(ox, 5, 3, 3, BOAT_HULL)
+ c.fill(ox, 6, 3, 2, BOAT_HULL_DARK)
+ awning_columns = list(range(1, 32))
+ rib_columns = [14, 20, 26]
+ cap_columns = [1] # stepped outer end of the awning
+
+ def local_top(lx):
+ return top_at(lx)
else:
- # Right half: stern tip at the far right, mirrored continuous hull.
- for i in range(24):
- half = 4 + i // 2
- c.fill(ox + 32 - min(32, half), 8 + i, min(32, half), 1, BOAT_HULL)
- c.fill(ox + 28, 6, 4, 3, BOAT_HULL_DARK)
- c.fill(ox + 30, 5, 2, 3, BOAT_HULL)
- for (x, top) in arc:
- c.fill(ox + 31 - x, top, 1, 32 - top, BAMBOO)
- c.fill(ox, 1, 20, 2, BAMBOO_HI)
- for rib in (1, 5, 9, 13, 17):
- c.fill(ox + rib, 1, 1, 30, BAMBOO_RIB)
- c.fill(ox, 30, 20, 2, BAMBOO_RIB)
- c.fill(ox + 18, 6, 2, 24, BAMBOO_HI)
+ c.fill(ox + 29, 5, 3, 3, BOAT_HULL)
+ c.fill(ox + 28, 6, 3, 2, BOAT_HULL_DARK)
+ awning_columns = list(range(0, 31))
+ rib_columns = [5, 11, 17]
+ cap_columns = [29] # mirrored stepped outer end
+
+ def local_top(lx):
+ return top_at(31 - lx)
+
+ # --- Solid arched span: each column fills from the arch line to the deck. ---
+ for lx in awning_columns:
+ t = local_top(lx)
+ if t is None:
+ continue
+ c.fill(ox + lx, t, 1, deck_y - t + 1, BAMBOO)
+ # Continuous ridge highlight: rows t..t+1 across the whole awning (no break).
+ for lx in awning_columns:
+ t = local_top(lx)
+ if t is None:
+ continue
+ c.fill(ox + lx, t, 1, min(2, deck_y - t + 1), BAMBOO_HI)
+ # Bamboo ribs below the ridge (texture only; ridge band stays continuous).
+ for lx in rib_columns:
+ t = local_top(lx)
+ if t is None or t + 2 > deck_y:
+ continue
+ c.fill(ox + lx, t + 2, 1, deck_y - t - 1, BAMBOO_RIB)
+ # Stepped end cap: one dark column closing the outer end of the awning.
+ for lx in cap_columns:
+ t = local_top(lx)
+ if t is None:
+ continue
+ c.fill(ox + lx, t, 1, deck_y - t + 1, BAMBOO_DARK)
+ # Deck shadow where the awning meets the hull.
+ for lx in awning_columns:
+ t = local_top(lx)
+ if t is None:
+ continue
+ c.set(ox + lx, deck_y, BAMBOO_RIB)
def draw_paving(c, ox):
diff --git a/src/LivingVillage.Desktop.Tests/P40ArtTests.fs b/src/LivingVillage.Desktop.Tests/P40ArtTests.fs
index 549640e..15e6601 100644
--- a/src/LivingVillage.Desktop.Tests/P40ArtTests.fs
+++ b/src/LivingVillage.Desktop.Tests/P40ArtTests.fs
@@ -23,6 +23,47 @@ type P40ArtTests () =
x >= 0 && x < map.Width && y >= 0 && y < map.Height
&& map.Tiles.[indexOf x y] = int MapGen.GroundTile.Stone
+ // --- P40 boat awning: solid arch silhouette (atlas pixel decode) -----------
+ // The atlas is written by scripts/make-cc0-art.py as non-interlaced 8-bit RGBA
+ // with filter byte 0 on every scanline, so a tiny decoder is enough to assert
+ // the awning is a filled arch with a continuous ridge (not a row of 1px posts).
+ let atlasRgba () : int * int * (int -> int -> (int * int * int * int)) =
+ let path = System.IO.Path.Combine(System.AppContext.BaseDirectory, cc0TileAtlasRelativePath)
+ let bytes = System.IO.File.ReadAllBytes path
+ let be32 (o: int) =
+ (int bytes.[o] <<< 24) ||| (int bytes.[o + 1] <<< 16) ||| (int bytes.[o + 2] <<< 8) ||| int bytes.[o + 3]
+ let width = be32 16
+ let height = be32 20
+ let mutable offset = 8
+ let mutable idat = System.IO.MemoryStream()
+ while offset < bytes.Length do
+ let length = be32 offset
+ let tag = System.Text.Encoding.ASCII.GetString(bytes, offset + 4, 4)
+ if tag = "IDAT" then idat.Write(bytes, offset + 8, length)
+ offset <- offset + 12 + length
+ use z = new System.IO.Compression.ZLibStream(new System.IO.MemoryStream(idat.ToArray()), System.IO.Compression.CompressionMode.Decompress)
+ use ms = new System.IO.MemoryStream()
+ z.CopyTo ms
+ let decompressed = ms.ToArray()
+ let stride = width * 4
+ let pixels = Array.zeroCreate<byte> (width * height * 4)
+ for y in 0 .. height - 1 do
+ let rowStart = y * (stride + 1) + 1 // skip the per-row filter byte (always 0 here)
+ Array.blit decompressed rowStart pixels (y * stride) stride
+ let get (x: int) (y: int) =
+ if x < 0 || x >= width || y < 0 || y >= height then (0, 0, 0, 0)
+ else
+ let i = (y * width + x) * 4
+ (int pixels.[i], int pixels.[i + 1], int pixels.[i + 2], int pixels.[i + 3])
+ width, height, get
+
+ let isBambooOpaque (r: int, g: int, b: int, a: int) =
+ a > 0
+ && ((r, g, b) = (44, 58, 34)
+ || (r, g, b) = (86, 104, 62)
+ || (r, g, b) = (28, 38, 22)
+ || (r, g, b) = (22, 30, 17))
+
[<TestMethod>]
member _.PavingCoversTrafficNetworkAndDropsIsolatedScatter () =
let paving = cc0PavingTiles map
@@ -102,3 +143,28 @@ type P40ArtTests () =
for index in [ cc0TileIndexPaving; cc0TileIndexRedLantern ] do
Assert.IsTrue(index >= 0 && index < cc0TileCount, sprintf "P40 tile index %d out of atlas" index)
Assert.AreEqual<int>(21, cc0TileCount)
+
+ [<TestMethod>]
+ member _.BoatAwningIsASolidArchWithAContinuousRidge () =
+ let width, _, pixel = atlasRgba ()
+ Assert.IsTrue(width >= (cc0TileIndexBoatRight + 1) * 32, "atlas must cover the boat tiles")
+ for tile in [ cc0TileIndexBoatLeft; cc0TileIndexBoatRight ] do
+ let ox = tile * 32
+ // Ridge: y in [1,2], x in [6,25] must have no transparent column break.
+ for x in 6 .. 25 do
+ for y in 1 .. 2 do
+ let (_, _, _, a) = pixel (ox + x) y
+ Assert.IsTrue(a > 0, sprintf "boat tile %d ridge pixel (%d,%d) is transparent" tile x y)
+ // Solid arch: below the arch line, every x in [4,27] has opaque bamboo.
+ for x in 4 .. 27 do
+ let hasBamboo =
+ [ 0 .. 31 ]
+ |> List.exists (fun y -> isBambooOpaque (pixel (ox + x) y))
+ Assert.IsTrue(hasBamboo, sprintf "boat tile %d column %d has no opaque bamboo" tile x)
+ // The two halves join: the arch top at the tile boundary is the same row.
+ let topRow (ox: int) (x: int) =
+ [ 0 .. 31 ] |> List.tryFind (fun y -> isBambooOpaque (pixel (ox + x) y))
+ Assert.AreEqual<Option<int>>(
+ topRow (cc0TileIndexBoatLeft * 32) 31,
+ topRow (cc0TileIndexBoatRight * 32) 0,
+ "left/right awning halves must meet at the same arch height (no broken arc)")
diff --git a/src/LivingVillage.Desktop/Assets/cc0-tiles.png b/src/LivingVillage.Desktop/Assets/cc0-tiles.png
index d26e8b3..97a875b 100644
--- a/src/LivingVillage.Desktop/Assets/cc0-tiles.png
+++ b/src/LivingVillage.Desktop/Assets/cc0-tiles.png
Binary files differ