DramaticShapeVoxelMod archival mirror · read-only
lib/ShinyPics.lua

ShinyPics.lua

lib/ShinyPics.lua6.4 KB1006442e102c553c
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
-- 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