mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 13:30:52 +02:00
189 lines
7.3 KiB
Lua
189 lines
7.3 KiB
Lua
-- 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))
|