#!/usr/bin/env python3 """Compose the local-only sprite atlases from the raw asset packs. Both inputs and outputs are licence-restricted and therefore untracked (see docs/ASSETS.md), which is why this is a build step rather than a committed file: anyone with the packs can regenerate, and nobody without them is blocked. pip install pillow && python3 tools/build_local_assets.py The bullet pack's layout is not obvious and cost a wrong guess once: * Each of the 8 PNGs is one ANIMATION FRAME, not one sprite sheet. * Within a file, a row is a bullet shape and a column is a COLOUR variant. So animating means cycling files, not walking columns -- walking columns just cycles through palettes while the shape sits still. Output is one tidy atlas: 8 frames across, one row per SimConfig.KIND_*. """ import os import sys try: from PIL import Image except ImportError: sys.exit("needs pillow: pip install pillow") ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) SRC = os.path.join(ROOT, "assetpacks", "New_All_Fire_Bullet_Pixel_16x16", "All_Fire_Bullet_Pixel_16x16_%02d.png") OUT = os.path.join(ROOT, "assets", "local", "bullets.png") CELL = 16 FRAMES = 8 # (source row, source column) per bullet kind, in SimConfig.KIND_* order. # Column selects the colour: 0 red, 1 orange, 2 amber, 3 gold, 4 bright yellow. # # Cells must have EMPTY BORDERS in every frame. Several sprites in this pack # overflow their 16px cell and bleed into the neighbour below, so a cell picked # purely by eye can drag in a slice of the sprite above it -- which is exactly # what put a solid bar across the top of the first "heavy" I chose. KINDS = [ ("player_shot", 7, 4), # small bright-yellow dot ("orb", 3, 1), # round orange orb ("needle", 6, 6), # compact dart, drawn rotated to velocity ("heavy", 5, 0), # larger red ball ] def main() -> None: for f in range(FRAMES): if not os.path.exists(SRC % f): sys.exit("missing source frame: %s\n" "The raw pack is untracked; see assets/local/README.md." % (SRC % f)) out = Image.new("RGBA", (CELL * FRAMES, CELL * len(KINDS)), (0, 0, 0, 0)) for f in range(FRAMES): src = Image.open(SRC % f).convert("RGBA") for k, (_name, row, col) in enumerate(KINDS): box = (col * CELL, row * CELL, (col + 1) * CELL, (row + 1) * CELL) out.paste(src.crop(box), (f * CELL, k * CELL)) os.makedirs(os.path.dirname(OUT), exist_ok=True) # Guard the rule the KINDS comment describes, so a future pick that bleeds # fails loudly here instead of looking like a rendering bug. for k in range(len(KINDS)): for f in range(FRAMES): for x in range(CELL): for y in (0, CELL - 1): if out.getpixel((f * CELL + x, k * CELL + y))[3] > 40: sys.exit("cell for %s bleeds at its border; pick another" % KINDS[k][0]) out.save(OUT) print("wrote %s (%dx%d): %d kinds x %d frames" % (OUT, out.width, out.height, len(KINDS), FRAMES)) for k, (name, row, col) in enumerate(KINDS): print(" row %d = %-12s (source row %2d, colour col %d)" % (k, name, row, col)) if __name__ == "__main__": main()