diff --git a/src/mods/Schemas.lua b/src/mods/Schemas.lua index af9e8921..8f71680a 100644 --- a/src/mods/Schemas.lua +++ b/src/mods/Schemas.lua @@ -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 diff --git a/src/pokemon/Sprites.lua b/src/pokemon/Sprites.lua index 3b30819d..c849e4de 100644 --- a/src/pokemon/Sprites.lua +++ b/src/pokemon/Sprites.lua @@ -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 diff --git a/src/ui/PartyMenu.lua b/src/ui/PartyMenu.lua index 21699a14..a0e9809b 100644 --- a/src/ui/PartyMenu.lua +++ b/src/ui/PartyMenu.lua @@ -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/.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 diff --git a/tests/mod_graphics_tests.lua b/tests/mod_graphics_tests.lua index 8c56df3d..65f2c5dc 100644 --- a/tests/mod_graphics_tests.lua +++ b/tests/mod_graphics_tests.lua @@ -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") diff --git a/tests/mod_qol_hooks_tests.lua b/tests/mod_qol_hooks_tests.lua index ba37e3a1..97ac1e3a 100644 --- a/tests/mod_qol_hooks_tests.lua +++ b/tests/mod_qol_hooks_tests.lua @@ -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