mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 11:20:50 +02:00
6b4dfe6b9b
Four things, all found by shooting the feature rather than by reading it. The flat-pic tint was a no-op for the five colour-table species. tintFor ran synthetic reference colours through the lookup, none of them were IN it, so every ratio came back 1 and the tint was discarded -- a shiny Gyarados drew an ordinary blue pic. Those species now measure the tint from the table's own entries. It was also a no-op for most SLIDE species, for a better-hidden reason: a hue rotation moves red toward cyan and cyan toward red, so averaged over a balanced set of references the ratios cancel and every species reports no tint. Charizard and Ponyta both came back neutral. The tint is now measured against the colour each species is actually MADE of -- a modal body colour, generated into data/shiny_colors.lua as `dom`. 132 of 151 now carry a usable tint; the rest genuinely shift too little for one to mean anything. The sparkle drew sixty quads a frame that nobody could see. It was built to numbers a tenth of the scale of a mon card -- a ring seven units across, inside a Gyarados -- and additive drawing keeps the depth test, so all of it was rejected. Sized against the card now, and pulled toward the camera the way the move-animation card is. And the summary PIC cannot be recoloured by touching pixels: the art is four-shade DMG grey and the colour is applied afterwards by the palette pass. That attempt is reverted, with the reason left where the next person will look for it. The star is the designation that works there. Also: tools/shiny_colors.py now resolves its own paths instead of hardcoding a worktree, and the extract test gained a hard failure when NOTHING recolours -- which is what a missing colour table looks like from the outside, and it passed through it once already. Evidence in .claude/shiny_update, every case beside its own control.
228 lines
9.9 KiB
Lua
228 lines
9.9 KiB
Lua
-- 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
|