-- 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([[
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.
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.
%d species · %d visibly recoloured · " .. "%d barely moved · %d with no palette
\n") :format(#rows, moved, still, missing)) w("| # | species | pal | kind | " .. "slide h / s / l | normal | shiny | Δ | " .. "
|---|---|---|---|---|---|---|---|
| %03d | %s | %s | " .. "%s | %s | %s | %s | " .. "%s |