summaryrefslogtreecommitdiff
path: root/scripts/make-cc0-art.py
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-21 14:34:09 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-21 14:34:09 +0800
commit2c1127e1522dca89c6710dda0b5e64d8ab89686d (patch)
treef0d8a9f00ae0e88b2540f426db71884d814cb7e9 /scripts/make-cc0-art.py
parent3849252be9ee5a6b9ad6b31b0fe1a19767621baa (diff)
downloadliving-village-2c1127e1522dca89c6710dda0b5e64d8ab89686d.tar.gz
feat(assets): CC0 素材接入与制作管线
third_party 收录 Kenney Tiny Farm(CC0,来源 kenney.nl,sha256 a06f75f3…)与 Puny Characters(CC0,opengameart 作者 Shade,sha256 971d7b07…),上游名未改,License 原文保留于包内;新增 scripts/make-cc0-art.py 确定性管线:Kenney 地块 16px → 2x 32px 整数放大(草亮/草深/路径/泥地四块 128x32 图集),Puny 角色 16x24 内容带 2x 精确映射 32x48(2560x48 图集,变体/方向/帧几何与江南人物一致;仅南向素材——北向沿用原朝向、西/东镜像,限制已在脚本注释与 provenance 记录)。
Diffstat (limited to 'scripts/make-cc0-art.py')
-rw-r--r--scripts/make-cc0-art.py170
1 files changed, 170 insertions, 0 deletions
diff --git a/scripts/make-cc0-art.py b/scripts/make-cc0-art.py
new file mode 100644
index 0000000..897ec4f
--- /dev/null
+++ b/scripts/make-cc0-art.py
@@ -0,0 +1,170 @@
+#!/usr/bin/env python3
+"""CC0 asset pack integration (third_party -> game atlases).
+
+Reads the two provenanced CC0 packs in third_party/ (Kenney Tiny Farm tiles,
+Puny Characters) and emits Deterministic CC0 Atlases for the prototype:
+
+ src/LivingVillage.Desktop/Assets/cc0-tiles.png 128x32 (4 ground tiles, 32px each)
+ src/LivingVillage.Desktop/Assets/cc0-characters.png 2560x48 (5 variants x 4 directions x 4 frames, 32x48 cells)
+
+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.
+
+Paths anchor to REPO/third_party; all outputs are deterministic (fixed poses,
+fixed tiles, NEAREST resampling only).
+"""
+
+from PIL import Image
+import struct, zlib, hashlib, subprocess
+from pathlib import Path
+
+REPO = Path(__file__).resolve().parents[1]
+THIRD = REPO / "third_party"
+DESKTOP = REPO / "src" / "LivingVillage.Desktop"
+ASSETS = DESKTOP / "Assets"
+
+OUTLINE = (23, 31, 34, 255)
+
+
+class Canvas:
+ def __init__(self, width, height):
+ self.width = width
+ self.height = height
+ self.pixels = [[(0, 0, 0, 0)] * width for _ in range(height)]
+
+ def set(self, x, y, color):
+ if 0 <= x < self.width and 0 <= y < self.height:
+ self.pixels[y][x] = color
+
+ def get(self, x, y):
+ return self.pixels[y][x]
+
+ def blit(self, img, ox, oy, scale=2):
+ px = img.load()
+ for y in range(img.height):
+ for x in range(img.width):
+ c = img.getpixel((x, y))
+ r, g, b, a = c
+ for dy in range(scale):
+ for dx in range(scale):
+ self.set(ox + x * scale + dx, oy + y * scale + dy, (r, g, b, a))
+
+
+def write_png(path, canvas):
+ raw = bytearray()
+ for y in range(canvas.height):
+ raw.append(0)
+ for x in range(canvas.width):
+ raw.extend(canvas.pixels[y][x])
+
+ def chunk(tag, payload):
+ return (
+ struct.pack(">I", len(payload))
+ + tag
+ + payload
+ + struct.pack(">I", zlib.crc32(tag + payload) & 0xFFFFFFFF)
+ )
+
+ header = struct.pack(">IIBBBBB", canvas.width, canvas.height, 8, 6, 0, 0, 0)
+ data = (
+ b'\x89PNG\r\n\x1a\n'
+ + chunk(b"IHDR", header)
+ + chunk(b"IDAT", zlib.compress(bytes(raw), 9))
+ + chunk(b"IEND", b"")
+ )
+ path.write_bytes(data)
+
+
+def verify_png(path, expected_w, expected_h):
+ data = path.read_bytes()
+ assert data[:8] == b'\x89PNG\r\n\x1a\n', path
+ width, height = struct.unpack(">II", data[16:24])
+ assert (width, height) == (expected_w, expected_h), (path, width, height)
+ idat = b""
+ offset = 8
+ while offset < len(data):
+ (length,) = struct.unpack(">I", data[offset : offset + 4])
+ tag = data[offset + 4 : offset + 8]
+ if tag == b"IDAT":
+ idat += data[offset + 8 : offset + 8 + length]
+ offset += 12 + length
+ raw = zlib.decompress(idat)
+ stride = expected_w * 4 + 1
+ assert len(raw) == stride * expected_h, (path, len(raw))
+ print(f"ok {path.name} {width}x{height} bytes={len(data)}")
+
+
+def build_cc0_tile_atlas():
+ """Four 32x32 ground tiles from Kenney Tiny Farm, upscaled 2x.
+
+ Tile selection is fixed by grid coordinates chosen from the pack's
+ tilemap_packed.png (grass: two greens; stone path: pale slab; dirt: apricot).
+ """
+ c = Canvas(128, 32)
+ packed = Image.open(THIRD / "kenney_tiny-farm" / "Tilemap" / "tilemap_packed.png").convert("RGBA")
+
+ def tile(grid_x, grid_y):
+ return packed.crop((grid_x * 17, grid_y * 17, grid_x * 17 + 16, grid_y * 17 + 16))
+
+ choices = [(10, 9), (9, 7), (6, 7), (6, 9)]
+ for index, (gx, gy) in enumerate(choices):
+ t = tile(gx, gy)
+ c.blit(t, index * 32, 0, scale=2)
+ return c
+
+
+def build_cc0_character_atlas():
+ """5 variants x 4 directions x 4 frames of 32x48 cells from Puny characters.
+
+ Layout: each 32x48 CELL = source 16x24 content band scaled 2x (32x48), anchored
+ bottom (feet on the ground line). Directions: the pack provides south-facing
+ art only; south/north use the same band (pack limitation recorded in the
+ commit), west/east are mirrored copies so walkers always read toward the player.
+ """
+ c = Canvas(2560, 48)
+
+ variant_files = [
+ "Soldier-Blue.png",
+ "Soldier-Yellow.png",
+ "Warrior-Blue.png",
+ "Archer-Green.png",
+ "Character-Base.png",
+ ]
+ variants = []
+ for name in variant_files:
+ im = Image.open(THIRD / "puny-characters" / "Puny-Characters" / name).convert("RGBA")
+ pose_cells = []
+ for i in range(4):
+ left = 8 + i * 32
+ pose_cells.append(im.crop((left, 2, left + 16, 26)))
+ variants.append(pose_cells)
+
+ cell_w = 32
+ for vi, pose_cells in enumerate(variants):
+ for di in range(4):
+ for frame in range(4):
+ sprite = pose_cells[frame].resize((32, 48), Image.NEAREST)
+ if di in (1, 3):
+ sprite = sprite.transpose(Image.FLIP_LEFT_RIGHT)
+ ox = cell_w * ((vi * 4 + di) * 4 + frame)
+ c.blit(sprite, ox, 0, scale=1)
+ return c
+
+
+
+
+def main():
+ ASSETS.mkdir(parents=True, exist_ok=True)
+ targets = [
+ ("cc0-tiles.png", build_cc0_tile_atlas(), 128, 32),
+ ("cc0-characters.png", build_cc0_character_atlas(), 2560, 48),
+ ]
+ for name, canvas, w, h in targets:
+ path = ASSETS / name
+ write_png(path, canvas)
+ verify_png(path, w, h)
+
+
+if __name__ == "__main__":
+ main()