shiny_palette_sheet.lua
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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
-- Every species' battle palette, normal beside shiny, as one HTML page. -- -- luajit mods/DramaticShapeVoxelMod/tools/shiny_palette_sheet.lua -- -- Run from the PROJECT ROOT. Writes -- mods/DramaticShapeVoxelMod/.claude/shiny_update/palettes.html -- -- ------- what it is actually showing -- -- Not the shiny COLOURS table (data/shiny_colors.lua) -- that is Stadium's -- values for a model's texels, and it is already checked against the Python -- that produced it. This is the other end: what those values become after -- ShinyPics puts them through the engine's four-shade battle palette, which -- is where the flat art gets its colour and the only place a mistake there -- shows up. -- -- Two rules are visible in the output and both were bugs first: -- -- * shade 1 and shade 4 never move. They are the shared paper (255,239,255) -- and the shared ink (25,16,16), not colours anybody chose for this -- animal, and sliding them turned shiny Golbat's white navy. -- * the five TABLE species rotate hue like everyone else, because their -- slide is measured back out of their lookup table rather than falling -- back to a multiply that can only darken. -- -- The COLORS pack is whatever PaletteFX defaults to in a headless process -- (the GBC pack). The RED++ pack is a different set of four colours per -- species and would want its own sheet. package.path = "./?.lua;./?/init.lua;" .. package.path local MOD = "mods/DramaticShapeVoxelMod" local OUT = MOD .. "/.claude/shiny_update/palettes.html" -- ------- the mod namespace, enough of it (see tests/shiny_test.lua) local loaded, V = {}, {} function V.require(n) if loaded[n] == nil then loaded[n] = assert(loadfile(MOD .. "/lib/" .. n .. ".lua"))(V) end return loaded[n] end function V.data(n) return assert(loadfile(MOD .. "/data/" .. n .. ".lua"))(V) end V.path = MOD V.mod = { id = "DRAMATIC_SHAPE", log = { warn = function() end, info = function() end } } local ShinyPics = V.require("ShinyPics") local ShinyPalette = V.require("ShinyPalette") local PaletteFX = require("src.render.PaletteFX") local Data = { pokemon = dofile("data/generated/pokemon.lua"), palettes = dofile("data/generated/palettes.lua"), } assert(ShinyPics.install(), "the palette wrap did not install") -- Def/Spd/Spc all 10 and Atk 10 is the Gen 2 pattern src/pokemon/Stats.lua -- reads; any mon carrying it is shiny as far as the engine is concerned. local SHINY = { dvs = { attack = 10, defense = 10, speed = 10, special = 10, hp = 15 } } -- ------- collect, in dex order local rows = {} for name, def in pairs(Data.pokemon) do if type(def) == "table" and def.dex and def.dex >= 1 and def.dex <= 151 then rows[#rows + 1] = { name = name, dex = def.dex } end end table.sort(rows, function(a, b) return a.dex < b.dex end) local function hex(c) return ("#%02x%02x%02x"):format(c[1] or 0, c[2] or 0, c[3] or 0) end local moved, still, missing = 0, 0, 0 for _, row in ipairs(rows) do row.normal = PaletteFX.monPal(Data, row.name) row.palName = PaletteFX.monPalName(Data, row.name) ShinyPics.note({ kind = "battle", species = row.name, mon = SHINY, data = Data }) row.shiny = PaletteFX.monPal(Data, row.name) row.shinyName = PaletteFX.monPalName(Data, row.name) local spec = ShinyPalette.forDex(row.dex) row.kind = spec and (spec.lut and "table" or "slide") or "none" local slide = spec and (spec.lut and ShinyPalette.lutSlide(row.dex) or spec.slide) row.slide = slide if not (row.normal and row.shiny) then missing = missing + 1 else -- how far the two middle shades actually travelled, as the largest -- per-channel step: a row that reads 0 is a shiny nobody can see local d = 0 for i = 2, #row.normal - 1 do local a, b = row.normal[i], row.shiny[i] if type(a) == "table" and type(b) == "table" then for k = 1, 3 do d = math.max(d, math.abs((a[k] or 0) - (b[k] or 0))) end end end row.delta = d if d >= 8 then moved = moved + 1 else still = still + 1 end end end -- ------- the page local out = {} local function w(s) out[#out + 1] = s end w([[<!doctype html> <html><head><meta charset="utf-8"> <title>Shiny battle palettes</title> <style> :root { color-scheme: light dark; } 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: 62em; margin: 0 0 20px; } table { border-collapse: collapse; } th, td { padding: 3px 10px 3px 0; text-align: left; vertical-align: middle; white-space: nowrap; } th { color: #9aa0aa; font-weight: 400; border-bottom: 1px solid #2a2c34; } tr:hover { background: #1c1e26; } .sw { display: inline-block; width: 26px; height: 20px; border: 1px solid #000; vertical-align: middle; } .fixed { opacity: .45; } .kind-table { color: #ffc46b; } .kind-slide { color: #7fb2ff; } .flat { color: #ff8a8a; } td.n { text-align: right; color: #9aa0aa; } </style></head><body> <h1>Shiny battle palettes — normal beside shiny</h1> <p class="lede">What <code>ShinyPics</code> hands the battle pic cache, per species. The first and last shades are the shared paper and ink and are held still on purpose (shown faded); only the two middle shades are the Pokemon. <span class="kind-slide">slide</span> species use Stadium's declared values; the five <span class="kind-table">table</span> species use a slide measured back out of their own lookup table, which is what lets Gyarados reach red. Δ is the largest per-channel step across the two middle shades — a row in <span class="flat">red</span> barely moved.</p> ]]) w(("<p class=\"lede\">%d species · %d visibly recoloured · " .. "%d barely moved · %d with no palette</p>\n") :format(#rows, moved, still, missing)) w("<table><tr><th>#</th><th>species</th><th>pal</th><th>kind</th>" .. "<th>slide h / s / l</th><th>normal</th><th>shiny</th><th>Δ</th>" .. "</tr>\n") for _, row in ipairs(rows) do local function swatches(cols) if not cols then return "—" end local o = {} for i, c in ipairs(cols) do local fixed = (i == 1 or i == #cols) and " fixed" or "" if type(c) == "table" and c[1] then o[#o + 1] = ("<span class=\"sw%s\" style=\"background:%s\" " .. "title=\"%d,%d,%d\"></span>") :format(fixed, hex(c), c[1], c[2], c[3]) end end return table.concat(o) end local s = row.slide w(("<tr><td class=\"n\">%03d</td><td>%s</td><td>%s</td>" .. "<td class=\"kind-%s\">%s</td><td>%s</td><td>%s</td><td>%s</td>" .. "<td class=\"n%s\">%s</td></tr>\n") :format(row.dex, row.name, tostring(row.palName), row.kind, row.kind, s and ("%.0f° / %+.1f / %+.1f"):format(s.h or 0, s.s or 0, s.l or 0) or "—", swatches(row.normal), swatches(row.shiny), (row.delta and row.delta < 8) and " flat" or "", row.delta and tostring(row.delta) or "—")) end w("</table></body></html>\n") local f = assert(io.open(OUT, "wb")) f:write(table.concat(out)) f:close() print(("%s -- %d species, %d recoloured, %d barely moved, %d no palette") :format(OUT, #rows, moved, still, missing))