summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-28 15:58:36 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-28 15:58:36 +0800
commitfdb28e93af2ee6413659ab4a773f80a9b67a3100 (patch)
tree1b5f3f4001f6f6fbc2facb01fba2fc648109dd55 /scripts
parentad3617731a97386127594261e9531abbdabb4aba (diff)
downloadliving-village-fdb28e93af2ee6413659ab4a773f80a9b67a3100.tar.gz
art(p39): 自绘中式元素补入 cc0 图集(索引 12-18,608x32)
- scripts/make-cc0-art.py 由 384x32(12格) 扩到 608x32(19格),新增 draw_bridge_center/end、draw_boat、draw_stone_lantern、draw_stall、draw_reeds - 旧索引 0-11 逐字节不变;产物 1523 bytes,sha256 ef86ced2…6adcb,二次生成一致 - docs/素材来源.md 更新为 19 格并新增第 4 节(P39 清单 + 摆放规则 + fallback 差异)
Diffstat (limited to 'scripts')
-rw-r--r--scripts/make-cc0-art.py201
1 files changed, 197 insertions, 4 deletions
diff --git a/scripts/make-cc0-art.py b/scripts/make-cc0-art.py
index 44bd013..7c5868c 100644
--- a/scripts/make-cc0-art.py
+++ b/scripts/make-cc0-art.py
@@ -4,7 +4,7 @@
Reads the provenanced CC0 packs in third_party/ (Kenney Tiny Farm tiles, Puny
Characters) and emits deterministic atlases for the prototype:
- src/LivingVillage.Desktop/Assets/cc0-tiles.png 384x32 (12 tiles, 32px each)
+ src/LivingVillage.Desktop/Assets/cc0-tiles.png 608x32 (19 tiles, 32px each)
src/LivingVillage.Desktop/Assets/cc0-characters.png 3840x48 (5 variants x 4 directions x 6 frames, 32x48 cells)
Ground tiles (indices 0-3) come from Kenney Tiny Farm via integer 2x NEAREST of
@@ -13,6 +13,14 @@ art (it is a farm pack), so indices 4-11 are *self-drawn* 32px pixel art in a
consistent Jiangnan palette (white plaster, dark slate tile, warm wood). Each
tile's provenance is recorded in docs/素材来源.md.
+P39 (indices 12-18) appends more self-drawn Jiangnan elements after the P34
+block, keeping every older index byte-for-byte backwards compatible:
+ 12 bridge centre / 13 bridge end / 14 boat left / 15 boat right
+ 16 stone lantern / 17 market stall / 18 reeds
+Bridge/boat/lantern/stall/reeds are all self-drawn (Kenney Tiny Farm has none
+of them), same palette, same 32px grid, NEAREST-safe (no resampling: drawn at
+final 1x resolution, so pixel edges are exact).
+
Mapping strategy (16 -> 32): integer 2x nearest-neighbour upscale of every source
sprite; ground tiles fill the 32x32 cell exactly; Puny sprites keep their
16x24-ish content band (2x -> 32x48) anchored to the cell bottom.
@@ -197,8 +205,181 @@ def draw_door(c, ox):
c.fill(ox + 20, 17, 3, 3, (222, 190, 120, 255))
+# --- P39 self-drawn Jiangnan elements (indices 12-18) -------------------------
+STONE = (150, 150, 142, 255)
+STONE_DARK = (104, 106, 102, 255)
+STONE_HI = (196, 196, 188, 255)
+BRIDGE_DECK = (128, 128, 122, 255)
+BOAT_HULL = (58, 48, 40, 255)
+BOAT_HULL_DARK = (38, 32, 27, 255)
+BOAT_ROOF = (46, 54, 42, 255)
+BOAT_ROOF_HI = (76, 88, 66, 255)
+LANTERN_PAPER = (228, 196, 128, 255)
+LANTERN_GLOW = (255, 208, 150, 255)
+LANTERN_GLOW_HI = (255, 232, 190, 255)
+LANTERN_STONE = (168, 166, 156, 255)
+REED_STEM = (74, 104, 60, 255)
+REED_LEAF = (92, 130, 72, 255)
+REED_TIP = (146, 158, 96, 255)
+STALL_AWNING = (176, 74, 58, 255)
+STALL_AWNING_2 = (196, 176, 132, 255)
+CARROT = (216, 126, 54, 255)
+CABBAGE = (120, 168, 84, 255)
+CLOTH = (208, 198, 172, 255)
+
+
+def draw_bridge_center(c, ox):
+ """Arch bridge span: stone deck with a railing and an arched underside.
+
+ The lower third is transparent so the water ripple still shows through the
+ arch; a warm railing sits on top of the deck.
+ """
+ c.fill(ox, 0, 32, 32, CLEAR)
+ # Deck (single stone span, bright top edge / dark underside).
+ c.fill(ox, 10, 32, 9, BRIDGE_DECK)
+ c.fill(ox, 10, 32, 2, STONE_HI)
+ c.fill(ox, 17, 32, 2, STONE_DARK)
+ # Arch cut-outs under the deck (transparent so water shows through).
+ c.fill(ox + 6, 19, 8, 13, CLEAR)
+ c.fill(ox + 18, 19, 8, 13, CLEAR)
+ c.fill(ox + 10, 19, 12, 5, BRIDGE_DECK)
+ # Wood railing above the deck.
+ c.fill(ox, 4, 32, 2, WOOD_DARK)
+ c.fill(ox, 7, 32, 2, WOOD)
+ for px in (3, 14, 25):
+ c.fill(ox + px, 4, 2, 6, WOOD)
+ c.set(ox + px, 9, WOOD_DARK)
+
+
+def draw_bridge_end(c, ox):
+ """Bridge end: stone abutment at the bank plus the last railing posts."""
+ c.fill(ox, 0, 32, 32, CLEAR)
+ # Stepped abutment: two stone steps widening down to the bank.
+ c.fill(ox + 4, 12, 24, 6, STONE)
+ c.fill(ox + 4, 12, 24, 2, STONE_HI)
+ c.fill(ox, 18, 32, 14, STONE)
+ c.fill(ox, 18, 32, 2, STONE_HI)
+ c.fill(ox, 29, 32, 3, STONE_DARK)
+ # End posts of the railing.
+ c.fill(ox + 2, 3, 3, 11, WOOD_DARK)
+ c.fill(ox + 2, 3, 3, 2, WOOD)
+ c.fill(ox + 27, 3, 3, 11, WOOD_DARK)
+ c.fill(ox + 27, 3, 3, 2, WOOD)
+ c.fill(ox + 2, 6, 28, 2, WOOD)
+
+
+def draw_boat(c, ox, right_half):
+ """Wu Peng boat half (2 tiles wide): dark hull + curved bamboo awning.
+
+ Drawn as two halves so a 2-tile boat reads as one continuous silhouette.
+ The hull floats on the water: its top edge (gunwale) is 4px down, so open
+ water ripple stays visible above the boat on the tile.
+ """
+ c.fill(ox, 0, 32, 32, CLEAR)
+ if not right_half:
+ # Left half: bow rising to the left, hull tapering.
+ for i in range(26):
+ half = 3 + i // 3
+ c.fill(ox + 0, 10 + i, half, 1, BOAT_HULL)
+ c.fill(ox, 10, 3, 1, BOAT_HULL_DARK)
+ c.fill(ox + 12, 9, 20, 2, BOAT_HULL_DARK)
+ c.fill(ox + 12, 11, 20, 13, BOAT_HULL)
+ # Awning (wu peng) left edge, arch top.
+ c.fill(ox + 12, 1, 20, 8, BOAT_ROOF)
+ c.fill(ox + 12, 1, 20, 2, BOAT_ROOF_HI)
+ c.fill(ox + 12, 1, 2, 8, BOAT_ROOF_HI)
+ c.fill(ox + 12, 8, 20, 2, BOAT_HULL_DARK)
+ # Stern post at the extreme left of the visible half.
+ c.fill(ox, 6, 2, 6, BOAT_HULL_DARK)
+ else:
+ # Right half: stern rising to the right, mirrored continuous hull.
+ for i in range(26):
+ half = 3 + i // 3
+ c.fill(ox + 32 - half, 10 + i, half, 1, BOAT_HULL)
+ c.fill(ox + 29, 10, 3, 1, BOAT_HULL_DARK)
+ c.fill(ox, 9, 20, 2, BOAT_HULL_DARK)
+ c.fill(ox, 11, 20, 13, BOAT_HULL)
+ c.fill(ox, 1, 20, 8, BOAT_ROOF)
+ c.fill(ox, 1, 20, 2, BOAT_ROOF_HI)
+ c.fill(ox + 18, 1, 2, 8, BOAT_ROOF_HI)
+ c.fill(ox, 8, 20, 2, BOAT_HULL_DARK)
+ c.fill(ox + 30, 6, 2, 6, BOAT_HULL_DARK)
+
+
+def draw_stone_lantern(c, ox):
+ """Ishidoro-style stone lantern with a warm lit window (night glow anchor).
+
+ The lit pane is a bright warm core (alpha opaque) sitting inside a stone
+ housing, so the renderer's radial glow layer adds a real halo on top.
+ """
+ c.fill(ox, 0, 32, 32, CLEAR)
+ # Base plinth.
+ c.fill(ox + 8, 27, 16, 5, LANTERN_STONE)
+ c.fill(ox + 8, 27, 16, 1, STONE_HI)
+ c.fill(ox + 11, 24, 10, 4, LANTERN_STONE)
+ # Shaft.
+ c.fill(ox + 14, 18, 4, 7, LANTERN_STONE)
+ # Light housing with a bright warm pane.
+ c.fill(ox + 6, 9, 20, 10, LANTERN_STONE)
+ c.fill(ox + 6, 9, 20, 1, STONE_HI)
+ c.fill(ox + 9, 11, 14, 7, LANTERN_GLOW)
+ c.fill(ox + 11, 12, 4, 5, LANTERN_GLOW_HI)
+ # Dark frame over the pane.
+ c.vline(ox + 9, 11, 7, STONE_DARK)
+ c.vline(ox + 22, 11, 7, STONE_DARK)
+ c.vline(ox + 15, 11, 7, STONE_DARK)
+ # Cap + finial.
+ c.fill(ox + 4, 6, 24, 4, STONE_DARK)
+ c.fill(ox + 6, 5, 20, 2, LANTERN_STONE)
+ c.fill(ox + 15, 1, 3, 4, LANTERN_STONE)
+
+
+def draw_stall(c, ox):
+ """Roadside market stall: striped awning + counter + produce baskets."""
+ c.fill(ox, 0, 32, 32, CLEAR)
+ # Awning: red/cream stripes with a scalloped lower edge.
+ for x in range(0, 32, 4):
+ stripe = STALL_AWNING if (x // 4) % 2 == 0 else STALL_AWNING_2
+ c.fill(ox + x, 6, 4, 8, stripe)
+ c.fill(ox, 13, 32, 2, WOOD_DARK)
+ for x in range(0, 32, 8):
+ c.fill(ox + x, 15, 8, 2, STALL_AWNING_2)
+ c.set(ox + x + 4, 17, WOOD_DARK)
+ # Support poles.
+ c.vline(ox + 2, 15, 13, WOOD_DARK)
+ c.vline(ox + 28, 15, 13, WOOD_DARK)
+ # Counter.
+ c.fill(ox + 2, 20, 28, 4, WOOD)
+ c.fill(ox + 2, 20, 28, 1, WOOD_DARK)
+ # Produce on the counter.
+ c.fill(ox + 6, 17, 4, 3, CARROT)
+ c.fill(ox + 7, 16, 2, 1, REED_LEAF)
+ c.fill(ox + 14, 16, 6, 4, CABBAGE)
+ c.fill(ox + 15, 15, 4, 1, REED_LEAF)
+ c.fill(ox + 24, 17, 4, 3, CLOTH)
+ # Base.
+ c.fill(ox + 2, 24, 28, 4, WOOD_DARK)
+
+
+def draw_reeds(c, ox):
+ """Waterside reeds: a few stems with leaf blades and seed tips."""
+ c.fill(ox, 0, 32, 32, CLEAR)
+ stems = [(6, 6, 24), (13, 10, 22), (21, 7, 23), (26, 13, 20)]
+ for (x, top, bottom) in stems:
+ c.fill(ox + x, top, 2, bottom - top, REED_STEM)
+ c.fill(ox + x, top, 2, 3, REED_TIP)
+ # Leaf blades angling off the stem.
+ c.fill(ox + x - 4, top + 5, 5, 1, REED_LEAF)
+ c.fill(ox + x - 3, top + 6, 4, 1, REED_LEAF)
+ c.fill(ox + x + 2, top + 9, 4, 1, REED_LEAF)
+ # Dense root cluster at the waterline (bottom of the tile).
+ c.fill(ox + 3, 28, 26, 4, REED_LEAF)
+ c.fill(ox + 8, 30, 16, 2, REED_STEM)
+
+
+
def build_cc0_tile_atlas():
- """Twelve 32x32 tiles: 4 Kenney ground + 8 self-drawn structure tiles.
+ """Nineteen 32x32 tiles: 4 Kenney ground + 15 self-drawn Jiangnan tiles.
Ground source is the *packed* sheet (tilemap_packed.png) which has NO spacing
between tiles: 192x176 = 12x11 tiles at a 16px pitch (the spaced tilemap.png
@@ -208,8 +389,12 @@ def build_cc0_tile_atlas():
wall, railing or door tiles), layout fixed by the P34 atlas contract:
4 water A / 5 water B / 6 roof ridge / 7 roof eave
8 wall wood / 9 wall window / 10 railing / 11 door
+ P39 appends indices 12-18 (bridge / boat / lantern / stall / reeds) after the
+ P34 block so every older index stays unchanged and backwards compatible:
+ 12 bridge centre / 13 bridge end / 14 boat left / 15 boat right
+ 16 stone lantern / 17 market stall / 18 reeds
"""
- c = Canvas(384, 32)
+ c = Canvas(608, 32)
packed = Image.open(THIRD / "kenney_tiny-farm" / "Tilemap" / "tilemap_packed.png").convert("RGBA")
def tile(grid_x, grid_y):
@@ -227,6 +412,14 @@ def build_cc0_tile_atlas():
draw_wall(c, 9 * 32, window=True)
draw_railing(c, 10 * 32)
draw_door(c, 11 * 32)
+ # P39 appended block.
+ draw_bridge_center(c, 12 * 32)
+ draw_bridge_end(c, 13 * 32)
+ draw_boat(c, 14 * 32, right_half=False)
+ draw_boat(c, 15 * 32, right_half=True)
+ draw_stone_lantern(c, 16 * 32)
+ draw_stall(c, 17 * 32)
+ draw_reeds(c, 18 * 32)
return c
@@ -281,7 +474,7 @@ def build_cc0_character_atlas():
def main():
ASSETS.mkdir(parents=True, exist_ok=True)
targets = [
- ("cc0-tiles.png", build_cc0_tile_atlas(), 384, 32),
+ ("cc0-tiles.png", build_cc0_tile_atlas(), 608, 32),
("cc0-characters.png", build_cc0_character_atlas(), 3840, 48),
]
for name, canvas, w, h in targets: