DramaticShapeVoxelMod archival mirror · read-only
tools/shiny_palette_sheet.lua

shiny_palette_sheet.lua

tools/shiny_palette_sheet.lua7.3 KB100644b7151c371f
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 &mdash; 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.
&Delta; is the largest per-channel step across the two middle shades &mdash;
a row in <span class="flat">red</span> barely moved.</p>
]])

w(("<p class=\"lede\">%d species &middot; %d visibly recoloured &middot; "
   .. "%d barely moved &middot; %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>&Delta;</th>"
  .. "</tr>\n")

for _, row in ipairs(rows) do
  local function swatches(cols)
    if not cols then return "&mdash;" 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&deg; / %+.1f / %+.1f"):format(s.h or 0, s.s or 0,
                                                       s.l or 0) or "&mdash;",
            swatches(row.normal), swatches(row.shiny),
            (row.delta and row.delta < 8) and " flat" or "",
            row.delta and tostring(row.delta) or "&mdash;"))
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))