From 53e1a535ddea86c8d3fe44c59be0a36f06abd2ec Mon Sep 17 00:00:00 2001 From: johnjohto Date: Wed, 29 Jul 2026 23:51:05 -0400 Subject: [PATCH] Support custom trainer palettes and base portraits --- src/battle/BattleState.lua | 24 +++++++++++++++++++++++- src/mods/Schemas.lua | 9 +++++++++ src/render/PaletteFX.lua | 2 +- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/battle/BattleState.lua b/src/battle/BattleState.lua index ad0e5e2d..b6eb4821 100644 --- a/src/battle/BattleState.lua +++ b/src/battle/BattleState.lua @@ -187,6 +187,27 @@ local function namedPalette(data, name) return { name = key, colors = colors } end +-- Custom trainer portraits can opt into the same Advanced OBJ palette source +-- as their overworld walker. Vanilla trainers preserve the hardware-faithful +-- MEWMON fallback used during the battle introduction. +function BattleState.trainerPalette(data, trainer) + local source = trainer and trainer.paletteSource + if source then + local PaletteFX = require("src.render.PaletteFX") + local colors, group = PaletteFX.spriteObp({ paletteSource = source }, trainer.id) + if colors then + return { name = "trainer:" .. source .. ":" .. tostring(group), colors = colors } + end + end + return namedPalette(data, "MEWMON") +end + +function BattleState.trainerPicPath(data, trainer) + if trainer and trainer.pic then return trainer.pic end + local base = trainer and trainer.basePic and data.trainers[trainer.basePic] + return base and base.pic or nil +end + -- The battle-BGP fade variant of a pic (AnimationFlashScreen and the -- SetAnimationBGPalette effects remap the four BG shades; on the SGB -- the colorizer then colors the REMAPPED shade, so a faded pic shows @@ -607,7 +628,8 @@ function BattleState.newTrainer(game, oppClass, partyIndex) -- MonsterPalettes[0] = PAL_MEWMON -- InitBattleCommon zeroes -- wEnemyMonSpecies2 before the intro's SET_PAL_BATTLE -- (engine/battle/core.asm:6682, engine/gfx/palettes.asm SetPal_Battle) - self.trainerPic = getImage(self.trainer.pic, namedPalette(game.data, "MEWMON")) + self.trainerPic = getImage(BattleState.trainerPicPath(game.data, self.trainer), + BattleState.trainerPalette(game.data, self.trainer)) self.introText = Strings("%s wants\nto fight!", self.trainer.name) return self end diff --git a/src/mods/Schemas.lua b/src/mods/Schemas.lua index c73aa09b..7e0f7490 100644 --- a/src/mods/Schemas.lua +++ b/src/mods/Schemas.lua @@ -555,6 +555,11 @@ R.trainers = { index = f.opt(f.int(0, 255)), -- unused vanilla classes ship without a pic, so it cannot be required pic = f.opt(f.path), + -- Optional Advanced-mode OBJ palette source for a custom trainer portrait. + -- It follows the same ROM crosswalk form as sprites.paletteSource. + paletteSource = f.opt(f.str), + -- Reuse a base trainer class's portrait without redistributing its asset. + basePic = f.opt(f.id("trainers")), baseMoney = f.opt(f.int(0)), parties = f.list(f.list(f.rec{ level = f.int(1), species = f.id("pokemon") })), @@ -574,6 +579,10 @@ R.sprites = { frames = f.int(1), walker = f.opt(f.bool), trueColor = f.opt(f.bool), + -- Mod art can opt into an existing ROM sprite's Advanced-mode OBJ + -- palette assignment without claiming that the image itself came from + -- the ROM (which is what `source` documents on imported records). + paletteSource = f.opt(f.str), }, example = 'mod.content.sprites:register("SPRITE_HERO", { image = "...", frames = 6 })', } diff --git a/src/render/PaletteFX.lua b/src/render/PaletteFX.lua index 4891f446..da89c91a 100644 --- a/src/render/PaletteFX.lua +++ b/src/render/PaletteFX.lua @@ -518,7 +518,7 @@ end function PaletteFX.spriteObp(spriteDef, seed) local pack = PaletteFX.gbcPack() local w = pack and pack.world - local src = spriteDef and spriteDef.source + local src = spriteDef and (spriteDef.paletteSource or spriteDef.source) if not (w and src) then return nil end local idx = tonumber(src:match("%[(%d+)%]")) -- RedBikeSprite loads outside SpriteSheetPointerTable