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

ShinyUI.lua

lib/ShinyUI.lua8.7 KB100644a75d57807c
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 189 190 191 192 193 194 195 196 197 198 199 200
-- Where a shiny SHOWS on the flat art: the battle pics and the status page.
--
-- The Stadium models carry genuinely recoloured texels (see ShinyPalette and
-- the extraction). Everything drawn as a Game Boy pic cannot, because the
-- engine bakes a species palette into an image cache keyed by path and
-- palette name -- a cache with no notion of WHICH Rattata is being drawn. So
-- the flat side is answered two ways:
--
--   the battle pic     tinted at draw time, per side, with the multiply
--                      ShinyPalette.tintFor derives from that species' own
--                      shiny slide. Under ADVANCED (`redpp`, the pokered-gbc
--                      colour pack) the pic is at its most colourful and the
--                      tint reads clearly; under the DMG modes there is
--                      barely any colour to shift, which is why the status
--                      page also carries a plain, mode-proof MARK.
--   the status page    a star beside the level, drawn in the engine's own
--                      GB pixel grid so it is palette-processed like every
--                      other pixel on the screen rather than floating over
--                      the finished frame.
--
-- Both are monkeypatches, idempotent by sentinel, the pattern the rest of
-- this mod uses (OverworldBattle.install, Stadium.install). The summary
-- screen has no hook at all -- there is no `ui.summary.*` anywhere in the
-- engine -- so a wrap is the only route to it, and it is deliberately a thin
-- one: draw the engine's screen, then add one glyph.

-- 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 ShinyUI = {}

