Merge pull request #2259 from Code-Grub/truecolor-menu-icons

This commit is contained in:
bryanthaboi
2026-09-12 08:07:03 -04:00
committed by GitHub
5 changed files with 115 additions and 13 deletions
+7 -2
View File
@@ -1819,14 +1819,19 @@ end
R.icons = {
semantics = "record", target = "icons.bySpecies",
value = f.union{ f.str, f.rec{ image = f.path, frames = f.opt(f.int(1)) } },
-- trueColor is the same 4-shade opt-out the pokemon and trainers records
-- carry: flagged art skips the OBP bake and reports its rect so the SGB
-- pass re-blits it unshaded, instead of being bucketed by red channel.
value = f.union{ f.str, f.rec{ image = f.path, frames = f.opt(f.int(1)),
trueColor = f.opt(f.bool) } },
gen2Value = f.union{
-- the assignment form: a species id mapped to a sheet name
f.str,
-- the sheet form: width/height are the sheet's pixel size, and every
-- vanilla sheet is a 16x32 two-frame strip
f.rec{ id = f.opt(f.str), index = f.opt(f.int(0, 255)), image = f.path,
width = f.int(1), height = f.int(1), frames = f.int(1) },
width = f.int(1), height = f.int(1), frames = f.int(1),
trueColor = f.opt(f.bool) },
},
gen2Extra = function(id, value)
if gen2IconIsSheet(id) then
+14 -6
View File
@@ -113,11 +113,17 @@ end
-- Resolve a party-menu icon image path for `mon`.
-- vanillaPath is the path PartyMenu already picked from icons.bySpecies /
-- def.icon / icons.byDex; the hook may replace it.
-- Returns path (possibly nil).
-- opts.trueColor is the icon record's own flag: an icons.bySpecies table
-- entry may carry one, the way a pokemon record carries it for a battle pic.
-- A hook may also set ctx.trueColor, to flag art it substitutes for art that
-- carried no flag -- the same ctx contract pokemon.sprite has, and the reason
-- the flag is read back off ctx rather than trusted from opts alone.
-- Returns path (possibly nil), trueColor.
function Sprites.iconPath(data, mon, vanillaPath, opts)
opts = opts or {}
local trueColor = opts.trueColor and true or false
if not vanillaPath and not Runtime.wantsHook("pokemon.icon") then
return vanillaPath
return vanillaPath, trueColor
end
local species = mon and mon.species
local ctx = {
@@ -126,12 +132,14 @@ function Sprites.iconPath(data, mon, vanillaPath, opts)
name = opts.name,
data = data,
kind = "icon",
trueColor = trueColor,
}
if not Runtime.wantsHook("pokemon.icon") then return vanillaPath end
if not Runtime.wantsHook("pokemon.icon") then return vanillaPath, trueColor end
local hooked = Runtime.call("pokemon.icon", samePath, vanillaPath, ctx)
if type(hooked) == "string" and hooked ~= "" then return hooked end
if hooked == nil or hooked == false then return nil end
return vanillaPath
trueColor = ctx.trueColor and true or false
if type(hooked) == "string" and hooked ~= "" then return hooked, trueColor end
if hooked == nil or hooked == false then return nil, false end
return vanillaPath, trueColor
end
return Sprites
+23 -4
View File
@@ -221,18 +221,20 @@ function PartyMenu.drawIcon(game, mon, x, y, selected, counter, forceAlt)
-- change its menu icon.
local entry = (icons.bySpecies and icons.bySpecies[mon.species])
or (def and def.icon)
local name, path
local name, path, trueColor
if type(entry) == "string" then
name = entry
path = icons.icons and icons.icons[entry]
elseif type(entry) == "table" then
path = entry.image
trueColor = entry.trueColor
end
if not path then
name = def and def.dex and icons.byDex and icons.byDex[def.dex]
path = name and icons.icons and icons.icons[name]
end
path = require("src.pokemon.Sprites").iconPath(game.data, mon, path, { name = name })
path, trueColor = require("src.pokemon.Sprites")
.iconPath(game.data, mon, path, { name = name, trueColor = trueColor })
if not path then return end
-- Built-in icon classes are DMG 2bpp OBJ art and get the OBP0 bake; a
-- mod's own image (an entry table rather than an icon name) is authored
@@ -240,13 +242,20 @@ function PartyMenu.drawIcon(game, mon, x, y, selected, counter, forceAlt)
-- split PartyMenu.mirrorsIcon makes for the OAM mirror. Both live in one
-- cache under different keys, so a mod pointing a table entry at a
-- built-in path still gets its unbaked copy. #274
local key = name and (path .. "#obp") or path
--
-- trueColor art is unbaked for the same reason it is unshaded: obpIcon is
-- itself a 4-shade remap keyed off the red channel, so running it over
-- full-colour art destroys exactly what the flag asks to keep. The flag
-- overrides `name`, because a pokemon.icon hook can substitute full-colour
-- art for a path that resolved to a built-in class and still carries one.
local baked = name ~= nil and not trueColor
local key = baked and (path .. "#obp") or path
if iconImages[key] == nil then
-- resolve through Assets so an overrides/ or transform-derived icon
-- (e.g. a per-species image at assets/generated/icons/<name>.png) is
-- picked up the same way battle sprites are
local ok, img
if name then
if baked then
ok, img = pcall(obpIcon, path)
else
ok, img = pcall(love.graphics.newImage, Assets.resolve(path))
@@ -290,6 +299,16 @@ function PartyMenu.drawIcon(game, mon, x, y, selected, counter, forceAlt)
-- whatever size the file is (unchanged path)
love.graphics.draw(img, x, y)
end
-- Report the covering rect so Renderer:endFrame can re-blit it unshaded
-- over the colorized pass. Every branch above lays a frame into a 16x16
-- OAM block except the last, which draws the file at its own size.
-- No vanilla icon record sets the flag, so this stays dead code without a
-- mod and the zone lists are exactly the ones the states returned.
if trueColor then
local mw, mh = 16, 16
if not (PartyMenu.mirrorsIcon(name) or ih > 16) then mw, mh = iw, ih end
require("src.render.PaletteFX").markTrueColor(x, y, mw, mh)
end
return true
end
+48 -1
View File
@@ -143,7 +143,8 @@ local savedLoaded = {}
for _, name in ipairs({ "src.render.PaletteFX", "src.render.Renderer",
"src.render.Font", "src.render.Assets",
"src.render.SpriteRenderer",
"src.render.TileRenderer", "src.mods.Loader" }) do
"src.render.TileRenderer", "src.mods.Loader",
"src.ui.PartyMenu" }) do
savedLoaded[name] = package.loaded[name]
package.loaded[name] = nil
end
@@ -157,6 +158,7 @@ local Font = require("src.render.Font")
local Hooks = require("src.mods.Hooks")
local HudTiles = require("src.render.HudTiles")
local PaletteFX = require("src.render.PaletteFX")
local PartyMenu = require("src.ui.PartyMenu")
local Registry = require("src.mods.Registry")
local Renderer = require("src.render.Renderer")
local Runtime = require("src.mods.Runtime")
@@ -409,6 +411,51 @@ check(BattleState.trainerTrueColor(trainerPicData,
"a basePic reuse inherits the base portrait's trueColor flag")
PaletteFX.setMode(savedColors)
-- ------- trueColor: mon menu icons take the same opt-out
-- Icons were the one art class that never had it. Seven records in
-- Schemas carry trueColor; icons.bySpecies did not, and Sprites.iconPath
-- returned a path alone where every sibling returns path, trueColor. So
-- no icon draw site could know its art was full colour, none reported a
-- rect, and on any screen declaring an SGB zone the art met the shade-remap
-- shader -- which buckets on the RED channel, so a warm pixel clears 0.83
-- and comes back c0, white in every named palette.
do
local iconData = {
pokemon = { PLAINMON = { dex = 1 }, FULLMON = { dex = 2 } },
icons = {
icons = { QUADRUPED = "assets/generated/icons/mon/quadruped.png" },
byDex = { [1] = "QUADRUPED", [2] = "QUADRUPED" },
bySpecies = {
FULLMON = { image = "mods/skin/full_icon.png", trueColor = true },
},
},
}
local game = { data = iconData }
PaletteFX.clearTrueColor()
PaletteFX.setPass("ui")
PartyMenu.drawIcon(game, { species = "PLAINMON" }, 24, 40, false, 0, false)
check(#PaletteFX.trueColorRects("ui") == 0,
"a built-in icon class reports no trueColor rect")
resetLog()
PartyMenu.drawIcon(game, { species = "FULLMON" }, 24, 40, false, 0, false)
local rects = PaletteFX.trueColorRects("ui")
check(#rects == 1 and rects[1].x == 24 and rects[1].y == 40
and rects[1].w == 16 and rects[1].h == 16,
"a trueColor icon reports its rectangle so the shader skips it")
-- the OBP bake is itself a 4-shade remap (obpIcon keys off the red
-- channel exactly as the shader does), so full-colour art must not take
-- it either. A baked icon is built from ImageData and carries .data;
-- art loaded straight from its path does not.
check(log.draws[1] and log.draws[1].what and log.draws[1].what.data == nil,
"and it is not run through the OBP bake")
PaletteFX.setPass(nil)
PaletteFX.clearTrueColor()
end
-- ------- trueColor: the colors == false zone sentinel
check(PaletteFX.zone(nil, 0, 0, 1, 1) == nil, "nil colors is still no zone")
+23
View File
@@ -448,6 +448,29 @@ do
check(Sprites.iconPath(data, mon, "assets/generated/icons/mon/quadruped.png")
== "assets/generated/icons/mon/quadruped.png",
"unwrapped pokemon.icon is vanilla")
-- the icon seam carries the same trueColor contract the sprite seam does:
-- a hook substituting full-colour art flags it on ctx, and the flag comes
-- back beside the path so the draw site can skip the OBP bake and report
-- the rect. Without it a pack's icons are bucketed by red channel and
-- come out white under any screen that declares an SGB zone.
unsub = wrap("pokemon.icon", function(next, p, ctx)
if ctx.mon and ctx.mon.skin == "alt" then
ctx.trueColor = true
return "mods/skinpicker/assets/pika_icon_hd.png"
end
return next(p, ctx)
end)
local hdIcon, hdTC = Sprites.iconPath(data, mon,
"assets/generated/icons/mon/quadruped.png", { name = "QUADRUPED" })
check(hdIcon == "mods/skinpicker/assets/pika_icon_hd.png" and hdTC == true,
"pokemon.icon can swap path + trueColor from mon state")
local plainIcon, plainTC = Sprites.iconPath(data, { species = "PIKACHU" },
"assets/generated/icons/mon/quadruped.png", { name = "QUADRUPED" })
check(plainIcon == "assets/generated/icons/mon/quadruped.png"
and plainTC == false,
"and an unflagged icon stays unflagged through the same hook")
unsub()
end