DramaticShapeVoxelMod archival mirror · read-only
tools/shiny_sprite_sheet.py

shiny_sprite_sheet.py

tools/shiny_sprite_sheet.py4.5 KB100644508d8e845c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
"""Build the normal-vs-shiny sprite sheet for all 151.

    luajit mods/DramaticShapeVoxelMod/tools/dump_shiny_palettes.lua > pals.json
    python mods/DramaticShapeVoxelMod/tools/shiny_sprite_sheet.py pals.json OUT.png

Run from the PROJECT ROOT.

WHY THIS IS A PALETTE JOB. The game's battle pics carry no colour: they are
four-shade DMG grey (255/170/85/0 plus transparency). Under ADVANCED the
colour comes entirely from a per-species palette laid over that art. So a
"shiny sprite" is the same pixels under a shifted palette -- which is what
this composites, using the real palettes out of data/palettes_gbc.lua and the
real shift out of the mod's own colour tables.

Each Pokemon appears as a PAIR, normal beside shiny, because a lone shiny
sprite says nothing about what changed.
"""
import json
import os
import sys
from PIL import Image, ImageDraw

ROOT = os.getcwd()
FRONT = os.path.join(ROOT, "assets", "generated", "battle", "front")

pals = json.load(open(sys.argv[1], encoding="utf-8"))
OUT = sys.argv[2] if len(sys.argv) > 2 else "shiny_sprites.png"

# the four DMG shades the art is drawn in, lightest first, matching the
# palette's own colour order
SHADES = [255, 170, 85, 0]

SCALE = 2
SW = 40 * SCALE            # sprite box
PAD = 3
LABEL = 11
CELL_W = SW * 2 + PAD      # a normal/shiny pair
CELL_H = SW + LABEL
COLS = 8                   # pairs per row
MARGIN = 10
GAP_X, GAP_Y = 14, 8


def slug_for(species):
    """assets/generated/battle/front filenames, which are not the species key."""
    s = species.lower()
    cands = [s, s.replace("_", ""), s.replace("_", "."),
             s.replace("_f", "f").replace("_m", "m")]
    for c in cands:
        p = os.path.join(FRONT, c + ".png")
        if os.path.exists(p):
            return p
    return None


def colorize(img, pal):
    """Map the four DMG shades onto a palette. Alpha is carried through."""
    src = img.convert("RGBA")
    out = Image.new("RGBA", src.size, (0, 0, 0, 0))
    sp, op = src.load(), out.load()
    for y in range(src.size[1]):
        for x in range(src.size[0]):
            r, g, b, a = sp[x, y]
            if a < 8:
                continue
            # the art is grey, so any channel identifies the shade; nearest
            # rather than exact, because a scaled or re-encoded asset can be
            # a unit off
            best, bi = None, 0
            for i, sh in enumerate(SHADES):
                d = abs(r - sh)
                if best is None or d < best:
                    best, bi = d, i
            c = pal[bi]
            op[x, y] = (c[0], c[1], c[2], a)
    return out


rows = (len(pals) + COLS - 1) // COLS
W = MARGIN * 2 + COLS * CELL_W + (COLS - 1) * GAP_X
H = MARGIN * 2 + 34 + rows * (CELL_H + GAP_Y)

sheet = Image.new("RGB", (W, H), (24, 24, 28))
d = ImageDraw.Draw(sheet)
d.text((MARGIN, 8),
       "Gen 1 battle sprites -- NORMAL (left) vs SHINY (right) of each pair."
       "  ADVANCED palettes, shifted by the Stadium shiny values.",
       fill=(235, 235, 235))
d.text((MARGIN, 21),
       "The art is 4-shade DMG grey; all colour is the palette, so a shiny"
       " sprite is the same pixels under a shifted palette.",
       fill=(150, 150, 158))

missing = []
for i, e in enumerate(pals):
    path = slug_for(e["species"])
    cx = MARGIN + (i % COLS) * (CELL_W + GAP_X)
    cy = MARGIN + 34 + (i // COLS) * (CELL_H + GAP_Y)
    if not path:
        missing.append(e["species"])
        continue
    src = Image.open(path)
    n = colorize(src, e["normal"]).resize((SW, SW), Image.NEAREST)
    s = colorize(src, e["shiny"]).resize((SW, SW), Image.NEAREST)
    # a faint plate behind each half so a dark shiny is not lost on the
    # background -- the same plate under both, so it cannot flatter one
    d.rectangle([cx, cy, cx + SW - 1, cy + SW - 1], fill=(44, 44, 50))
    d.rectangle([cx + SW + PAD, cy, cx + SW * 2 + PAD - 1, cy + SW - 1],
                fill=(44, 44, 50))
    sheet.paste(n, (cx, cy), n)
    sheet.paste(s, (cx + SW + PAD, cy), s)
    tag = "%03d %s" % (e["dex"], e["name"][:11])
    same = e["normal"] == e["shiny"]
    d.text((cx + 1, cy + SW + 1), tag,
           fill=(120, 120, 128) if same else (225, 225, 232))

d.text((MARGIN, H - 12),
       "grey label = palette identical between the two (that species' shiny"
       " does not move this palette)",
       fill=(120, 120, 128))

sheet.save(OUT)
print("wrote", OUT, sheet.size)
if missing:
    print("no sprite for:", ", ".join(missing))
same_n = sum(1 for e in pals if e["normal"] == e["shiny"])
print("pairs: %d, palettes that shift: %d, identical: %d"
      % (len(pals), len(pals) - same_n, same_n))