-- 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() ShinyUI.installBattlePics() 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. The engine's pic layer is reached through -- BattleState:drawPicsLayer, which draws BOTH sides in one call -- so a -- per-side tint has to bracket each side separately, which is exactly what -- OverworldBattle.sideTexture already does when it renders one side at a -- time into its own canvas. That is where the tint belongs on the 3D path; -- this wrap covers the FLAT path, where the engine draws the battle itself. function ShinyUI.installBattlePics() local ok, BattleState = pcall(require, "src.battle.BattleState") if not ok or type(BattleState) ~= "table" then return end if BattleState.dramaticShapeShinyPics then return end local inner = BattleState.drawPicsLayer if type(inner) ~= "function" then return end function BattleState:drawPicsLayer(...) -- THE 3D PATH HAS ALREADY DONE THIS, per side and better: when the mod -- is rendering one side into its own canvas it brackets that draw with -- that side's own tint (OverworldBattle.sideTexture). Tinting again here -- would square it. Asked as a question rather than left to install -- order, because both wraps are installed from main.lua and whichever -- ran first would otherwise silently decide the outcome. local okOw, Ow = pcall(V.require, "OverworldBattle") if okOw and Ow and Ow.texturingSide and Ow.texturingSide() then return inner(self, ...) end -- Both sides at once, so when they disagree the tint cannot be applied -- per-side here without splitting the engine's own draw. When only ONE -- side is shiny we tint the whole layer by it: the other side's pic is -- dimmed slightly, which is far less wrong than a shiny drawn in its -- ordinary colours -- and when both are shiny each gets the mean. local data = self.game and self.game.data local a = self.player and ShinyUI.tintFor(self.player.mon, data) local b = self.enemy and ShinyUI.tintFor(self.enemy.mon, data) local tint = a or b if a and b then tint = { (a[1] + b[1]) / 2, (a[2] + b[2]) / 2, (a[3] + b[3]) / 2 } end if not tint then return inner(self, ...) end local args = { ... } local out ShinyUI.withTint(tint, function() out = { inner(self, unpack(args)) } end) return unpack(out or {}) end BattleState.dramaticShapeShinyPics = true end return ShinyUI