#!/usr/bin/env python3 """Build the VINYLGOD terrain tile atlases from the flux textures. One atlas PNG per surface, each holding N 32x32 variants as frames (FrameSize metadata). Tiles are cut from a downscaled texture and edge-blended against their own wrap so random placement does not show hard seams. All tiles stay terrain type `Clear` — this is a pure visual upgrade, no pathing/placement changes. """ import os, struct, zlib from PIL import Image, ImageChops SRC = os.path.join(os.path.dirname(__file__), '..', 'assets_src', 'terrain') DST = os.path.join(os.path.dirname(__file__), '..', 'mods', 'vinylgod', 'tilesets', 'vinylgod') os.makedirs(DST, exist_ok=True) TILE = 32 # surface -> (grid, target average brightness nudge) SURFACES = { 'asphalt': 4, # 16 base variants 'concrete': 3, # 9 'dirt': 3, 'grooves': 3, 'rubble': 3, } def seamless(im): """Cheap wrap-blend so tile edges meet: cross-fade with a half-offset copy.""" w, h = im.size off = ImageChops.offset(im, w // 2, h // 2) mask = Image.new('L', (w, h), 0) px = mask.load() fw, fh = w // 6, h // 6 for y in range(h): for x in range(w): ex = min(x, w - 1 - x) ey = min(y, h - 1 - y) v = 255 if ex < fw: v = min(v, int(255 * ex / fw)) if ey < fh: v = min(v, int(255 * ey / fh)) px[x, y] = 255 - v # strong at edges return Image.composite(off, im, mask) def chunk(tag, data): return struct.pack('>I', len(data)) + tag + data + struct.pack('>I', zlib.crc32(tag + data)) def save_frames(path, frames): w, h = TILE * len(frames), TILE sheet = Image.new('RGB', (w, h)) for i, f in enumerate(frames): sheet.paste(f, (i * TILE, 0)) raw = sheet.convert('RGBA').tobytes() rows = b''.join(b'\x00' + raw[y * w * 4:(y + 1) * w * 4] for y in range(h)) out = b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', struct.pack('>IIBBBBB', w, h, 8, 6, 0, 0, 0)) out += chunk(b'tEXt', b'FrameSize\x00' + f'{TILE},{TILE}'.encode()) out += chunk(b'IDAT', zlib.compress(rows)) + chunk(b'IEND', b'') open(path, 'wb').write(out) def stats(im): px = list(im.getdata()) n = len(px) out = [] for c in range(3): vals = [p[c] for p in px] m = sum(vals) / n sd = (sum((v - m) ** 2 for v in vals) / n) ** 0.5 or 1.0 out.append((m, sd)) return out def harmonize(im, target, strength=1.0): """Pull a surface toward the base asphalt's tone so surfaces read as one material family instead of five clashing ones.""" src = stats(im) px = im.load() w, h = im.size for y in range(h): for x in range(w): r, g, b = px[x, y][:3] new = [] for c, v in enumerate((r, g, b)): sm, ssd = src[c] tm, tsd = target[c] z = (v - sm) / ssd nv = tm + z * tsd nv = v + (nv - v) * strength new.append(max(0, min(255, int(nv)))) px[x, y] = tuple(new) return im base = Image.open(os.path.join(SRC, 'asphalt.png')).convert('RGB') TARGET = stats(base.resize((128, 128), Image.LANCZOS)) counts = {} for name, grid in SURFACES.items(): src = Image.open(os.path.join(SRC, f'{name}.png')).convert('RGB') src = src.resize((TILE * grid, TILE * grid), Image.LANCZOS) if name != 'asphalt': src = harmonize(src, TARGET) frames = [] for gy in range(grid): for gx in range(grid): t = src.crop((gx * TILE, gy * TILE, (gx + 1) * TILE, (gy + 1) * TILE)) frames.append(seamless(t)) save_frames(os.path.join(DST, f'{name}.png'), frames) counts[name] = len(frames) print(f'{name}: {len(frames)} tiles') print('counts =', counts)