summaryrefslogtreecommitdiff
path: root/scripts/make-cc0-art.py
blob: 897ec4f741fdc34b04ad34f5140c0355ec0f9ab3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
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()