shiny_pic_sheet.py
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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
#!/usr/bin/env python3 """Every battle pic, baked normal beside baked shiny, as one HTML page. luajit mods/DramaticShapeVoxelMod/tools/shiny_pic_dump.lua > /tmp/pics.tsv python mods/DramaticShapeVoxelMod/tools/shiny_pic_sheet.py /tmp/pics.tsv Run from the PROJECT ROOT. Writes mods/DramaticShapeVoxelMod/.claude/shiny_update/sprites.html, self-contained (every pic is a data: URI), so it can be opened or sent on its own. ------- the bake is the engine's, exactly src/battle/BattleState.lua:147 getImage() is the only place a battle pic gets its colour, and it does it ONCE at load with mapPixel: col = r > 0.83 and c[1] or r > 0.5 and c[2] or r > 0.17 and c[3] or c[4] Four-shade DMG art, keyed on the RED channel alone, snapped to the species palette. That line is reproduced below rather than approximated, because the whole question this sheet answers is what the player will actually see -- an approximation of the bake would be answering a different one. The colours come from tools/shiny_pic_dump.lua, which runs the real ShinyPics wrap over the real PaletteFX, so nothing here re-derives them. """ import base64 import io import os import sys from PIL import Image ROOT = os.getcwd() MOD = "mods/DramaticShapeVoxelMod" OUT = os.path.join(MOD, ".claude/shiny_update/sprites.html") SCALE = 3 # nearest-neighbour, so the pixels stay pixels def parse_color(s): r, g, b = (int(v) for v in s.split(",")) return (r, g, b) def bake(img, pal): """getImage's mapPixel: red channel picks the shade, alpha is kept.""" img = img.convert("RGBA") px = img.load() w, h = img.size for y in range(h): for x in range(w): r, g, b, a = px[x, y] if a == 0: continue f = r / 255.0 if f > 0.83: c = pal[0] elif f > 0.5: c = pal[1] elif f > 0.17: c = pal[2] else: c = pal[3] px[x, y] = (c[0], c[1], c[2], a) return img def data_uri(img): img = img.resize((img.width * SCALE, img.height * SCALE), Image.NEAREST) buf = io.BytesIO() img.save(buf, format="PNG") return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode() def main(): src = sys.argv[1] if len(sys.argv) > 1 else "-" stream = sys.stdin if src == "-" else open(src, encoding="utf-8") rows = [] with stream: for line in stream: line = line.rstrip("\n") if not line: continue f = line.split("\t") rows.append({ "dex": int(f[0]), "name": f[1], "path": f[2], "kind": f[3], "normal": [parse_color(c) for c in f[4:8]], "shiny": [parse_color(c) for c in f[8:12]], }) cards, missing = [], 0 for row in rows: path = os.path.join(ROOT, row["path"]) if not os.path.exists(path): missing += 1 continue art = Image.open(path) n = data_uri(bake(art.copy(), row["normal"])) s = data_uri(bake(art.copy(), row["shiny"])) cards.append( '<figure class="k-{kind}">' '<div class="pair"><img src="{n}" alt="{name} normal">' '<img src="{s}" alt="{name} shiny"></div>' '<figcaption>{dex:03d} {name}<span>{kind}</span></figcaption>' "</figure>".format(kind=row["kind"], n=n, s=s, name=row["name"], dex=row["dex"]) ) html = """<!doctype html> <html><head><meta charset="utf-8"><title>Shiny battle pics</title> <style> body { font: 13px/1.5 ui-monospace, Menlo, Consolas, monospace; margin: 24px; background: #14151a; color: #e6e6ea; } h1 { font-size: 18px; margin: 0 0 4px; } p.lede { color: #9aa0aa; max-width: 64em; margin: 0 0 20px; } .grid { display: flex; flex-wrap: wrap; gap: 14px; } figure { margin: 0; background: #1c1e26; border: 1px solid #2a2c34; border-radius: 6px; padding: 8px; } .k-table { border-color: #7a5a1e; } .pair { display: flex; gap: 6px; background: #fff; border-radius: 3px; padding: 2px; } img { display: block; image-rendering: pixelated; } figcaption { margin-top: 6px; color: #9aa0aa; display: flex; justify-content: space-between; gap: 10px; } figcaption span { color: #6f757f; } .k-table figcaption span { color: #ffc46b; } </style></head><body> <h1>Shiny battle pics — normal on the left, shiny on the right</h1> <p class="lede">Each pair is the same four-shade art baked twice, through the palette the game itself would use: <code>getImage</code> keys on the red channel alone and snaps to the species palette, once, at load. The colours come from the live <code>ShinyPics</code> wrap, so this is what the flat battle screen draws — not a preview of it. Bordered cards are the five species Stadium gives a real alternate texture, whose slide is measured back out of that texture rather than declared.</p> <p class="lede">%d species%s</p> <div class="grid">%s</div> </body></html> """ % (len(cards), "" if not missing else " · %d with no art on disk" % missing, "\n".join(cards)) os.makedirs(os.path.dirname(OUT), exist_ok=True) with open(OUT, "w", encoding="utf-8") as f: f.write(html) size = os.path.getsize(OUT) / 1024.0 print("%s -- %d pairs, %d missing, %.0f KB" % (OUT, len(cards), missing, size)) if __name__ == "__main__": main()