-- ------- the tint, applied to one draw
--
-- Lifted in shape from OverworldBattle.withTint, and for the same reason it
-- exists there: the pics layer sets its own colour many times over as it
-- draws (the faint slide's fade, the damage blink), so the way to tint the
-- result without clobbering any of that is to multiply every colour it sets
-- on its way past. Restored unconditionally, including on error, because a
-- leaked setColor would tint the entire rest of the frame.
function ShinyUI.withTint(tint, fn, ...)
  if not tint then return fn(...) end
  local r, g, b = tint[1] or 1, tint[2] or 1, tint[3] or 1
  if r > 0.999 and g > 0.999 and b > 0.999 then return fn(...) end
  local gfx = love.graphics
  local setColor = gfx.setColor
  gfx.setColor = function(cr, cg, cb, ca, ...)
    if type(cr) == "table" then
      return setColor({ (cr[1] or 1) * r, (cr[2] or 1) * g, (cr[3] or 1) * b,
                        cr[4] }, cg, ...)
    end
    if cr == nil then return setColor(cr, cg, cb, ca, ...) end
    return setColor(cr * r, (cg or 1) * g, (cb or 1) * b, ca, ...)
  end
  local ok, err = pcall(fn, ...)
  gfx.setColor = setColor
  setColor(1, 1, 1, 1)
  if not ok then error(err, 0) end
end

-- The tint for a mon, or nil when it is not shiny or we have no colours.
function ShinyUI.tintFor(mon, data)
  if not (mon and Shiny.isShiny(mon)) then return nil end
  local def = data and data.pokemon and data.pokemon[mon.species]
  local dex = def and def.dex
  if not dex then return nil end
  return ShinyPalette.tintFor(dex)
end

-- ------- the star
--
-- Drawn as rectangles rather than as a font character because the Game Boy
-- charmap has no star, and a letter would read as a typo. Four spokes and a
-- centre, in the same near-black the screen's text uses, so the palette pass
-- treats it exactly like a glyph -- under ADVANCED and under every DMG mode
-- alike.
--
-- Seven pixels square, which is the largest that fits the gap beside the
-- level without touching the DrawLineBox bracket at column 19.
function ShinyUI.drawStar(px, py)
  local g = love.graphics
  local r, gg, b, a = g.getColor()
  g.setColor(0, 0, 0, 1)
  -- vertical, horizontal, then the four diagonal nubs
  g.rectangle("fill", px + 3, py, 1, 7)
  g.rectangle("fill", px, py + 3, 7, 1)
  g.rectangle("fill", px + 1, py + 1, 1, 1)
  g.rectangle("fill", px + 5, py + 1, 1, 1)
  g.rectangle("fill", px + 1, py + 5, 1, 1)
  g.rectangle("fill", px + 5, py + 5, 1, 1)
  g.setColor(r, gg, b, a)
end

-- ------- install

function ShinyUI.install()
  ShinyUI.installSummary()
end

-- The status page. Wraps the draw and adds the star afterwards, so the
-- engine's own layout is untouched and a layout change upstream costs us
-- the glyph's position and nothing else.
function ShinyUI.installSummary()
  local ok, SummaryMenu = pcall(require, "src.ui.SummaryMenu")
  if not ok or type(SummaryMenu) ~= "table" then return end
  if SummaryMenu.dramaticShapeShiny then return end
  local inner = SummaryMenu.draw
  if type(inner) ~= "function" then return end

  function SummaryMenu:draw(...)
    local out = { inner(self, ...) }
    -- page 1 only: page 2 wipes the block the level sits in
    -- (status_screen.asm ClearScreenArea over (9,2)), so a mark left there
    -- would be half-erased by the engine's own clear.
    if self.page == 1 and Shiny.isShiny(self.mon) then
      -- beside PrintLevel at (14,2): column 13, row 2, in the gap the
      -- level's own leading space leaves
      pcall(ShinyUI.drawStar, 104, 17)
    end
    return unpack(out)
  end

  -- The summary PIC, through its PALETTE.
  --
  -- Recolouring the sprite's pixels here does NOT work, and it is worth
  -- writing down why rather than leaving it to be re-attempted: the summary
  -- art is four-shade DMG grey, and the screen's colour is applied
  -- afterwards by the palette pass over the finished frame. Whatever RGB is
  -- put in the ImageData is remapped away by it. The colour of that pic
  -- lives in the palette and nowhere else, so the palette is what has to
  -- move. (Tried it, shot it, reverted it.)
  --
  -- This is the ADVANCED-palette answer, and it is better than a multiply:
  -- SetPal_StatusScreen hands the pic zone the species palette
  -- (PaletteFX.monPal), and sgbPalettes is a method on the MENU, so unlike
  -- the battle pic's image cache it knows which individual is on screen.
  -- Running those four colours through the species' own shiny transform
  -- gives the summary a genuinely recoloured Pokemon -- brightening
  -- included, which a draw-colour multiply cannot do.
  --
  -- Only ZONE entries are touched. The first palette the engine returns is
  -- the whole-screen HP-bar one, and rotating that would recolour the text.
  local innerPal = SummaryMenu.sgbPalettes
  if type(innerPal) == "function" then
    function SummaryMenu:sgbPalettes(game, ...)
      local out = innerPal(self, game, ...)
      if type(out) ~= "table" or not Shiny.isShiny(self.mon) then return out end
      local def = game and game.data and game.data.pokemon
                  and game.data.pokemon[self.mon.species]
      local fn = def and def.dex
                 and ShinyPalette.transform(ShinyPalette.forDex(def.dex))
      if not fn then return out end
      for _, z in ipairs(out) do
        if type(z) == "table" and z.w and z.h and type(z.colors) == "table" then
          -- copied, never mutated in place: monPal hands back the dataset's
          -- own palette table, and writing through it would recolour every
          -- Pokemon of the species everywhere for the rest of the process
          local cols = {}
          for i, c in ipairs(z.colors) do
            if type(c) == "table" and c[1] then
              local r, g, b = fn(c[1], c[2], c[3])
              cols[i] = { r, g, b }
            else
              cols[i] = c
            end
          end
          z.colors = cols
        end
      end
      return out
    end
  end

  SummaryMenu.dramaticShapeShiny = true
end

-- ------- the battle pics are NOT tinted here any more
--
-- There used to be a third wrap in this file: a multiply over
-- BattleState:drawPicsLayer, with the tint above. It is gone, and the reason
-- is worth keeping so it is not put back.
--
-- A multiply can only DARKEN. Shiny Gyarados is blue turning red, and the
-- nearest a multiply gets to that is a dimmer blue -- so every species whose
-- shiny is lighter, or is a rotation rather than a dimming, read as the
-- ordinary one with the brightness down. And the engine's pic layer draws
-- BOTH sides in one call, so a shiny also dimmed the ordinary mon opposite it.
--
-- lib/ShinyPics.lua replaces it by moving the PALETTE instead, which is where
-- a battle pic's colour actually lives: getImage bakes the four DMG shades
-- into the species palette once and caches the result, so handing that bake a
-- shiny palette (under a cache name of its own) gives a genuinely recoloured
-- pic -- brightening included -- for one side alone.
--
-- ShinyUI.withTint and ShinyUI.tintFor stay: the 3D path still uses them for
-- the per-side canvas, and they are the only tint left in the mod.

return ShinyUI