diff --git a/lib/ShinyPalette.lua b/lib/ShinyPalette.lua index c493246..a7747c1 100644 --- a/lib/ShinyPalette.lua +++ b/lib/ShinyPalette.lua @@ -355,6 +355,32 @@ function ShinyPalette.tintFor(dex) return t end +-- A transform for PALETTE colours rather than texture texels. +-- +-- The two are not the same job, and using the texel transform on a palette +-- quietly does nothing for five species. A lookup table answers only the +-- colours that are in it -- the ones its model is painted with -- and the +-- engine's ADVANCED palettes are a different set of colours entirely +-- (BLUEMON's blue is not any blue on the Gyarados model). Asked to shift a +-- palette, the table therefore returns it unchanged, and the most dramatic +-- shiny in the game comes out identical. +-- +-- So: slide species use the slide, which is defined on all colours. Table +-- species fall back to their tint multiplier, which IS derived from the +-- table and does carry its direction. +function ShinyPalette.paletteTransform(dex) + local spec = ShinyPalette.forDex(dex) + if not spec then return nil end + if not spec.lut then return ShinyPalette.transform(spec) end + local t = ShinyPalette.tintFor(dex) + if not t then return nil end + return function(r, g, b) + return floor(min(255, r * t[1]) + 0.5), + floor(min(255, g * t[2]) + 0.5), + floor(min(255, b * t[3]) + 0.5) + end +end + -- ------- the pass over one species' whole texture array -- Recolour `textures` in place, skipping the ones that must not move. diff --git a/tools/dump_shiny_palettes.lua b/tools/dump_shiny_palettes.lua new file mode 100644 index 0000000..cf4c90f --- /dev/null +++ b/tools/dump_shiny_palettes.lua @@ -0,0 +1,69 @@ +-- Dump every species' ADVANCED palette and its shiny-shifted twin, as JSON. +-- +-- luajit mods/DramaticShapeVoxelMod/tools/dump_shiny_palettes.lua > pals.json +-- +-- Run from the PROJECT ROOT. +-- +-- The game's battle pics are four-shade DMG grey (0/85/170/255) -- there is +-- no colour in the art at all. Under ADVANCED (`redpp`) the colour comes +-- entirely from a per-species palette applied over the top, which is why a +-- shiny SPRITE is a shifted palette rather than repainted art. This dumps +-- both halves so a comparison sheet can be built from them. +-- +-- Reads the real generated data directly rather than going through +-- PaletteFX: the headless fixture dataset carries FIXMON placeholders, not +-- the 151, so a dump driven through it is of nothing. + +local POK = dofile("data/generated/pokemon.lua") +local PACK = dofile("data/palettes_gbc.lua") + +local V = { path = "mods/DramaticShapeVoxelMod" } +local loaded = {} +function V.require(name) + if loaded[name] == nil then + loaded[name] = assert(loadfile(V.path .. "/lib/" .. name .. ".lua"))(V) + end + return loaded[name] +end +V.mod = { log = { warn = function() end, info = function() end } } +local ShinyPalette = V.require("ShinyPalette") + +local mons = POK.pokemon or POK +local rows = {} +for species, def in pairs(mons) do + local dex = type(def) == "table" and def.dex + if type(species) == "string" and dex and dex >= 1 and dex <= 151 then + rows[#rows + 1] = { species = species, dex = dex, + name = def.name or species } + end +end +table.sort(rows, function(a, b) return a.dex < b.dex end) + +local function esc(s) return (tostring(s):gsub('"', '\\"')) end + +io.write("[\n") +for i, r in ipairs(rows) do + local palName = PACK.pokemon[r.species] + local pal = palName and PACK.palettes[palName] + if pal then + local fn = ShinyPalette.paletteTransform(r.dex) + local n, s = {}, {} + for k = 1, 4 do + local c = pal[k] or pal[#pal] + local cr, cg, cb = c[1], c[2], c[3] + n[k] = ("[%d,%d,%d]"):format(cr, cg, cb) + if fn then + local sr, sg, sb = fn(cr, cg, cb) + s[k] = ("[%d,%d,%d]"):format(sr, sg, sb) + else + s[k] = n[k] + end + end + io.write(('%s{"dex":%d,"species":"%s","name":"%s","pal":"%s",' + .. '"normal":[%s],"shiny":[%s],"shifted":%s}') + :format(i > 1 and ",\n" or "", r.dex, esc(r.species), esc(r.name), + esc(palName), table.concat(n, ","), table.concat(s, ","), + fn and "true" or "false")) + end +end +io.write("\n]\n") diff --git a/tools/shiny_sprite_sheet.py b/tools/shiny_sprite_sheet.py new file mode 100644 index 0000000..508d8e8 --- /dev/null +++ b/tools/shiny_sprite_sheet.py @@ -0,0 +1,128 @@ +"""Build the normal-vs-shiny sprite sheet for all 151. + + luajit mods/DramaticShapeVoxelMod/tools/dump_shiny_palettes.lua > pals.json + python mods/DramaticShapeVoxelMod/tools/shiny_sprite_sheet.py pals.json OUT.png + +Run from the PROJECT ROOT. + +WHY THIS IS A PALETTE JOB. The game's battle pics carry no colour: they are +four-shade DMG grey (255/170/85/0 plus transparency). Under ADVANCED the +colour comes entirely from a per-species palette laid over that art. So a +"shiny sprite" is the same pixels under a shifted palette -- which is what +this composites, using the real palettes out of data/palettes_gbc.lua and the +real shift out of the mod's own colour tables. + +Each Pokemon appears as a PAIR, normal beside shiny, because a lone shiny +sprite says nothing about what changed. +""" +import json +import os +import sys +from PIL import Image, ImageDraw + +ROOT = os.getcwd() +FRONT = os.path.join(ROOT, "assets", "generated", "battle", "front") + +pals = json.load(open(sys.argv[1], encoding="utf-8")) +OUT = sys.argv[2] if len(sys.argv) > 2 else "shiny_sprites.png" + +# the four DMG shades the art is drawn in, lightest first, matching the +# palette's own colour order +SHADES = [255, 170, 85, 0] + +SCALE = 2 +SW = 40 * SCALE # sprite box +PAD = 3 +LABEL = 11 +CELL_W = SW * 2 + PAD # a normal/shiny pair +CELL_H = SW + LABEL +COLS = 8 # pairs per row +MARGIN = 10 +GAP_X, GAP_Y = 14, 8 + + +def slug_for(species): + """assets/generated/battle/front filenames, which are not the species key.""" + s = species.lower() + cands = [s, s.replace("_", ""), s.replace("_", "."), + s.replace("_f", "f").replace("_m", "m")] + for c in cands: + p = os.path.join(FRONT, c + ".png") + if os.path.exists(p): + return p + return None + + +def colorize(img, pal): + """Map the four DMG shades onto a palette. Alpha is carried through.""" + src = img.convert("RGBA") + out = Image.new("RGBA", src.size, (0, 0, 0, 0)) + sp, op = src.load(), out.load() + for y in range(src.size[1]): + for x in range(src.size[0]): + r, g, b, a = sp[x, y] + if a < 8: + continue + # the art is grey, so any channel identifies the shade; nearest + # rather than exact, because a scaled or re-encoded asset can be + # a unit off + best, bi = None, 0 + for i, sh in enumerate(SHADES): + d = abs(r - sh) + if best is None or d < best: + best, bi = d, i + c = pal[bi] + op[x, y] = (c[0], c[1], c[2], a) + return out + + +rows = (len(pals) + COLS - 1) // COLS +W = MARGIN * 2 + COLS * CELL_W + (COLS - 1) * GAP_X +H = MARGIN * 2 + 34 + rows * (CELL_H + GAP_Y) + +sheet = Image.new("RGB", (W, H), (24, 24, 28)) +d = ImageDraw.Draw(sheet) +d.text((MARGIN, 8), + "Gen 1 battle sprites -- NORMAL (left) vs SHINY (right) of each pair." + " ADVANCED palettes, shifted by the Stadium shiny values.", + fill=(235, 235, 235)) +d.text((MARGIN, 21), + "The art is 4-shade DMG grey; all colour is the palette, so a shiny" + " sprite is the same pixels under a shifted palette.", + fill=(150, 150, 158)) + +missing = [] +for i, e in enumerate(pals): + path = slug_for(e["species"]) + cx = MARGIN + (i % COLS) * (CELL_W + GAP_X) + cy = MARGIN + 34 + (i // COLS) * (CELL_H + GAP_Y) + if not path: + missing.append(e["species"]) + continue + src = Image.open(path) + n = colorize(src, e["normal"]).resize((SW, SW), Image.NEAREST) + s = colorize(src, e["shiny"]).resize((SW, SW), Image.NEAREST) + # a faint plate behind each half so a dark shiny is not lost on the + # background -- the same plate under both, so it cannot flatter one + d.rectangle([cx, cy, cx + SW - 1, cy + SW - 1], fill=(44, 44, 50)) + d.rectangle([cx + SW + PAD, cy, cx + SW * 2 + PAD - 1, cy + SW - 1], + fill=(44, 44, 50)) + sheet.paste(n, (cx, cy), n) + sheet.paste(s, (cx + SW + PAD, cy), s) + tag = "%03d %s" % (e["dex"], e["name"][:11]) + same = e["normal"] == e["shiny"] + d.text((cx + 1, cy + SW + 1), tag, + fill=(120, 120, 128) if same else (225, 225, 232)) + +d.text((MARGIN, H - 12), + "grey label = palette identical between the two (that species' shiny" + " does not move this palette)", + fill=(120, 120, 128)) + +sheet.save(OUT) +print("wrote", OUT, sheet.size) +if missing: + print("no sprite for:", ", ".join(missing)) +same_n = sum(1 for e in pals if e["normal"] == e["shiny"]) +print("pairs: %d, palettes that shift: %d, identical: %d" + % (len(pals), len(pals) - same_n, same_n))