#!/usr/bin/env python3 """Turn the raw FLUX assets into game-ready textures in public/sprites/. Sprites (magenta #ff00ff background) get chroma-keyed to transparency, autocropped, and centered on a square canvas. Tiles (full-bleed textures) are just resized square. Output filename = the texture key GameScene.js loads. /opt/homebrew/bin/uv run --with pillow --with numpy python assets/process_sprites.py """ from pathlib import Path from PIL import Image, ImageFilter import numpy as np HERE = Path(__file__).resolve().parent SRC = HERE / "generated" OUT = HERE.parent / "public" / "sprites" OUT.mkdir(parents=True, exist_ok=True) # game texture key -> (source file, size px, is_tile) TABLE = { "floor": ("floor_parquet.png", 96, True), "floor_carpet": ("floor_carpet.png", 96, True), "floor_checker": ("floor_checker.png", 96, True), "wall": ("wall_crate.png", 48, True), "wall_shelf": ("wall_shelf.png", 48, True), "wall_poster": ("wall_poster.png", 48, True), "pirate": ("enemy_record_pirate.png", 48, False), "ghoul": ("enemy_crate_ghoul.png", 48, False), "gold": ("pickup_goldrecord.png", 38, False), "floor_archive": ("floor_archive.png", 96, True), "wall_archive": ("wall_archive.png", 48, True), "mite": ("enemy_dust_mite.png", 26, False), "queen": ("enemy_mite_queen.png", 96, False), "lamp": ("prop_lamp.png", 40, False), "player": ("player_digger.png", 48, False), "poser": ("enemy_poser.png", 48, False), "nerd": ("enemy_gearnerd.png", 48, False), "soundguy": ("enemy_soundguy.png", 48, False), "generator": ("gen_discount_bin.png", 48, False), "exit": ("prop_djbooth.png", 48, False), "door": ("prop_velvet_rope.png", 48, False), "espresso": ("pickup_espresso.png", 38, False), "lanyard": ("pickup_lanyard.png", 38, False), "airhorn": ("pickup_airhorn.png", 38, False), "vinyl": ("pickup_vinyl.png", 24, False), "floor_warehouse": ("floor_warehouse.png", 96, True), "wall_warehouse": ("wall_warehouse.png", 48, True), "floor_festival": ("floor_festival.png", 96, True), "wall_festival": ("wall_festival.png", 48, True), "gatekeeper": ("enemy_gatekeeper.png", 52, False), "shazam": ("enemy_shazam_zombie.png", 48, False), "warped": ("enemy_warped_record.png", 44, False), "conductor": ("boss_spectral_conductor.png", 96, False), "rpm78": ("boss_78rpm.png", 120, False), "port_digger": ("portrait_digger.png", 112, False), "port_selector": ("portrait_selector.png", 112, False), "port_promoter": ("portrait_promoter.png", 112, False), "port_soundtech": ("portrait_soundtech.png", 112, False), } # full-art images: downscale + copy, never chroma-keyed (they aren't sprites) ART = { "logo": ("logo_marquee_b.png", 800), } def chroma_key(im): arr = np.array(im.convert("RGBA")) r, g, b = arr[..., 0].astype(int), arr[..., 1].astype(int), arr[..., 2].astype(int) # magenta: high red+blue, low green, red~=blue (catches the darker shadow halo too) mask = (r > 135) & (b > 135) & (g < 100) & (np.abs(r - b) < 70) arr[..., 3][mask] = 0 return Image.fromarray(arr, "RGBA") for key, (fname, size, is_tile) in TABLE.items(): im = Image.open(SRC / fname).convert("RGBA") if is_tile: im = im.resize((size, size), Image.LANCZOS) else: im = chroma_key(im) bbox = im.getbbox() if bbox: im = im.crop(bbox) im.thumbnail((size, size), Image.LANCZOS) canvas = Image.new("RGBA", (size, size), (0, 0, 0, 0)) canvas.paste(im, ((size - im.width) // 2, (size - im.height) // 2), im) im = canvas # crisp up the LANCZOS-softened downscale im = im.filter(ImageFilter.UnsharpMask(radius=1.2, percent=150, threshold=1)) im.save(OUT / f"{key}.png") print(f"{key:10s} <- {fname:24s} {size}px {'tile' if is_tile else 'keyed'}") for key, (fname, width) in ART.items(): im = Image.open(SRC / fname).convert("RGB") im.thumbnail((width, width * 2), Image.LANCZOS) im.save(OUT / f"{key}.png") print(f"{key:10s} <- {fname:24s} art {im.width}x{im.height}") print(f"\nwrote {len(TABLE) + len(ART)} textures to {OUT}")