-- 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([[ Shiny battle palettes

Shiny battle palettes — normal beside shiny

What ShinyPics 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. slide species use Stadium's declared values; the five table 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 red barely moved.

]]) w(("

%d species · %d visibly recoloured · " .. "%d barely moved · %d with no palette

\n") :format(#rows, moved, still, missing)) w("" .. "" .. "\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] = ("") :format(fixed, hex(c), c[1], c[2], c[3]) end end return table.concat(o) end local s = row.slide w(("" .. "" .. "\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("
#speciespalkindslide h / s / lnormalshinyΔ
%03d%s%s%s%s%s%s%s
\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))