vinylgauntlet/assets/process_sprites.py
type-two 8e967407c1 Tier 2 complete: digging, treasure rooms, the Archive, infinite mazes
- Digging: hold E (or DIG touch button) by a crate wall, 2s rooted channel, once
  per tile. Loot: 70% +200 vibe, 25% gold record, 4% grail (full vibe), 1%
  bootleg (every bin on the level erupts).
- Treasure rooms: every 3rd level, 20s bonus room strewn with gold, no enemies,
  booth exits instantly, "CLOSING TIME" when the timer dies.
- THE ARCHIVE (4th theme): FLUX-generated dark tileset, real darkness via
  RenderTexture with radial glow holes at player + lamps, dust mites spawn in
  packs of 3, Mite Queen miniboss (12 HP, broods mites every 5s, airhorn-immune,
  drops 3 gold + 1000 pts). Archive tune pool: 195 dnb/hardcore/breakbeat/
  electro 12"s (718 total tunes now).
- Infinite: levels past the 4 authored maps are deterministic seeded mazes
  (recursive backtracker + loops + rooms), difficulty scales with depth.

All verified in-browser via engine stepping.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 17:35:42 +10:00

77 lines
3.2 KiB
Python

#!/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),
}
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'}")
print(f"\nwrote {len(TABLE)} textures to {OUT}")