#!/usr/bin/env python3 """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-characters.png 1024x48 (4 variants x 4 directions x 2 frames, 32x48 cells) jiangnan-interior.png 392x96 (variable-size bottom-aligned interior cells) Cell geometry is mirrored in VillageArt.fs; keep both in sync. """ import struct import zlib from pathlib import Path ASSET_DIR = Path(__file__).resolve().parents[1] / "src" / "LivingVillage.Desktop" / "Assets" OUTLINE = (23, 31, 34, 255) SKIN = (232, 180, 139, 255) HAIR = (42, 36, 37, 255) SHOE = (40, 30, 28, 255) EYE = (43, 32, 30, 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 rect(self, x, y, w, h, color): for yy in range(y, y + h): for xx in range(x, x + w): self.set(xx, yy, color) def outline(self, x0, y0, w, h, color=OUTLINE): opaque = { (x, y) for y in range(y0, y0 + h) for x in range(x0, x0 + w) if self.get(x, y)[3] == 255 } for (x, y) in opaque: for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)): nx, ny = x + dx, y + dy if x0 <= nx < x0 + w and y0 <= ny < y0 + h and self.get(nx, ny)[3] == 0: self.set(nx, ny, color) 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_world_atlas(): c = Canvas(256, 32) tile = 32 def slot(index): return index * tile grass = (66, 112, 70, 255) grass_light = (92, 139, 82, 255) grass_dark = (54, 92, 60, 255) water = (54, 116, 145, 255) water_light = (105, 174, 184, 255) water_deep = (44, 93, 124, 255) path = (170, 153, 123, 255) path_light = (201, 183, 145, 255) path_grout = (140, 124, 96, 255) roof = (45, 49, 57, 255) roof_light = (76, 80, 85, 255) roof_dark = (30, 33, 38, 255) wall = (225, 216, 195, 255) wall_light = (238, 232, 214, 255) wall_shadow = (166, 147, 119, 255) wall_seam = (196, 180, 152, 255) wood = (119, 76, 47, 255) wood_light = (164, 111, 61, 255) wood_dark = (83, 52, 39, 255) bamboo = (51, 101, 61, 255) bamboo_light = (107, 153, 81, 255) bamboo_node = (38, 76, 46, 255) soil = (122, 78, 49, 255) soil_light = (146, 96, 60, 255) sprout = (86, 139, 68, 255) sprout_light = (107, 171, 85, 255) x0 = slot(0) c.rect(x0, 0, 32, 32, grass) for y in range(32): for x in range(32): if (x * 13 + y * 7) % 29 == 0: c.set(x0 + x, y, grass_light) elif (x * 7 + y * 11) % 43 == 0: c.set(x0 + x, y, grass_dark) for bx, by in ((5, 7), (13, 19), (22, 11), (28, 25), (9, 27), (18, 3)): c.set(x0 + bx, by, grass_light) c.set(x0 + bx, by + 1, grass_light) x0 = slot(1) c.rect(x0, 0, 32, 32, water) c.rect(x0 + 8, 2, 2, 26, water_deep) c.rect(x0 + 23, 2, 2, 26, water_deep) for y in (5, 16, 27): for x in range(3, 29): if (x + y) % 5 != 0: c.set(x0 + x, y, water_light) for sx, sy in ((12, 9), (26, 20), (6, 22)): c.set(x0 + sx, sy, (214, 231, 238, 255)) x0 = slot(2) c.rect(x0, 0, 32, 32, path) c.rect(x0, 0, 32, 1, path_light) c.rect(x0, 31, 32, 1, path_light) for y in (10, 21): c.rect(x0, y, 32, 1, path_grout) for x in (11, 22): c.rect(x0 + x, 0, 1, 10, path_grout) for x in (5, 17, 28): c.rect(x0 + x, 11, 1, 10, path_grout) for x in (12, 24): c.rect(x0 + x, 22, 1, 10, path_grout) for y in range(1, 31): for x in range(1, 31): if (x * 5 + y * 3) % 23 == 0 and c.get(x0 + x, y) == path: c.set(x0 + x, y, path_light) x0 = slot(3) c.rect(x0, 0, 32, 32, roof) for band in range(4): y = band * 8 c.rect(x0, y, 32, 1, roof_light) for x in range((band % 2) * 4, 32, 8): c.rect(x0 + x, y + 1, 1, 7, roof_dark) c.rect(x0, 31, 32, 1, roof_dark) x0 = slot(4) c.rect(x0, 0, 32, 32, wall) c.rect(x0, 0, 32, 1, wall_light) c.rect(x0 + 15, 0, 2, 32, wall_seam) c.rect(x0, 9, 32, 1, wall_seam) c.rect(x0, 18, 32, 1, wall_seam) c.rect(x0, 27, 32, 5, wall_shadow) c.rect(x0, 31, 32, 1, (120, 102, 80, 255)) x0 = slot(5) c.rect(x0, 0, 32, 32, wood) for y in (0, 8, 16, 24): c.rect(x0, y, 32, 1, wood_light) for y in (7, 15, 23, 31): c.rect(x0, y, 32, 1, wood_dark) for y in (4, 12, 20, 28): c.set(x0 + 4, y, (61, 45, 49, 255)) c.set(x0 + 27, y, (61, 45, 49, 255)) x0 = slot(6) for stem in (5, 15, 25): c.rect(x0 + stem, 2, 3, 30, bamboo) c.set(x0 + stem, 2, bamboo_light) c.set(x0 + stem, 3, bamboo_light) c.set(x0 + stem, 4, bamboo_light) for y in (8, 16, 24): c.rect(x0 + stem, y, 3, 1, bamboo_node) for i, ly in enumerate((6, 14, 22)): if i % 2 == 0: c.rect(x0 + stem - 4, ly, 5, 2, bamboo_light) c.rect(x0 + stem + 3, ly + 3, 4, 2, bamboo) else: c.rect(x0 + stem + 3, ly, 5, 2, bamboo_light) c.rect(x0 + stem - 4, ly + 3, 4, 2, bamboo) c.outline(x0, 0, 32, 32) x0 = slot(7) for row_y in (5, 14, 23): c.rect(x0 + 2, row_y, 28, 4, soil) c.rect(x0 + 2, row_y, 28, 1, soil_light) for sx in (6, 15, 24): c.rect(x0 + sx, row_y - 3, 2, 3, sprout) c.set(x0 + sx, row_y - 3, sprout_light) c.set(x0 + sx + 1, row_y - 3, sprout_light) c.outline(x0, 0, 32, 32) return c VARIANTS = [ {"robe": (46, 57, 108, 255), "light": (104, 145, 183, 255), "dark": (32, 41, 80, 255), "hat": False}, {"robe": (141, 91, 45, 255), "light": (205, 163, 76, 255), "dark": (104, 66, 32, 255), "hat": False}, {"robe": (46, 113, 91, 255), "light": (121, 185, 139, 255), "dark": (32, 84, 68, 255), "hat": False}, {"robe": (122, 96, 58, 255), "light": (176, 141, 84, 255), "dark": (92, 71, 42, 255), "hat": True}, ] DIRECTIONS = ["north", "south", "west", "east"] def draw_character_cell(c, ox, variant, direction, frame): robe = variant["robe"] light = variant["light"] dark = variant["dark"] hat = (196, 158, 74, 255) hat_light = (228, 197, 112, 255) hat_dark = (168, 132, 58, 255) c.rect(ox + 8, 44, 16, 1, (23, 31, 34, 110)) c.rect(ox + 10, 45, 12, 1, (23, 31, 34, 90)) if frame == 0: c.rect(ox + 10, 36, 4, 7, dark) c.rect(ox + 9, 42, 6, 3, SHOE) c.rect(ox + 18, 36, 4, 5, dark) c.rect(ox + 18, 39, 6, 3, SHOE) else: c.rect(ox + 18, 36, 4, 7, dark) c.rect(ox + 18, 42, 6, 3, SHOE) c.rect(ox + 10, 36, 4, 5, dark) c.rect(ox + 10, 39, 6, 3, SHOE) c.rect(ox + 7, 22, 18, 14, robe) c.rect(ox + 23, 22, 2, 14, dark) c.rect(ox + 7, 31, 18, 3, light) c.rect(ox + 13, 22, 6, 2, light) c.rect(ox + 15, 24, 2, 2, light) c.rect(ox + 5, 24, 2, 9, robe) c.rect(ox + 25, 24, 2, 9, robe) if direction != "north": c.rect(ox + 5, 32, 2, 2, light) c.rect(ox + 25, 32, 2, 2, light) c.rect(ox + 5, 34, 2, 3, SKIN) c.rect(ox + 25, 34, 2, 3, SKIN) c.rect(ox + 10, 12, 12, 10, SKIN) c.rect(ox + 9, 8, 14, 4, HAIR) c.rect(ox + 9, 12, 2, 3, HAIR) c.rect(ox + 21, 12, 2, 3, HAIR) if direction == "north": c.rect(ox + 10, 12, 12, 5, HAIR) elif direction == "south": c.rect(ox + 12, 15, 2, 2, EYE) c.rect(ox + 18, 15, 2, 2, EYE) c.rect(ox + 15, 19, 2, 1, (196, 120, 104, 255)) elif direction == "west": c.rect(ox + 18, 12, 4, 6, HAIR) c.rect(ox + 11, 15, 2, 2, EYE) elif direction == "east": c.rect(ox + 10, 12, 4, 6, HAIR) c.rect(ox + 19, 15, 2, 2, EYE) if variant["hat"]: c.rect(ox + 11, 3, 10, 7, hat) c.rect(ox + 12, 4, 8, 1, hat_light) c.rect(ox + 6, 9, 20, 2, hat) c.rect(ox + 7, 11, 18, 1, hat_dark) c.outline(ox, 0, 32, 48) def build_character_atlas(): c = Canvas(1024, 48) cell = 0 for variant in VARIANTS: for direction in DIRECTIONS: for frame in (0, 1): draw_character_cell(c, cell * 32, variant, direction, frame) cell += 1 return c def build_interior_atlas(): c = Canvas(392, 96) floor = (172, 138, 96, 255) floor_line = (146, 113, 74, 255) floor_light = (188, 155, 110, 255) wall = (225, 216, 195, 255) wall_light = (238, 232, 214, 255) wall_seam = (196, 180, 152, 255) wood = (119, 76, 47, 255) wood_light = (164, 111, 61, 255) wood_dark = (83, 52, 39, 255) frame_wood = (94, 66, 40, 255) ink = (61, 45, 49, 255) brass = (196, 158, 74, 255) sky = (86, 143, 170, 255) cloud = (214, 231, 238, 255) x0, y0 = 0, 64 c.rect(x0, y0, 32, 32, floor) for y in (7, 15, 23, 31): c.rect(x0, y0 + y, 32, 1, floor_line) c.rect(x0 + 20, y0, 1, 7, floor_line) for x in (6, 26): c.rect(x0 + x, y0 + 8, 1, 7, floor_line) c.rect(x0 + 13, y0 + 16, 1, 7, floor_line) for x in (3, 22): c.rect(x0 + x, y0 + 24, 1, 7, floor_line) for y in range(32): for x in range(32): if (x * 11 + y * 5) % 37 == 0: c.set(x0 + x, y0 + y, floor_light) x0, y0 = 32, 64 c.rect(x0, y0, 32, 32, wall) c.rect(x0, y0, 32, 1, wall_light) c.rect(x0 + 15, y0, 2, 32, wall_seam) c.rect(x0, y0 + 28, 32, 4, floor_line) c.rect(x0, y0 + 31, 32, 1, wood_dark) x0, y0 = 64, 56 c.rect(x0, y0, 48, 40, frame_wood) c.rect(x0 + 3, y0 + 3, 42, 28, sky) c.rect(x0 + 6, y0 + 7, 10, 3, cloud) c.rect(x0 + 20, y0 + 5, 12, 3, cloud) c.rect(x0 + 14, y0 + 3, 3, 28, frame_wood) c.rect(x0 + 30, y0 + 3, 3, 28, frame_wood) c.rect(x0 + 3, y0 + 14, 42, 3, frame_wood) c.rect(x0, y0 + 33, 48, 7, wood) c.rect(x0, y0 + 38, 48, 2, wood_dark) c.outline(x0, y0, 48, 40) x0, y0 = 112, 48 c.rect(x0 + 6, y0 + 16, 8, 28, frame_wood) c.rect(x0 + 50, y0 + 16, 8, 28, frame_wood) c.rect(x0 + 12, y0 + 16, 2, 28, wood_dark) c.rect(x0 + 56, y0 + 16, 2, 28, wood_dark) c.rect(x0, y0 + 4, 64, 14, wood) c.rect(x0, y0 + 4, 64, 3, wood_light) c.rect(x0, y0 + 16, 64, 2, wood_dark) c.rect(x0 + 12, y0 + 10, 6, 5, wall) c.rect(x0 + 13, y0 + 11, 4, 2, sky) c.rect(x0 + 40, y0 + 7, 10, 8, ink) c.rect(x0 + 43, y0 + 5, 4, 2, ink) c.rect(x0 + 50, y0 + 9, 3, 3, ink) c.outline(x0, y0, 64, 48) x0, y0 = 176, 0 c.rect(x0, y0, 48, 96, frame_wood) c.rect(x0, y0, 5, 96, wood_dark) c.rect(x0 + 43, y0, 5, 96, wood_dark) c.rect(x0 + 5, y0 + 4, 38, 88, (224, 211, 185, 255)) c.rect(x0 + 9, y0 + 8, 30, 14, (238, 232, 214, 255)) c.rect(x0 + 9, y0 + 22, 30, 2, wall_seam) quilt = (114, 62, 67, 255) quilt_line = (94, 48, 52, 255) c.rect(x0 + 5, y0 + 28, 38, 64, quilt) c.rect(x0 + 5, y0 + 28, 38, 2, quilt_line) for y in (38, 48, 58, 68, 78): c.rect(x0 + 5, y0 + y, 38, 2, quilt_line) c.rect(x0, y0 + 88, 48, 6, wood_dark) c.rect(x0, y0 + 92, 48, 2, ink) c.outline(x0, y0, 48, 96) x0, y0 = 224, 0 c.rect(x0, y0, 32, 96, ink) c.rect(x0 + 4, y0 + 4, 24, 88, (142, 102, 72, 255)) c.rect(x0 + 6, y0 + 6, 20, 84, (163, 124, 86, 255)) c.rect(x0 + 8, y0 + 60, 16, 4, frame_wood) c.rect(x0 + 10, y0 + 56, 12, 4, frame_wood) c.rect(x0 + 12, y0 + 52, 8, 4, frame_wood) c.rect(x0 + 8, y0 + 68, 16, 2, (188, 163, 126, 255)) c.outline(x0, y0, 32, 96) x0, y0 = 256, 32 rug = (150, 62, 58, 255) rug_inner = (171, 84, 72, 255) rug_dark = (134, 52, 50, 255) c.rect(x0, y0, 96, 64, rug) c.rect(x0, y0, 96, 5, brass) c.rect(x0, y0 + 59, 96, 5, brass) c.rect(x0, y0, 5, 64, brass) c.rect(x0 + 91, y0, 5, 64, brass) c.rect(x0 + 9, y0 + 9, 78, 46, rug_inner) for dy in range(-9, 10): w = (9 - abs(dy)) * 2 c.rect(x0 + 48 - w // 2, y0 + 32 + dy, w, 1, brass) for cx, cy in ((12, 12), (82, 12), (12, 50), (82, 50)): c.rect(x0 + cx, y0 + cy, 2, 2, brass) for y in range(9, 55): for x in range(9, 87): if (x * 7 + y * 13) % 41 == 0: c.set(x0 + x, y0 + y, rug_dark) x0, y0 = 352, 60 c.rect(x0, y0 + 8, 40, 28, wood) c.rect(x0, y0, 40, 12, frame_wood) c.rect(x0, y0, 40, 2, wood_light) c.rect(x0, y0 + 11, 40, 2, ink) c.rect(x0 + 17, y0 + 9, 6, 8, brass) c.rect(x0 + 19, y0 + 12, 2, 3, ink) for bx, by in ((0, 8), (37, 8), (0, 31), (37, 31)): c.rect(x0 + bx, y0 + by, 3, 3, wood_light) c.rect(x0, y0 + 33, 40, 3, ink) c.outline(x0, y0, 40, 36) return c def main(): ASSET_DIR.mkdir(parents=True, exist_ok=True) targets = [ ("jiangnan-world.png", build_world_atlas(), 256, 32), ("jiangnan-characters.png", build_character_atlas(), 1024, 48), ("jiangnan-interior.png", build_interior_atlas(), 392, 96), ] for name, canvas, w, h in targets: path = ASSET_DIR / name write_png(path, canvas) verify_png(path, w, h) if __name__ == "__main__": main()