- Title: FLUX marquee logo + 4 portrait class-select cards (keys 1-4 or tap) - Themes 4-5: WAREHOUSE RAVE (shazam zombie turrets firing slow requests, gatekeeper tanks) and FESTIVAL GROUNDS (wobbling warped records); tune pools extracted for both (acid/tech-house/minimal, trance/big-beat/prog) -> 1008 tunes - Spectral Conductor boss (level 8 of each 12-cycle): 20 HP, teleports to a corner every 5 hits, doubles generator rate + 1.5x enemy speed while alive - The Monstrous 78 RPM (level 12): 30 HP, bounces Pong-style, spits warped shards, only vulnerable while "changing sides" (dimmed); killing it reveals THE WHITE LABEL (??? - ???) in the notebook and starts NG+ density - Boss arenas have no exit until the boss falls - the booth appears where it died - Fix: makeGlow crashes on scene restart (createCanvas returns null if key exists) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
101 lines
4.3 KiB
Python
101 lines
4.3 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),
|
|
"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}")
|