Files
2026-08-08 18:51:01 -04:00

157 lines
6.4 KiB
Lua

-- A shiny's battle pic, genuinely recoloured.
--
-- ------- why the tint had to go
--
-- The first answer to "a shiny on the flat art" was a MULTIPLY at draw time,
-- and it was the wrong shape twice over:
--
-- * A multiply can only DARKEN. Shiny Gyarados is blue turning RED; the
-- nearest a multiply gets is a dimmer blue. Every species whose shiny is
-- lighter, or is a hue rotation rather than a dimming, came out looking
-- like the ordinary one with the brightness down -- which is exactly what
-- "shinies don't work in 2D" describes.
-- * It tinted the whole PICS LAYER, both sides at once, because that is the
-- granularity the engine's own draw has. A shiny facing an ordinary mon
-- dimmed its opponent too.
--
-- ------- where the colour actually lives
--
-- The battle pic is not drawn from four-shade art at play time. getImage
-- (src/battle/BattleState.lua:147) snaps the four DMG shades to the species'
-- palette ONCE, with mapPixel, and caches the finished image under
-- `path .. "#" .. pal.name`. By the time anything is drawn the colour is
-- already baked in, and the only way to change it is to hand that bake a
-- different palette -- which also means a different cache NAME, or the shiny
-- and the ordinary pic fight over one cache slot.
--
-- That is the whole of this file. It is the same conclusion ShinyUI reached
-- for the status screen ("the palette is what has to move"), applied to the
-- one other place a Pokemon is drawn flat.
--
-- ------- the seam
--
-- monPalette (BattleState.lua:216) is a local, so it cannot be wrapped. What
-- it calls -- PaletteFX.monPal and PaletteFX.monPalName -- are not, and they
-- are asked in that order for every battle pic the game builds.
--
-- Neither is told WHICH Pokemon is being drawn; both take a species. The
-- individual arrives one call earlier, at the engine's own `pokemon.sprite`
-- hook, which carries ctx.mon -- so the hook notes "the pic about to be built
-- is this shiny mon's" and the two palette wraps consume that note. A flag
-- rather than an argument, because the argument does not exist.
--
-- It is consumed ONCE, and matched on species as well, so a leak (monPalette
-- returns early when a species has no palette at all, and then never asks for
-- the name) cannot recolour somebody else's pic -- the worst case is one
-- extra ordinary pic built under a shiny cache key, which the next call
-- corrects.
-- the mod namespace (see main.lua): V.require loads a sibling module
local V = ...
local Shiny = V.require("Shiny")
local ShinyPalette = V.require("ShinyPalette")
local ShinyPics = {}
-- { species = <name>, dex = <n> } while a shiny's pic is being built
local pending = nil
-- The suffix that makes the shiny pic its own cache entry. Part of the
-- palette NAME rather than the path, because the name is what getImage keys
-- on and the path is real art on disk that this mod does not add to.
ShinyPics.SUFFIX = "-SHINY"
-- ------- what the sprite hook notices
--
-- Called for every battle pic the engine resolves. Returns nothing: the point
-- is the note it leaves.
function ShinyPics.note(ctx)
pending = nil
if type(ctx) ~= "table" or ctx.kind ~= "battle" then return end
local mon = ctx.mon
if not (mon and Shiny.isShiny(mon)) then return end
local def = ctx.data and ctx.data.pokemon and ctx.data.pokemon[ctx.species]
local dex = def and def.dex
if not dex then return end
pending = { species = ctx.species, dex = dex }
end
-- Whether the pic currently being built is a shiny's -- for a test, and for
-- the palette wraps below.
function ShinyPics.pendingDex(species)
if pending and pending.species == species then return pending.dex end
return nil
end
-- ------- the palette wraps
--
-- Idempotent by sentinel, the pattern every wrap in this mod uses.
function ShinyPics.install()
local ok, PaletteFX = pcall(require, "src.render.PaletteFX")
if not ok or type(PaletteFX) ~= "table" then return false end
if PaletteFX.dramaticShapeShiny then return true end
local innerPal = PaletteFX.monPal
local innerName = PaletteFX.monPalName
if type(innerPal) ~= "function" or type(innerName) ~= "function" then
return false
end
function PaletteFX.monPal(data, species, transformed, ...)
local cols = innerPal(data, species, transformed, ...)
local dex = ShinyPics.pendingDex(species)
if not (cols and dex) then
-- nothing to recolour, and monPalette's early return means the name
-- wrap below may never run: drop the note here rather than leave it
-- for whoever asks next
if not cols then pending = nil end
return cols
end
local fn = ShinyPalette.paletteTransform(dex)
if not fn then return cols end
-- ------- the first and last shades DO NOT MOVE
--
-- A Game Boy mon palette is four shades and only the middle two are the
-- Pokemon. The first is the PAPER -- 255,239,255 in every species'
-- palette in the dataset, the white the pic sits on -- and the last is
-- the INK, 25,16,16, the outline every pic is drawn with. Both are shared
-- constants, not colours anybody chose for this animal.
--
-- Sliding them is what a shiny looks like when it is broken: shiny Golbat
-- rotates far enough that its white became NAVY (31,34,93) and the pic
-- read as a mon on a blue card rather than a green Golbat. Stadium's
-- slides were authored for model textures, which have no paper and no
-- outline in them, so there was nothing there to warn against it.
--
-- COPIED, never written through, for the rest. monPal hands back the
-- dataset's own palette table, and mutating it would recolour every
-- Pokemon of the species everywhere for the rest of the process -- the
-- same trap ShinyUI's summary wrap documents.
local last = #cols
local out = {}
for i, c in ipairs(cols) do
if type(c) == "table" and c[1] and i > 1 and i < last then
local r, g, b = fn(c[1], c[2], c[3])
out[i] = { r, g, b }
else
out[i] = c
end
end
return out
end
function PaletteFX.monPalName(data, species, ...)
local name = innerName(data, species, ...)
local dex = ShinyPics.pendingDex(species)
pending = nil -- consumed: one pic, one note
if not (name and dex) then return name end
if not ShinyPalette.paletteTransform(dex) then return name end
return name .. ShinyPics.SUFFIX
end
PaletteFX.dramaticShapeShiny = true
return true
end
return ShinyPics